@abstraxn/signer-react 3.3.8 → 3.3.10

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/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.3.10] - 2026-09-18
9
+
10
+ ### Fixed
11
+ - **Mobile WalletConnect reject / confirm / on-chain failure** (aligned with WC + EIP-1193):
12
+ - **Reject:** On return from MetaMask, force-resume the relay (time-bounded). Map EIP-1193 `4001` / WC `5000` to `User rejected the request`. If WC stays silent and pending nonce never moved, surface the same reject error so loaders clear.
13
+ - **Confirm:** WC success is a **tx hash** only. Gated chain poll recovers the hash when the relay drops after approve (success-only; does not invent rejects).
14
+ - **On-chain failure:** `useWaitForTxnReceipt` defaults to `throwOnRevert: true` so a mined `status: reverted` throws `Transaction reverted` (receipt check is dapp/RPC, not WalletConnect).
15
+
16
+ ### Added
17
+ - `isUserRejectionError`, `toUserRejectionError`, `forceResumeWalletConnectOnReturn`.
18
+
19
+ ## [3.3.9] - 2026-09-17
20
+
21
+ ### Fixed
22
+ - **Mobile WalletConnect `writeContract` loader stuck after MetaMask confirm** – On iOS/Android the browser often drops the WalletConnect relay while MetaMask is open, so `eth_sendTransaction` never returns even though the tx is on-chain. `useWriteContract` now races the wallet call against an RPC scan for the matching outgoing contract tx (`from` / `to` / function selector) and clears pending WC mobile flags when the hash is recovered that way.
23
+ - **Stacked “Open in MetaMask?” / “All Wallets” after an already-connected session** – Auto-deeplink and WalletConnect modal restore are skipped when a WC session already exists; only the connect flow may schedule a single wallet open. Connected sessions hide the picker instead of reopening it.
24
+
25
+ ### Changed
26
+ - **Chain-poll recovery is gated** – The on-chain hash poll runs only for WalletConnect when the device is mobile or the document is `hidden`. Desktop injected wallets and idle desktop WC keep the plain `writeContract` path (no always-on poll).
27
+ - **`gasLimit` on `useWriteContract`** – Mapped to viem’s `gas` option on the direct `walletClient.writeContract` path so callers that pass `gasLimit` are honored.
28
+
29
+ ### Added
30
+ - Public helpers: `pollForOutgoingTx`, `clearWalletConnectPendingState`, `waitUntilDocumentVisible`, `isWalletConnectConnector`, `isMobileDevice`.
31
+
8
32
  ## [3.3.8] - 2026-09-16
9
33
 
10
34
  ### Fixed
@@ -360,15 +360,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
360
360
  transactions: includeTransactions extends true ? ({
361
361
  input: import("viem").Hex;
362
362
  s: import("viem").Hex;
363
- type: "legacy";
363
+ from: Address;
364
+ to: Address | null;
364
365
  value: bigint;
366
+ hash: import("viem").Hash;
367
+ type: "legacy";
365
368
  yParity?: undefined | undefined;
366
- from: Address;
367
369
  gas: bigint;
368
- hash: import("viem").Hash;
369
370
  nonce: number;
370
371
  r: import("viem").Hex;
371
- to: Address | null;
372
372
  typeHex: import("viem").Hex | null;
373
373
  v: bigint;
374
374
  accessList?: undefined | undefined;
@@ -385,15 +385,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
385
385
  } | {
386
386
  input: import("viem").Hex;
387
387
  s: import("viem").Hex;
388
- type: "eip2930";
388
+ from: Address;
389
+ to: Address | null;
389
390
  value: bigint;
391
+ hash: import("viem").Hash;
392
+ type: "eip2930";
390
393
  yParity: number;
391
- from: Address;
392
394
  gas: bigint;
393
- hash: import("viem").Hash;
394
395
  nonce: number;
395
396
  r: import("viem").Hex;
396
- to: Address | null;
397
397
  typeHex: import("viem").Hex | null;
398
398
  v: bigint;
399
399
  accessList: import("viem").AccessList;
@@ -410,15 +410,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
410
410
  } | {
411
411
  input: import("viem").Hex;
412
412
  s: import("viem").Hex;
413
- type: "eip1559";
413
+ from: Address;
414
+ to: Address | null;
414
415
  value: bigint;
416
+ hash: import("viem").Hash;
417
+ type: "eip1559";
415
418
  yParity: number;
416
- from: Address;
417
419
  gas: bigint;
418
- hash: import("viem").Hash;
419
420
  nonce: number;
420
421
  r: import("viem").Hex;
421
- to: Address | null;
422
422
  typeHex: import("viem").Hex | null;
423
423
  v: bigint;
424
424
  accessList: import("viem").AccessList;
@@ -435,15 +435,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
435
435
  } | {
436
436
  input: import("viem").Hex;
437
437
  s: import("viem").Hex;
438
- type: "eip4844";
438
+ from: Address;
439
+ to: Address | null;
439
440
  value: bigint;
441
+ hash: import("viem").Hash;
442
+ type: "eip4844";
440
443
  yParity: number;
441
- from: Address;
442
444
  gas: bigint;
443
- hash: import("viem").Hash;
444
445
  nonce: number;
445
446
  r: import("viem").Hex;
446
- to: Address | null;
447
447
  typeHex: import("viem").Hex | null;
448
448
  v: bigint;
449
449
  accessList: import("viem").AccessList;
@@ -460,15 +460,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
460
460
  } | {
461
461
  input: import("viem").Hex;
462
462
  s: import("viem").Hex;
463
- type: "eip7702";
463
+ from: Address;
464
+ to: Address | null;
464
465
  value: bigint;
466
+ hash: import("viem").Hash;
467
+ type: "eip7702";
465
468
  yParity: number;
466
- from: Address;
467
469
  gas: bigint;
468
- hash: import("viem").Hash;
469
470
  nonce: number;
470
471
  r: import("viem").Hex;
471
- to: Address | null;
472
472
  typeHex: import("viem").Hex | null;
473
473
  v: bigint;
474
474
  accessList: import("viem").AccessList;
@@ -510,15 +510,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
510
510
  getTransaction: <blockTag extends import("viem").BlockTag = "latest">(args: import("viem").GetTransactionParameters<blockTag>) => Promise<{
511
511
  input: import("viem").Hex;
512
512
  s: import("viem").Hex;
513
- type: "legacy";
513
+ from: Address;
514
+ to: Address | null;
514
515
  value: bigint;
516
+ hash: import("viem").Hash;
517
+ type: "legacy";
515
518
  yParity?: undefined | undefined;
516
- from: Address;
517
519
  gas: bigint;
518
- hash: import("viem").Hash;
519
520
  nonce: number;
520
521
  r: import("viem").Hex;
521
- to: Address | null;
522
522
  typeHex: import("viem").Hex | null;
523
523
  v: bigint;
524
524
  accessList?: undefined | undefined;
@@ -535,15 +535,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
535
535
  } | {
536
536
  input: import("viem").Hex;
537
537
  s: import("viem").Hex;
538
- type: "eip2930";
538
+ from: Address;
539
+ to: Address | null;
539
540
  value: bigint;
541
+ hash: import("viem").Hash;
542
+ type: "eip2930";
540
543
  yParity: number;
541
- from: Address;
542
544
  gas: bigint;
543
- hash: import("viem").Hash;
544
545
  nonce: number;
545
546
  r: import("viem").Hex;
546
- to: Address | null;
547
547
  typeHex: import("viem").Hex | null;
548
548
  v: bigint;
549
549
  accessList: import("viem").AccessList;
@@ -560,15 +560,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
560
560
  } | {
561
561
  input: import("viem").Hex;
562
562
  s: import("viem").Hex;
563
- type: "eip1559";
563
+ from: Address;
564
+ to: Address | null;
564
565
  value: bigint;
566
+ hash: import("viem").Hash;
567
+ type: "eip1559";
565
568
  yParity: number;
566
- from: Address;
567
569
  gas: bigint;
568
- hash: import("viem").Hash;
569
570
  nonce: number;
570
571
  r: import("viem").Hex;
571
- to: Address | null;
572
572
  typeHex: import("viem").Hex | null;
573
573
  v: bigint;
574
574
  accessList: import("viem").AccessList;
@@ -585,15 +585,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
585
585
  } | {
586
586
  input: import("viem").Hex;
587
587
  s: import("viem").Hex;
588
- type: "eip4844";
588
+ from: Address;
589
+ to: Address | null;
589
590
  value: bigint;
591
+ hash: import("viem").Hash;
592
+ type: "eip4844";
590
593
  yParity: number;
591
- from: Address;
592
594
  gas: bigint;
593
- hash: import("viem").Hash;
594
595
  nonce: number;
595
596
  r: import("viem").Hex;
596
- to: Address | null;
597
597
  typeHex: import("viem").Hex | null;
598
598
  v: bigint;
599
599
  accessList: import("viem").AccessList;
@@ -610,15 +610,15 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
610
610
  } | {
611
611
  input: import("viem").Hex;
612
612
  s: import("viem").Hex;
613
- type: "eip7702";
613
+ from: Address;
614
+ to: Address | null;
614
615
  value: bigint;
616
+ hash: import("viem").Hash;
617
+ type: "eip7702";
615
618
  yParity: number;
616
- from: Address;
617
619
  gas: bigint;
618
- hash: import("viem").Hash;
619
620
  nonce: number;
620
621
  r: import("viem").Hex;
621
- to: Address | null;
622
622
  typeHex: import("viem").Hex | null;
623
623
  v: bigint;
624
624
  accessList: import("viem").AccessList;
@@ -8165,11 +8165,12 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8165
8165
  [x: `bytes3[${string}]`]: undefined;
8166
8166
  [x: `bytes1[${string}]`]: undefined;
8167
8167
  [x: `bytes2[${string}]`]: undefined;
8168
+ [x: `bytes16[${string}]`]: undefined;
8169
+ [x: `bytes8[${string}]`]: undefined;
8168
8170
  [x: `bytes4[${string}]`]: undefined;
8169
8171
  [x: `bytes5[${string}]`]: undefined;
8170
8172
  [x: `bytes6[${string}]`]: undefined;
8171
8173
  [x: `bytes7[${string}]`]: undefined;
8172
- [x: `bytes8[${string}]`]: undefined;
8173
8174
  [x: `bytes9[${string}]`]: undefined;
8174
8175
  [x: `bytes10[${string}]`]: undefined;
8175
8176
  [x: `bytes11[${string}]`]: undefined;
@@ -8177,7 +8178,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8177
8178
  [x: `bytes13[${string}]`]: undefined;
8178
8179
  [x: `bytes14[${string}]`]: undefined;
8179
8180
  [x: `bytes15[${string}]`]: undefined;
8180
- [x: `bytes16[${string}]`]: undefined;
8181
8181
  [x: `bytes17[${string}]`]: undefined;
8182
8182
  [x: `bytes18[${string}]`]: undefined;
8183
8183
  [x: `bytes19[${string}]`]: undefined;
@@ -8195,8 +8195,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8195
8195
  [x: `bytes31[${string}]`]: undefined;
8196
8196
  [x: `bytes32[${string}]`]: undefined;
8197
8197
  [x: `int[${string}]`]: undefined;
8198
- [x: `int8[${string}]`]: undefined;
8199
8198
  [x: `int16[${string}]`]: undefined;
8199
+ [x: `int8[${string}]`]: undefined;
8200
8200
  [x: `int24[${string}]`]: undefined;
8201
8201
  [x: `int32[${string}]`]: undefined;
8202
8202
  [x: `int40[${string}]`]: undefined;
@@ -8228,8 +8228,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8228
8228
  [x: `int248[${string}]`]: undefined;
8229
8229
  [x: `int256[${string}]`]: undefined;
8230
8230
  [x: `uint[${string}]`]: undefined;
8231
- [x: `uint8[${string}]`]: undefined;
8232
8231
  [x: `uint16[${string}]`]: undefined;
8232
+ [x: `uint8[${string}]`]: undefined;
8233
8233
  [x: `uint24[${string}]`]: undefined;
8234
8234
  [x: `uint32[${string}]`]: undefined;
8235
8235
  [x: `uint40[${string}]`]: undefined;
@@ -8267,11 +8267,12 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8267
8267
  bytes3?: undefined;
8268
8268
  bytes1?: undefined;
8269
8269
  bytes2?: undefined;
8270
+ bytes16?: undefined;
8271
+ bytes8?: undefined;
8270
8272
  bytes4?: undefined;
8271
8273
  bytes5?: undefined;
8272
8274
  bytes6?: undefined;
8273
8275
  bytes7?: undefined;
8274
- bytes8?: undefined;
8275
8276
  bytes9?: undefined;
8276
8277
  bytes10?: undefined;
8277
8278
  bytes11?: undefined;
@@ -8279,7 +8280,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8279
8280
  bytes13?: undefined;
8280
8281
  bytes14?: undefined;
8281
8282
  bytes15?: undefined;
8282
- bytes16?: undefined;
8283
8283
  bytes17?: undefined;
8284
8284
  bytes18?: undefined;
8285
8285
  bytes19?: undefined;
@@ -8296,8 +8296,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8296
8296
  bytes30?: undefined;
8297
8297
  bytes31?: undefined;
8298
8298
  bytes32?: undefined;
8299
- int8?: undefined;
8300
8299
  int16?: undefined;
8300
+ int8?: undefined;
8301
8301
  int24?: undefined;
8302
8302
  int32?: undefined;
8303
8303
  int40?: undefined;
@@ -8328,8 +8328,8 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8328
8328
  int240?: undefined;
8329
8329
  int248?: undefined;
8330
8330
  int256?: undefined;
8331
- uint8?: undefined;
8332
8331
  uint16?: undefined;
8332
+ uint8?: undefined;
8333
8333
  uint24?: undefined;
8334
8334
  uint32?: undefined;
8335
8335
  uint40?: undefined;
@@ -8811,10 +8811,16 @@ export declare function useSignAndSendTxn(provider: PublicClient): {
8811
8811
  * ```
8812
8812
  */
8813
8813
  export declare function useWaitForTxnReceipt(provider: PublicClient): {
8814
- waitForTxnReceipt: ({ hash, confirmations, timeout, }: {
8814
+ waitForTxnReceipt: ({ hash, confirmations, timeout, throwOnRevert, }: {
8815
8815
  hash: `0x${string}`;
8816
8816
  confirmations?: number;
8817
8817
  timeout?: number;
8818
+ /**
8819
+ * When true (default), a mined receipt with status reverted throws
8820
+ * `Transaction reverted` — on-chain failure is not a WalletConnect concern;
8821
+ * the dapp must check the receipt after receiving the hash.
8822
+ */
8823
+ throwOnRevert?: boolean;
8818
8824
  }) => Promise<TransactionReceipt>;
8819
8825
  };
8820
8826
  /**
@@ -13140,11 +13146,12 @@ export declare function useExternalWalletClient(): {
13140
13146
  [x: `bytes3[${string}]`]: undefined;
13141
13147
  [x: `bytes1[${string}]`]: undefined;
13142
13148
  [x: `bytes2[${string}]`]: undefined;
13149
+ [x: `bytes16[${string}]`]: undefined;
13150
+ [x: `bytes8[${string}]`]: undefined;
13143
13151
  [x: `bytes4[${string}]`]: undefined;
13144
13152
  [x: `bytes5[${string}]`]: undefined;
13145
13153
  [x: `bytes6[${string}]`]: undefined;
13146
13154
  [x: `bytes7[${string}]`]: undefined;
13147
- [x: `bytes8[${string}]`]: undefined;
13148
13155
  [x: `bytes9[${string}]`]: undefined;
13149
13156
  [x: `bytes10[${string}]`]: undefined;
13150
13157
  [x: `bytes11[${string}]`]: undefined;
@@ -13152,7 +13159,6 @@ export declare function useExternalWalletClient(): {
13152
13159
  [x: `bytes13[${string}]`]: undefined;
13153
13160
  [x: `bytes14[${string}]`]: undefined;
13154
13161
  [x: `bytes15[${string}]`]: undefined;
13155
- [x: `bytes16[${string}]`]: undefined;
13156
13162
  [x: `bytes17[${string}]`]: undefined;
13157
13163
  [x: `bytes18[${string}]`]: undefined;
13158
13164
  [x: `bytes19[${string}]`]: undefined;
@@ -13170,8 +13176,8 @@ export declare function useExternalWalletClient(): {
13170
13176
  [x: `bytes31[${string}]`]: undefined;
13171
13177
  [x: `bytes32[${string}]`]: undefined;
13172
13178
  [x: `int[${string}]`]: undefined;
13173
- [x: `int8[${string}]`]: undefined;
13174
13179
  [x: `int16[${string}]`]: undefined;
13180
+ [x: `int8[${string}]`]: undefined;
13175
13181
  [x: `int24[${string}]`]: undefined;
13176
13182
  [x: `int32[${string}]`]: undefined;
13177
13183
  [x: `int40[${string}]`]: undefined;
@@ -13203,8 +13209,8 @@ export declare function useExternalWalletClient(): {
13203
13209
  [x: `int248[${string}]`]: undefined;
13204
13210
  [x: `int256[${string}]`]: undefined;
13205
13211
  [x: `uint[${string}]`]: undefined;
13206
- [x: `uint8[${string}]`]: undefined;
13207
13212
  [x: `uint16[${string}]`]: undefined;
13213
+ [x: `uint8[${string}]`]: undefined;
13208
13214
  [x: `uint24[${string}]`]: undefined;
13209
13215
  [x: `uint32[${string}]`]: undefined;
13210
13216
  [x: `uint40[${string}]`]: undefined;
@@ -13242,11 +13248,12 @@ export declare function useExternalWalletClient(): {
13242
13248
  bytes3?: undefined;
13243
13249
  bytes1?: undefined;
13244
13250
  bytes2?: undefined;
13251
+ bytes16?: undefined;
13252
+ bytes8?: undefined;
13245
13253
  bytes4?: undefined;
13246
13254
  bytes5?: undefined;
13247
13255
  bytes6?: undefined;
13248
13256
  bytes7?: undefined;
13249
- bytes8?: undefined;
13250
13257
  bytes9?: undefined;
13251
13258
  bytes10?: undefined;
13252
13259
  bytes11?: undefined;
@@ -13254,7 +13261,6 @@ export declare function useExternalWalletClient(): {
13254
13261
  bytes13?: undefined;
13255
13262
  bytes14?: undefined;
13256
13263
  bytes15?: undefined;
13257
- bytes16?: undefined;
13258
13264
  bytes17?: undefined;
13259
13265
  bytes18?: undefined;
13260
13266
  bytes19?: undefined;
@@ -13271,8 +13277,8 @@ export declare function useExternalWalletClient(): {
13271
13277
  bytes30?: undefined;
13272
13278
  bytes31?: undefined;
13273
13279
  bytes32?: undefined;
13274
- int8?: undefined;
13275
13280
  int16?: undefined;
13281
+ int8?: undefined;
13276
13282
  int24?: undefined;
13277
13283
  int32?: undefined;
13278
13284
  int40?: undefined;
@@ -13303,8 +13309,8 @@ export declare function useExternalWalletClient(): {
13303
13309
  int240?: undefined;
13304
13310
  int248?: undefined;
13305
13311
  int256?: undefined;
13306
- uint8?: undefined;
13307
13312
  uint16?: undefined;
13313
+ uint8?: undefined;
13308
13314
  uint24?: undefined;
13309
13315
  uint32?: undefined;
13310
13316
  uint40?: undefined;
package/dist/src/hooks.js CHANGED
@@ -9,10 +9,10 @@ import { toAccount } from 'viem/accounts';
9
9
  import { Connection as SolanaConnection, Transaction as SolanaTransaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL, TransactionInstruction, } from '@solana/web3.js';
10
10
  import { SolanaRelayer } from '@abstraxn/solana-relayer';
11
11
  import { useWalletClient as useWagmiWalletClient, useAccount, useConfig, useChainId as useWagmiChainId, useSwitchChain as useWagmiSwitchChain, useSignMessage as useWagmiSignMessage, useReconnect as useWagmiReconnect } from 'wagmi';
12
- import { getWalletClient, switchChain } from '@wagmi/core';
12
+ import { getWalletClient, switchChain, getPublicClient } from '@wagmi/core';
13
13
  import { getConnectorMeta } from './connectors';
14
14
  import { getChainById } from './chains';
15
- import { waitUntilDocumentVisible } from './walletConnectMobile';
15
+ import { waitUntilDocumentVisible, pollForOutgoingTx, clearWalletConnectPendingState, isWalletConnectConnector, isMobileDevice, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
16
16
  /**
17
17
  * Hook to check if wallet is connected
18
18
  */
@@ -2057,7 +2057,7 @@ export function useSignAndSendTxn(provider) {
2057
2057
  * ```
2058
2058
  */
2059
2059
  export function useWaitForTxnReceipt(provider) {
2060
- const waitForTxnReceipt = useCallback(async ({ hash, confirmations, timeout, }) => {
2060
+ const waitForTxnReceipt = useCallback(async ({ hash, confirmations, timeout, throwOnRevert = true, }) => {
2061
2061
  if (!provider) {
2062
2062
  throw new Error('Provider (publicClient) is required');
2063
2063
  }
@@ -2080,9 +2080,18 @@ export function useWaitForTxnReceipt(provider) {
2080
2080
  timeout: Math.min(20_000, remaining),
2081
2081
  pollingInterval: 1_500,
2082
2082
  });
2083
+ if (throwOnRevert &&
2084
+ (receipt.status === 'reverted' ||
2085
+ receipt.status === 0 ||
2086
+ receipt.status === '0x0')) {
2087
+ throw new Error('Transaction reverted');
2088
+ }
2083
2089
  return receipt;
2084
2090
  }
2085
2091
  catch (error) {
2092
+ if (error instanceof Error && error.message === 'Transaction reverted') {
2093
+ throw error;
2094
+ }
2086
2095
  lastError = error;
2087
2096
  await waitUntilDocumentVisible();
2088
2097
  await new Promise((resolve) => setTimeout(resolve, 1_000));
@@ -2096,6 +2105,9 @@ export function useWaitForTxnReceipt(provider) {
2096
2105
  : new Error('Failed to wait for transaction receipt');
2097
2106
  }
2098
2107
  catch (error) {
2108
+ if (error instanceof Error && error.message === 'Transaction reverted') {
2109
+ throw error;
2110
+ }
2099
2111
  if (error instanceof Error && error.message.startsWith('Failed to wait for transaction receipt')) {
2100
2112
  throw error;
2101
2113
  }
@@ -2430,69 +2442,133 @@ export function useWriteContract() {
2430
2442
  }
2431
2443
  // Cast to any to avoid type issues with different viem versions
2432
2444
  const walletClientAny = activeClient;
2433
- // Try using writeContract method directly if available
2434
- if (walletClientAny && typeof walletClientAny.writeContract === 'function') {
2435
- return walletClientAny.writeContract({
2445
+ const fromAddress = account ||
2446
+ walletClientAny?.account?.address;
2447
+ const resolvedGas = gas !== undefined ? gas : gasLimit !== undefined ? gasLimit : undefined;
2448
+ const sendWrite = async () => {
2449
+ // Try using writeContract method directly if available
2450
+ if (walletClientAny && typeof walletClientAny.writeContract === 'function') {
2451
+ return walletClientAny.writeContract({
2452
+ address,
2453
+ abi: abi,
2454
+ functionName: functionName,
2455
+ args: args,
2456
+ value,
2457
+ account,
2458
+ gas: resolvedGas,
2459
+ gasPrice,
2460
+ maxFeePerGas,
2461
+ maxPriorityFeePerGas,
2462
+ nonce,
2463
+ chain: null, // Let the client determine the chain (or use current)
2464
+ });
2465
+ }
2466
+ // If writeContract is not available, use getContract to create a contract instance
2467
+ // This works with wagmi's connectorClient
2468
+ const contract = getContract({
2436
2469
  address,
2470
+ abi: abi,
2471
+ client: walletClientAny,
2472
+ });
2473
+ // Get the write function from the contract
2474
+ const writeFunction = contract.write?.[functionName];
2475
+ if (!writeFunction || typeof writeFunction !== 'function') {
2476
+ throw new Error(`Function "${functionName}" not found in contract ABI or is not a write function. ` +
2477
+ 'Make sure the function exists in the ABI and is a state-changing function.');
2478
+ }
2479
+ // Prepare the call options (viem contract write methods accept options as the last parameter)
2480
+ const hasOptions = value !== undefined || account !== undefined || resolvedGas !== undefined ||
2481
+ gasPrice !== undefined || maxFeePerGas !== undefined ||
2482
+ maxPriorityFeePerGas !== undefined || nonce !== undefined;
2483
+ // Viem contract write methods expect args as an array, not spread
2484
+ const functionArgs = args || [];
2485
+ if (hasOptions) {
2486
+ const callOptions = {};
2487
+ if (value !== undefined)
2488
+ callOptions.value = value;
2489
+ if (account !== undefined)
2490
+ callOptions.account = account;
2491
+ if (resolvedGas !== undefined)
2492
+ callOptions.gas = resolvedGas;
2493
+ if (gasPrice !== undefined)
2494
+ callOptions.gasPrice = gasPrice;
2495
+ if (maxFeePerGas !== undefined)
2496
+ callOptions.maxFeePerGas = maxFeePerGas;
2497
+ if (maxPriorityFeePerGas !== undefined)
2498
+ callOptions.maxPriorityFeePerGas = maxPriorityFeePerGas;
2499
+ if (nonce !== undefined)
2500
+ callOptions.nonce = nonce;
2501
+ return writeFunction(functionArgs, callOptions);
2502
+ }
2503
+ return writeFunction(functionArgs);
2504
+ };
2505
+ // Mobile WalletConnect: MetaMask confirms on-chain but eth_sendTransaction
2506
+ // never resolves if the relay WS died while the browser was backgrounded.
2507
+ // Only race an RPC scan in that scenario — not for desktop / injected wallets.
2508
+ const publicClient = getPublicClient(config, {
2509
+ chainId: currentChainId,
2510
+ });
2511
+ const documentHidden = typeof document !== "undefined" &&
2512
+ document.visibilityState === "hidden";
2513
+ const shouldPollForWcHang = isWalletConnectConnector(connector) &&
2514
+ (isMobileDevice() || documentHidden);
2515
+ if (!shouldPollForWcHang || !publicClient || !fromAddress) {
2516
+ return sendWrite();
2517
+ }
2518
+ let startBlock = 0n;
2519
+ try {
2520
+ startBlock = await publicClient.getBlockNumber();
2521
+ }
2522
+ catch {
2523
+ // ignore — poll will use a recent window
2524
+ }
2525
+ let dataPrefix;
2526
+ try {
2527
+ const data = encodeFunctionData({
2437
2528
  abi: abi,
2438
2529
  functionName: functionName,
2439
2530
  args: args,
2440
- value,
2441
- account,
2442
- gas,
2443
- gasPrice,
2444
- maxFeePerGas,
2445
- maxPriorityFeePerGas,
2446
- nonce,
2447
- chain: null, // Let the client determine the chain (or use current)
2448
2531
  });
2532
+ dataPrefix = data.slice(0, 10);
2449
2533
  }
2450
- // If writeContract is not available, use getContract to create a contract instance
2451
- // This works with wagmi's connectorClient
2452
- const contract = getContract({
2453
- address,
2454
- abi: abi,
2455
- client: walletClientAny,
2534
+ catch {
2535
+ // Matching without selector is still useful (from + to).
2536
+ }
2537
+ const cancelPoll = { cancelled: false };
2538
+ const sendPromise = sendWrite().then((hash) => hash, (err) => {
2539
+ if (isUserRejectionError(err))
2540
+ throw toUserRejectionError(err);
2541
+ throw err;
2542
+ });
2543
+ // Success-only chain recovery when WC hash never arrives.
2544
+ const pollPromise = pollForOutgoingTx({
2545
+ publicClient,
2546
+ from: fromAddress,
2547
+ to: address,
2548
+ dataPrefix,
2549
+ value,
2550
+ startBlock,
2551
+ signal: cancelPoll,
2552
+ initialDelayMs: 1_000,
2553
+ timeoutMs: 60_000,
2456
2554
  });
2457
- // Get the write function from the contract
2458
- const writeFunction = contract.write?.[functionName];
2459
- if (!writeFunction || typeof writeFunction !== 'function') {
2460
- throw new Error(`Function "${functionName}" not found in contract ABI or is not a write function. ` +
2461
- 'Make sure the function exists in the ABI and is a state-changing function.');
2555
+ try {
2556
+ const hash = await Promise.race([sendPromise, pollPromise]);
2557
+ void sendPromise.then(() => undefined, () => undefined);
2558
+ return hash;
2462
2559
  }
2463
- // Prepare the call options (viem contract write methods accept options as the last parameter)
2464
- const hasOptions = value !== undefined || account !== undefined || gas !== undefined || gasLimit !== undefined ||
2465
- gasPrice !== undefined || maxFeePerGas !== undefined ||
2466
- maxPriorityFeePerGas !== undefined || nonce !== undefined;
2467
- // Viem contract write methods expect args as an array, not spread
2468
- const functionArgs = args || [];
2469
- if (hasOptions) {
2470
- const callOptions = {};
2471
- if (value !== undefined)
2472
- callOptions.value = value;
2473
- if (account !== undefined)
2474
- callOptions.account = account;
2475
- // `gas` in viem is the gas *limit*. Allow either `gas` or `gasLimit` from the hook caller.
2476
- if (gas !== undefined) {
2477
- callOptions.gas = gas;
2560
+ finally {
2561
+ cancelPoll.cancelled = true;
2562
+ try {
2563
+ const getProvider = connector?.getProvider;
2564
+ if (typeof getProvider === 'function') {
2565
+ const provider = await getProvider();
2566
+ clearWalletConnectPendingState(provider);
2567
+ }
2478
2568
  }
2479
- else if (gasLimit !== undefined) {
2480
- callOptions.gas = gasLimit;
2569
+ catch {
2570
+ // ignore
2481
2571
  }
2482
- if (gasPrice !== undefined)
2483
- callOptions.gasPrice = gasPrice;
2484
- if (maxFeePerGas !== undefined)
2485
- callOptions.maxFeePerGas = maxFeePerGas;
2486
- if (maxPriorityFeePerGas !== undefined)
2487
- callOptions.maxPriorityFeePerGas = maxPriorityFeePerGas;
2488
- if (nonce !== undefined)
2489
- callOptions.nonce = nonce;
2490
- // Call the write function with args array and options object
2491
- return writeFunction(functionArgs, callOptions);
2492
- }
2493
- else {
2494
- // Call the write function with args array only
2495
- return writeFunction(functionArgs);
2496
2572
  }
2497
2573
  })();
2498
2574
  }, [hookClient, isPending, isError, error, isConnected, config, currentChainId, reconnect, connector]);
@@ -16,3 +16,4 @@ export type { OnboardingUIComponentProps, OnboardingUIComponentRef } from './Onb
16
16
  export { ALL_CHAINS, EVM_CHAINS, SOLANA_CHAINS, getChainById, getChainsByType, getDefaultChains, toCoreChain, type ChainData, } from './chains';
17
17
  export { CONNECTORS, getConnectorMeta, isOAuthConnector, isWalletConnector, isAuthConnector, type ConnectorType, type ConnectorMeta, } from './connectors';
18
18
  export { createWagmiConfig } from './wagmiConfig';
19
+ export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, forceResumeWalletConnectOnReturn, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
package/dist/src/index.js CHANGED
@@ -11,4 +11,5 @@ export { OnboardingUIComponent as OnboardingUI } from './OnboardingUI';
11
11
  export { ALL_CHAINS, EVM_CHAINS, SOLANA_CHAINS, getChainById, getChainsByType, getDefaultChains, toCoreChain, } from './chains';
12
12
  export { CONNECTORS, getConnectorMeta, isOAuthConnector, isWalletConnector, isAuthConnector, } from './connectors';
13
13
  export { createWagmiConfig } from './wagmiConfig';
14
+ export { pollForOutgoingTx, clearWalletConnectPendingState, waitUntilDocumentVisible, isWalletConnectConnector, isMobileDevice, forceResumeWalletConnectOnReturn, isUserRejectionError, toUserRejectionError, } from './walletConnectMobile';
14
15
  //# sourceMappingURL=index.js.map
@@ -34,12 +34,34 @@ export declare function getDappReturnUrl(): string;
34
34
  * navigating Safari to a new URL (which reloads and drops in-memory state).
35
35
  */
36
36
  export declare function getSourceBrowserNativeRedirect(): string | undefined;
37
+ export declare function isMobileDevice(): boolean;
38
+ /**
39
+ * Undo the inline `display: none !important` we apply after connect so the
40
+ * WalletConnect modal can show "Continue in wallet" and process the response.
41
+ * Prefer hideWalletConnectModal() once a session already exists — restoring
42
+ * during a later tx reopens the "All Wallets" picker on top of MetaMask.
43
+ */
44
+ export declare function restoreWalletConnectModal(): void;
45
+ /** Force-close / hide Reown AppKit + WalletConnect modal DOM after connect or tx. */
46
+ export declare function hideWalletConnectModal(provider?: any): void;
37
47
  /**
38
48
  * Open the already-connected wallet app so the user sees the confirmation UI.
39
49
  * WalletConnect often sends the session_request but does not deeplink on a
40
50
  * later tx if the QR modal is hidden — Create Proposal then sits on a loader.
51
+ *
52
+ * When a session already exists, do NOT reopen the WC "All Wallets" modal —
53
+ * that stacks under the iOS "Open in MetaMask?" prompt and stays after confirm.
54
+ *
55
+ * Returns false if skipped (already backgrounded / already opened once).
56
+ */
57
+ export declare function openConnectedWalletApp(provider: any): boolean;
58
+ /**
59
+ * At most one MetaMask deeplink per approval request.
60
+ * Extra retries queue iOS "Open in MetaMask?" prompts that stay on screen
61
+ * after the tx is already confirmed (JS cannot dismiss that system dialog).
62
+ * Cancel remaining work as soon as the tab is backgrounded (user opened MM).
41
63
  */
42
- export declare function openConnectedWalletApp(provider: any): void;
64
+ export declare function scheduleOpenConnectedWalletApp(provider: any, delaysMs?: number[]): () => void;
43
65
  /**
44
66
  * Lazy metadata so url/redirect are read at connect/request time, not at
45
67
  * provider mount (which is often the homepage in Next.js layouts).
@@ -81,17 +103,47 @@ export declare function resumeWalletConnectRelayer(provider: any, options?: {
81
103
  /** Resolves when the dapp tab is in the foreground again (mobile wallet return). */
82
104
  export declare function waitUntilDocumentVisible(): Promise<void>;
83
105
  /**
84
- * Undo the inline `display: none !important` we apply after connect so the
85
- * WalletConnect modal can show "Continue in wallet" and process the response.
106
+ * Clear mobile WC pending flags after we recover the tx hash from chain
107
+ * (poll won while eth_sendTransaction is still hanging).
86
108
  */
87
- export declare function restoreWalletConnectModal(): void;
109
+ export declare function clearWalletConnectPendingState(provider: any): void;
110
+ /** EIP-1193 4001 / WC 5000 (and common message shapes). */
111
+ export declare function isUserRejectionError(error: unknown): boolean;
112
+ export declare function toUserRejectionError(error?: unknown): Error;
113
+ /**
114
+ * Success-only recovery: find an on-chain hash when WC never returns one.
115
+ * Does not infer reject (that is handled by relay resume + reject fallback).
116
+ */
117
+ export declare function pollForOutgoingTx(params: {
118
+ publicClient: {
119
+ getBlockNumber: () => Promise<bigint>;
120
+ getBlock: (args: {
121
+ blockNumber: bigint;
122
+ includeTransactions: boolean;
123
+ }) => Promise<{
124
+ transactions?: unknown[];
125
+ } | null>;
126
+ };
127
+ from: string;
128
+ to: string;
129
+ dataPrefix?: string;
130
+ value?: bigint;
131
+ startBlock: bigint;
132
+ signal: {
133
+ cancelled: boolean;
134
+ };
135
+ timeoutMs?: number;
136
+ initialDelayMs?: number;
137
+ }): Promise<`0x${string}`>;
138
+ /** Time-bounded force-resume (must not hang on transportOpen). */
139
+ export declare function forceResumeWalletConnectOnReturn(provider: any, timeoutMs?: number): Promise<void>;
88
140
  /**
89
141
  * Patch the EIP-1193 provider so signing/tx requests resume the relay first.
90
142
  * Covers wagmi hooks, viem walletClient, and direct provider.request calls.
91
143
  */
92
144
  export declare function patchWalletConnectProviderForMobile(provider: any): void;
93
145
  /**
94
- * When the dapp tab is shown again (user left MetaMask), restart the relay so
95
- * the pending WalletConnect response can be received.
146
+ * When the dapp tab is shown again, restart the relay so pending
147
+ * approve/reject responses can be delivered (WalletConnect mobile linking).
96
148
  */
97
149
  export declare function subscribeWalletConnectForegroundResume(provider: any): () => void;
@@ -128,7 +128,7 @@ function isAndroid() {
128
128
  return false;
129
129
  return /Android/i.test(navigator.userAgent || "");
130
130
  }
131
- function isMobileDevice() {
131
+ export function isMobileDevice() {
132
132
  return isIOS() || isAndroid();
133
133
  }
134
134
  function isWalletHttpDeepLink(url) {
@@ -151,50 +151,185 @@ function canOpenAsWalletApp(url) {
151
151
  return isWalletHttpDeepLink(url);
152
152
  return true;
153
153
  }
154
+ function navigateToWalletDeepLink(target) {
155
+ if (!target || typeof window === "undefined")
156
+ return;
157
+ try {
158
+ // Prefer an invisible <a> click — more reliable than location.href behind
159
+ // iOS "open another application?" prompts and after a prior soft navigation.
160
+ const a = document.createElement("a");
161
+ a.href = target;
162
+ a.rel = "noreferrer";
163
+ a.style.display = "none";
164
+ document.body.appendChild(a);
165
+ a.click();
166
+ a.remove();
167
+ }
168
+ catch {
169
+ try {
170
+ window.location.href = target;
171
+ }
172
+ catch {
173
+ // Ignore
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Undo the inline `display: none !important` we apply after connect so the
179
+ * WalletConnect modal can show "Continue in wallet" and process the response.
180
+ * Prefer hideWalletConnectModal() once a session already exists — restoring
181
+ * during a later tx reopens the "All Wallets" picker on top of MetaMask.
182
+ */
183
+ export function restoreWalletConnectModal() {
184
+ if (typeof document === "undefined")
185
+ return;
186
+ WALLETCONNECT_MODAL_SELECTORS.forEach((sel) => {
187
+ try {
188
+ document.querySelectorAll(sel).forEach((el) => {
189
+ const htmlEl = el;
190
+ if (htmlEl?.style) {
191
+ htmlEl.style.removeProperty("display");
192
+ }
193
+ });
194
+ }
195
+ catch {
196
+ // Ignore invalid selectors in older browsers.
197
+ }
198
+ });
199
+ }
200
+ /** Force-close / hide Reown AppKit + WalletConnect modal DOM after connect or tx. */
201
+ export function hideWalletConnectModal(provider) {
202
+ try {
203
+ if (typeof provider?.modal?.close === "function") {
204
+ provider.modal.close();
205
+ }
206
+ }
207
+ catch {
208
+ // Ignore
209
+ }
210
+ if (typeof document === "undefined")
211
+ return;
212
+ WALLETCONNECT_MODAL_SELECTORS.forEach((sel) => {
213
+ try {
214
+ document.querySelectorAll(sel).forEach((el) => {
215
+ try {
216
+ if (typeof el.close === "function") {
217
+ el.close();
218
+ }
219
+ }
220
+ catch {
221
+ // Ignore
222
+ }
223
+ const htmlEl = el;
224
+ if (htmlEl?.style) {
225
+ htmlEl.style.setProperty("display", "none", "important");
226
+ }
227
+ });
228
+ }
229
+ catch {
230
+ // Ignore invalid selectors
231
+ }
232
+ });
233
+ }
154
234
  /**
155
235
  * Open the already-connected wallet app so the user sees the confirmation UI.
156
236
  * WalletConnect often sends the session_request but does not deeplink on a
157
237
  * later tx if the QR modal is hidden — Create Proposal then sits on a loader.
238
+ *
239
+ * When a session already exists, do NOT reopen the WC "All Wallets" modal —
240
+ * that stacks under the iOS "Open in MetaMask?" prompt and stays after confirm.
241
+ *
242
+ * Returns false if skipped (already backgrounded / already opened once).
158
243
  */
159
244
  export function openConnectedWalletApp(provider) {
160
245
  if (typeof window === "undefined" || !isMobileDevice())
161
- return;
162
- restoreWalletConnectModal();
163
- try {
164
- if (typeof provider?.modal?.open === "function") {
165
- void provider.modal.open();
246
+ return false;
247
+ // Never queue another metamask:// while the tab is already leaving / hidden —
248
+ // those become stuck "Open in MetaMask?" dialogs after the user returns.
249
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
250
+ return false;
251
+ }
252
+ if (provider?.__abstraxnWcDeeplinkOpened) {
253
+ return false;
254
+ }
255
+ const hasSession = hasActiveWalletConnectSession(provider);
256
+ // Only touch the WC modal when there is no session yet (connect flow).
257
+ // For later txs, deeplink straight into the peer wallet app.
258
+ if (!hasSession) {
259
+ restoreWalletConnectModal();
260
+ try {
261
+ if (typeof provider?.modal?.open === "function") {
262
+ void provider.modal.open();
263
+ }
264
+ }
265
+ catch {
266
+ // Modal may not exist after we force-hid it post-connect.
166
267
  }
167
268
  }
168
- catch {
169
- // Modal may not exist after we force-hid it post-connect.
269
+ else {
270
+ // Keep the picker closed while MetaMask opens.
271
+ hideWalletConnectModal(provider);
170
272
  }
171
273
  const peer = provider?.session?.peer?.metadata ??
172
274
  provider?.signer?.session?.peer?.metadata;
173
275
  const native = String(peer?.redirect?.native || "");
174
276
  const universal = String(peer?.redirect?.universal || "");
175
277
  const name = String(peer?.name || "").toLowerCase();
176
- let target = "";
278
+ const candidates = [];
177
279
  if (canOpenAsWalletApp(native))
178
- target = native;
179
- else if (canOpenAsWalletApp(universal))
180
- target = universal;
181
- else if (name.includes("metamask") || !name) {
182
- target = isIOS() ? "metamask://" : "https://metamask.app.link/wc";
280
+ candidates.push(native);
281
+ if (canOpenAsWalletApp(universal))
282
+ candidates.push(universal);
283
+ if (name.includes("metamask") || !name) {
284
+ candidates.push(isIOS() ? "metamask://" : "https://metamask.app.link/wc", "https://metamask.app.link/wc");
183
285
  }
184
286
  else if (name.includes("rainbow")) {
185
- target = "rainbow://";
287
+ candidates.push("rainbow://");
186
288
  }
187
289
  else if (name.includes("trust")) {
188
- target = "trust://";
189
- }
190
- if (!target)
191
- return;
192
- try {
193
- window.location.href = target;
194
- }
195
- catch {
196
- // Ignore
290
+ candidates.push("trust://");
197
291
  }
292
+ const unique = [...new Set(candidates.filter(Boolean))];
293
+ if (!unique.length)
294
+ return false;
295
+ provider.__abstraxnWcDeeplinkOpened = true;
296
+ navigateToWalletDeepLink(unique[0]);
297
+ return true;
298
+ }
299
+ /**
300
+ * At most one MetaMask deeplink per approval request.
301
+ * Extra retries queue iOS "Open in MetaMask?" prompts that stay on screen
302
+ * after the tx is already confirmed (JS cannot dismiss that system dialog).
303
+ * Cancel remaining work as soon as the tab is backgrounded (user opened MM).
304
+ */
305
+ export function scheduleOpenConnectedWalletApp(provider, delaysMs = [150]) {
306
+ if (typeof window === "undefined" || !isMobileDevice())
307
+ return () => { };
308
+ provider.__abstraxnWcDeeplinkOpened = false;
309
+ const timers = [];
310
+ let cancelled = false;
311
+ const cancel = () => {
312
+ cancelled = true;
313
+ timers.splice(0).forEach((id) => window.clearTimeout(id));
314
+ if (typeof document !== "undefined") {
315
+ document.removeEventListener("visibilitychange", onHidden);
316
+ }
317
+ };
318
+ const onHidden = () => {
319
+ if (document.visibilityState === "hidden") {
320
+ // User is in (or opening) MetaMask — stop any further deeplinks.
321
+ cancel();
322
+ }
323
+ };
324
+ document.addEventListener("visibilitychange", onHidden);
325
+ delaysMs.forEach((ms) => {
326
+ timers.push(window.setTimeout(() => {
327
+ if (cancelled)
328
+ return;
329
+ openConnectedWalletApp(provider);
330
+ }, ms));
331
+ });
332
+ return cancel;
198
333
  }
199
334
  function getWalletConnectRedirect() {
200
335
  const native = getSourceBrowserNativeRedirect();
@@ -418,25 +553,190 @@ export function waitUntilDocumentVisible() {
418
553
  });
419
554
  }
420
555
  /**
421
- * Undo the inline `display: none !important` we apply after connect so the
422
- * WalletConnect modal can show "Continue in wallet" and process the response.
556
+ * Clear mobile WC pending flags after we recover the tx hash from chain
557
+ * (poll won while eth_sendTransaction is still hanging).
423
558
  */
424
- export function restoreWalletConnectModal() {
425
- if (typeof document === "undefined")
559
+ export function clearWalletConnectPendingState(provider) {
560
+ if (!provider)
426
561
  return;
427
- WALLETCONNECT_MODAL_SELECTORS.forEach((sel) => {
562
+ try {
563
+ provider[PENDING_FLAG] = 0;
564
+ provider[NEEDS_RESUME_FLAG] = false;
565
+ provider.__abstraxnWcDeeplinkOpened = true;
566
+ hideWalletConnectModal(provider);
567
+ }
568
+ catch {
569
+ // ignore
570
+ }
571
+ }
572
+ /** EIP-1193 4001 / WC 5000 (and common message shapes). */
573
+ export function isUserRejectionError(error) {
574
+ const walk = (err, depth = 0) => {
575
+ if (err == null || depth > 8)
576
+ return false;
577
+ if (typeof err === "string") {
578
+ return /user rejected|rejected by user|user denied|denied transaction|request rejected|cancelled by user/i.test(err);
579
+ }
580
+ if (typeof err !== "object")
581
+ return false;
582
+ const e = err;
583
+ if (e.code === 4001 || e.code === 5000 || e.code === "4001" || e.code === "ACTION_REJECTED") {
584
+ return true;
585
+ }
586
+ if (typeof e.name === "string" && /UserRejected/i.test(e.name))
587
+ return true;
588
+ const msg = `${e.message ?? ""} ${e.shortMessage ?? ""}`;
589
+ if (/user rejected|rejected by user|user denied|denied transaction|request rejected|user cancelled|user canceled/i.test(msg)) {
590
+ return true;
591
+ }
592
+ return walk(e.cause, depth + 1);
593
+ };
594
+ return walk(error);
595
+ }
596
+ export function toUserRejectionError(error) {
597
+ if (error instanceof Error && isUserRejectionError(error)) {
598
+ const err = new Error("User rejected the request");
599
+ err.code = 4001;
600
+ err.cause = error;
601
+ return err;
602
+ }
603
+ const err = new Error("User rejected the request");
604
+ err.code = 4001;
605
+ if (error !== undefined)
606
+ err.cause = error;
607
+ return err;
608
+ }
609
+ /**
610
+ * Success-only recovery: find an on-chain hash when WC never returns one.
611
+ * Does not infer reject (that is handled by relay resume + reject fallback).
612
+ */
613
+ export async function pollForOutgoingTx(params) {
614
+ const { publicClient, from, to, dataPrefix, value, startBlock, signal, timeoutMs = 60_000, initialDelayMs = 1_000, } = params;
615
+ await waitUntilDocumentVisible();
616
+ if (signal.cancelled)
617
+ throw new Error("cancelled");
618
+ if (initialDelayMs > 0) {
619
+ await new Promise((r) => setTimeout(r, initialDelayMs));
620
+ }
621
+ if (signal.cancelled)
622
+ throw new Error("cancelled");
623
+ const fromLc = from.toLowerCase();
624
+ const toLc = to.toLowerCase();
625
+ const prefixLc = dataPrefix ? dataPrefix.toLowerCase() : undefined;
626
+ const started = Date.now();
627
+ while (!signal.cancelled && Date.now() - started < timeoutMs) {
428
628
  try {
429
- document.querySelectorAll(sel).forEach((el) => {
430
- const htmlEl = el;
431
- if (htmlEl?.style) {
432
- htmlEl.style.removeProperty("display");
629
+ const latest = await publicClient.getBlockNumber();
630
+ const fromBlock = startBlock > 0n ? startBlock : latest > 12n ? latest - 12n : 0n;
631
+ for (let b = fromBlock; b <= latest; b++) {
632
+ if (signal.cancelled)
633
+ break;
634
+ const block = await publicClient.getBlock({
635
+ blockNumber: b,
636
+ includeTransactions: true,
637
+ });
638
+ const txs = (block?.transactions || []);
639
+ for (const tx of txs) {
640
+ if (!tx || typeof tx === "string")
641
+ continue;
642
+ if (String(tx.from || "").toLowerCase() !== fromLc)
643
+ continue;
644
+ if (String(tx.to || "").toLowerCase() !== toLc)
645
+ continue;
646
+ if (value !== undefined &&
647
+ BigInt(tx.value ?? 0) !== value) {
648
+ continue;
649
+ }
650
+ if (prefixLc) {
651
+ const input = String(tx.input || tx.data || "").toLowerCase();
652
+ if (!input.startsWith(prefixLc))
653
+ continue;
654
+ }
655
+ const hash = tx.hash;
656
+ if (typeof hash === "string" && hash.startsWith("0x")) {
657
+ return hash;
658
+ }
433
659
  }
434
- });
660
+ }
435
661
  }
436
662
  catch {
437
- // Ignore invalid selectors in older browsers.
663
+ // RPC blip
438
664
  }
665
+ await new Promise((r) => setTimeout(r, 2_000));
666
+ }
667
+ // Stay pending until cancelled — reject UX is owned by the approval race.
668
+ if (signal.cancelled)
669
+ throw new Error("cancelled");
670
+ await new Promise(() => { });
671
+ throw new Error("cancelled");
672
+ }
673
+ /** Time-bounded force-resume (must not hang on transportOpen). */
674
+ export async function forceResumeWalletConnectOnReturn(provider, timeoutMs = 1_500) {
675
+ if (!provider)
676
+ return;
677
+ try {
678
+ provider[NEEDS_RESUME_FLAG] = true;
679
+ await Promise.race([
680
+ resumeWalletConnectRelayer(provider, { force: true }),
681
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
682
+ ]);
683
+ }
684
+ catch {
685
+ // best-effort
686
+ }
687
+ }
688
+ async function readPendingNonce(provider) {
689
+ try {
690
+ const accounts = provider?.accounts ||
691
+ provider?.session?.namespaces?.eip155?.accounts?.map((a) => a.includes(":") ? a.split(":").pop() : a) ||
692
+ [];
693
+ const address = accounts[0];
694
+ if (!address || typeof provider.request !== "function")
695
+ return null;
696
+ // Use unbound original if patched — readPendingNonce is called from
697
+ // inside the patch with the live provider; eth_getTransactionCount is
698
+ // not an approval method so the patch forwards it.
699
+ const hex = await provider.request({
700
+ method: "eth_getTransactionCount",
701
+ params: [address, "pending"],
702
+ });
703
+ return typeof hex === "string" ? parseInt(hex, 16) : Number(hex);
704
+ }
705
+ catch {
706
+ return null;
707
+ }
708
+ }
709
+ /**
710
+ * Prefer WC delivering real approve/reject after return (docs: respondSessionRequest).
711
+ * If still pending with no nonce bump → EIP-1193-style user rejection.
712
+ * If nonce moved → do not fake reject (let success hash arrive via WC or poll).
713
+ */
714
+ async function raceApprovalWithResumeAndRejectFallback(resultPromise, provider, startNonce) {
715
+ const normalizedResult = resultPromise.then((v) => v, (err) => {
716
+ if (isUserRejectionError(err))
717
+ throw toUserRejectionError(err);
718
+ throw err;
439
719
  });
720
+ const rejectFallback = (async () => {
721
+ await waitUntilDocumentVisible();
722
+ await forceResumeWalletConnectOnReturn(provider, 1_500);
723
+ await new Promise((r) => setTimeout(r, 400));
724
+ await forceResumeWalletConnectOnReturn(provider, 1_500);
725
+ // Window for MetaMask's real JSON-RPC reject/hash after resume.
726
+ await new Promise((r) => setTimeout(r, 5_000));
727
+ if (!hasPendingWalletConnectApproval(provider)) {
728
+ return await new Promise(() => { });
729
+ }
730
+ const currentNonce = await readPendingNonce(provider);
731
+ if (startNonce != null &&
732
+ currentNonce != null &&
733
+ currentNonce > startNonce) {
734
+ // Tx submitted — confirmation path owns this.
735
+ return await new Promise(() => { });
736
+ }
737
+ throw toUserRejectionError();
738
+ })();
739
+ return await Promise.race([normalizedResult, rejectFallback]);
440
740
  }
441
741
  function getRequestMethod(args) {
442
742
  const payload = args[0];
@@ -468,62 +768,101 @@ export function patchWalletConnectProviderForMobile(provider) {
468
768
  }
469
769
  if (!isApproval && wasBackgrounded) {
470
770
  try {
471
- await resumeWalletConnectRelayer(provider, { force: true });
771
+ await Promise.race([
772
+ resumeWalletConnectRelayer(provider, { force: true }),
773
+ new Promise((resolve) => setTimeout(resolve, 1_500)),
774
+ ]);
472
775
  }
473
776
  finally {
474
777
  provider[NEEDS_RESUME_FLAG] = false;
475
778
  }
476
779
  return originalRequest(...args);
477
780
  }
781
+ let cancelDeeplinks;
782
+ let startNonce = null;
478
783
  if (isApproval) {
479
784
  provider[PENDING_FLAG] = (provider[PENDING_FLAG] || 0) + 1;
480
- restoreWalletConnectModal();
785
+ startNonce = await readPendingNonce(provider);
786
+ if (hasActiveWalletConnectSession(provider)) {
787
+ hideWalletConnectModal(provider);
788
+ }
789
+ else {
790
+ restoreWalletConnectModal();
791
+ }
481
792
  syncWalletConnectDappRedirect(provider);
793
+ provider[NEEDS_RESUME_FLAG] = true;
482
794
  }
483
795
  try {
484
- // Never force-restart right before sending a new tx — that is what
485
- // blocked the MetaMask popup on Create Proposal after 3.3.7.
486
796
  await Promise.race([
487
797
  resumeWalletConnectRelayer(provider, { force: false }),
488
798
  new Promise((resolve) => setTimeout(resolve, 1000)),
489
799
  ]);
490
800
  const resultPromise = originalRequest(...args);
491
- if (typeof window !== "undefined") {
492
- window.setTimeout(() => openConnectedWalletApp(provider), 400);
801
+ if (isApproval) {
802
+ if (!hasActiveWalletConnectSession(provider)) {
803
+ cancelDeeplinks = scheduleOpenConnectedWalletApp(provider);
804
+ }
805
+ else {
806
+ hideWalletConnectModal(provider);
807
+ }
808
+ if (isMobileDevice()) {
809
+ return await raceApprovalWithResumeAndRejectFallback(resultPromise, provider, startNonce);
810
+ }
493
811
  }
494
812
  return await resultPromise;
495
813
  }
496
814
  finally {
815
+ cancelDeeplinks?.();
497
816
  if (isApproval) {
498
817
  provider[PENDING_FLAG] = Math.max(0, (provider[PENDING_FLAG] || 1) - 1);
818
+ provider.__abstraxnWcDeeplinkOpened = false;
819
+ hideWalletConnectModal(provider);
820
+ if (!hasPendingWalletConnectApproval(provider)) {
821
+ provider[NEEDS_RESUME_FLAG] = false;
822
+ }
499
823
  }
500
824
  }
501
825
  };
502
826
  }
503
827
  /**
504
- * When the dapp tab is shown again (user left MetaMask), restart the relay so
505
- * the pending WalletConnect response can be received.
828
+ * When the dapp tab is shown again, restart the relay so pending
829
+ * approve/reject responses can be delivered (WalletConnect mobile linking).
506
830
  */
507
831
  export function subscribeWalletConnectForegroundResume(provider) {
508
832
  if (typeof window === "undefined" || !provider)
509
833
  return () => { };
510
- let timer = null;
834
+ const timers = [];
511
835
  const resume = () => {
512
- if (timer)
513
- clearTimeout(timer);
836
+ timers.splice(0).forEach((t) => clearTimeout(t));
514
837
  const pending = hasPendingWalletConnectApproval(provider);
515
838
  const needsResume = pending || !!provider[NEEDS_RESUME_FLAG];
516
- // Do not restart after connect-only backgrounding — that broke the next
517
- // session_request so MetaMask never opened for Create Proposal.
518
839
  if (!needsResume) {
519
840
  clearWalletReturnIfSameDocument();
520
841
  return;
521
842
  }
522
- timer = setTimeout(() => {
843
+ const run = () => {
844
+ if (!hasPendingWalletConnectApproval(provider) &&
845
+ !provider[NEEDS_RESUME_FLAG]) {
846
+ return;
847
+ }
523
848
  clearWalletReturnIfSameDocument();
524
- void resumeWalletConnectRelayer(provider, { force: true });
525
- restoreWalletConnectModal();
526
- }, 50);
849
+ void forceResumeWalletConnectOnReturn(provider, 1_500).then(() => {
850
+ if (!hasPendingWalletConnectApproval(provider)) {
851
+ provider[NEEDS_RESUME_FLAG] = false;
852
+ hideWalletConnectModal(provider);
853
+ }
854
+ });
855
+ if (!hasActiveWalletConnectSession(provider)) {
856
+ restoreWalletConnectModal();
857
+ }
858
+ else {
859
+ hideWalletConnectModal(provider);
860
+ }
861
+ };
862
+ run();
863
+ [800, 2_000].forEach((ms) => {
864
+ timers.push(setTimeout(run, ms));
865
+ });
527
866
  };
528
867
  const onVisibility = () => {
529
868
  if (document.visibilityState === "hidden") {
@@ -539,8 +878,7 @@ export function subscribeWalletConnectForegroundResume(provider) {
539
878
  window.addEventListener("pageshow", resume);
540
879
  window.addEventListener("focus", resume);
541
880
  return () => {
542
- if (timer)
543
- clearTimeout(timer);
881
+ timers.splice(0).forEach((t) => clearTimeout(t));
544
882
  document.removeEventListener("visibilitychange", onVisibility);
545
883
  window.removeEventListener("pageshow", resume);
546
884
  window.removeEventListener("focus", resume);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abstraxn/signer-react",
3
- "version": "3.3.8",
3
+ "version": "3.3.10",
4
4
  "description": "React SDK for Abstraxn Wallet - React components, hooks, and providers for seamless Web3 wallet integration",
5
5
  "main": "./dist/src/index.js",
6
6
  "module": "./dist/src/index.js",