@abstraxn/signer-react 3.1.4 → 3.3.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.
@@ -1,6 +1,8 @@
1
- import type { WhoamiResponse, TransactionRequest, EnableMfaResponse, VerifySetupMfaResponse, DisableMfaResponse } from '@abstraxn/signer-core';
1
+ import type { WhoamiResponse, TransactionRequest, EnableMfaResponse, VerifySetupMfaResponse, DisableMfaResponse, SupportedAuthMethod, LinkedAuthMethod } from '@abstraxn/signer-core';
2
2
  import type { Chain, PublicClient, Address, Abi, GetContractReturnType, TransactionReceipt } from 'viem';
3
3
  import { Connection as SolanaConnection, Transaction as SolanaTransaction, TransactionInstruction, type Commitment, type SendOptions } from '@solana/web3.js';
4
+ import { SolanaRelayer } from '@abstraxn/solana-relayer';
5
+ import type { BuildTxParams as SolanaGaslessBuildTxParams, BuildRelayTxResult as SolanaGaslessBuildTxResult, RelaySubmitResponse as SolanaGaslessRelaySubmitResponse, RelayStatusResponse as SolanaGaslessRelayStatusResponse } from '@abstraxn/solana-relayer';
4
6
  import type { TypedData, TypedDataDomain } from 'viem';
5
7
  import { type ConnectorType, type ConnectorMeta } from './connectors';
6
8
  /**
@@ -46,6 +48,20 @@ export { useAbstraxnWallet } from './AbstraxnProvider';
46
48
  * Hook to get wallet instance (for advanced usage)
47
49
  */
48
50
  export declare function useWallet(): import("@abstraxn/signer-core").AbstraxnWallet | null;
51
+ export interface SmartAccountOwnerOptions {
52
+ /** Override the signer address. By default, uses connected wallet address from context. */
53
+ address?: Address;
54
+ }
55
+ export interface SmartAccountOwnerResult {
56
+ smartAccountOwner: any | null;
57
+ isReady: boolean;
58
+ address: Address | null;
59
+ }
60
+ /**
61
+ * Hook to get an AA-compatible owner account for permissionless-compatible SDKs.
62
+ * This does not include bundler/paymaster integration; apps can plug in any provider.
63
+ */
64
+ export declare function useSmartAccountOwner(options?: SmartAccountOwnerOptions): SmartAccountOwnerResult;
49
65
  /**
50
66
  * Hook to export wallet private key
51
67
  * Returns the export bundle containing the private key
@@ -127,6 +143,28 @@ export declare function useDisableMfa(): {
127
143
  confirmDisable: () => Promise<DisableMfaResponse | null>;
128
144
  reset: () => void;
129
145
  };
146
+ export interface UseAuthMethodLinkingReturn {
147
+ supportedMethods: SupportedAuthMethod[];
148
+ linkedMethods: LinkedAuthMethod[];
149
+ loading: boolean;
150
+ actionProvider: string | null;
151
+ error: string | null;
152
+ refreshAuthMethods: (options?: {
153
+ force?: boolean;
154
+ }) => Promise<void>;
155
+ isLinked: (provider: string) => boolean;
156
+ linkProvider: (provider: 'google' | 'discord' | 'twitter' | 'x' | 'passkey') => Promise<void>;
157
+ requestEmailLinkOtp: (email: string) => Promise<{
158
+ otpId: string;
159
+ }>;
160
+ confirmEmailLink: (otpId: string, otpCode: string) => Promise<void>;
161
+ unlinkProvider: (provider: string) => Promise<void>;
162
+ }
163
+ /**
164
+ * Hook for linking/unlinking auth methods without any UI concerns.
165
+ * It exposes state and actions only; consumers own rendering and messaging.
166
+ */
167
+ export declare function useAuthMethodLinking(): UseAuthMethodLinkingReturn;
130
168
  /**
131
169
  * Hook to access external wallet functionality
132
170
  * Returns external wallet connection state and methods
@@ -270,7 +308,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
270
308
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
271
309
  } | undefined;
272
310
  chain: Chain | undefined;
273
- dataSuffix?: import("viem").DataSuffix | undefined;
274
311
  experimental_blockTag?: import("viem").BlockTag | undefined;
275
312
  key: string;
276
313
  name: string;
@@ -453,7 +490,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
453
490
  getChainId: () => Promise<import("viem").GetChainIdReturnType>;
454
491
  getCode: (args: import("viem").GetBytecodeParameters) => Promise<import("viem").GetBytecodeReturnType>;
455
492
  getContractEvents: <const abi extends Abi | readonly unknown[], eventName extends import("viem").ContractEventName<abi> | undefined = undefined, strict extends boolean | undefined = undefined, fromBlock extends import("viem").BlockNumber | import("viem").BlockTag | undefined = undefined, toBlock extends import("viem").BlockNumber | import("viem").BlockTag | undefined = undefined>(args: import("viem").GetContractEventsParameters<abi, eventName, strict, fromBlock, toBlock>) => Promise<import("viem").GetContractEventsReturnType<abi, eventName, strict, fromBlock, toBlock>>;
456
- getDelegation: (args: import("viem").GetDelegationParameters) => Promise<import("viem").GetDelegationReturnType>;
457
493
  getEip712Domain: (args: import("viem").GetEip712DomainParameters) => Promise<import("viem").GetEip712DomainReturnType>;
458
494
  getEnsAddress: (args: import("viem").GetEnsAddressParameters) => Promise<import("viem").GetEnsAddressReturnType>;
459
495
  getEnsAvatar: (args: import("viem").GetEnsAvatarParameters) => Promise<import("viem").GetEnsAvatarReturnType>;
@@ -3879,7 +3915,6 @@ export declare function usePublicClient(chain: Chain, rpcUrl: string): {
3879
3915
  cacheTime?: undefined;
3880
3916
  ccipRead?: undefined;
3881
3917
  chain?: undefined;
3882
- dataSuffix?: undefined;
3883
3918
  experimental_blockTag?: undefined;
3884
3919
  key?: undefined;
3885
3920
  name?: undefined;
@@ -3929,7 +3964,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
3929
3964
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
3930
3965
  } | undefined;
3931
3966
  chain: Chain | undefined;
3932
- dataSuffix?: import("viem").DataSuffix | undefined;
3933
3967
  experimental_blockTag?: import("viem").BlockTag | undefined;
3934
3968
  key: string;
3935
3969
  name: string;
@@ -8341,7 +8375,6 @@ export declare function useWalletClient(chain: any, rpcUrl?: string): {
8341
8375
  cacheTime?: undefined;
8342
8376
  ccipRead?: undefined;
8343
8377
  chain?: undefined;
8344
- dataSuffix?: undefined;
8345
8378
  experimental_blockTag?: undefined;
8346
8379
  key?: undefined;
8347
8380
  name?: undefined;
@@ -8583,6 +8616,47 @@ export declare function useSignAndSendSolanaTxn(connection: SolanaConnection): {
8583
8616
  isConnected: boolean;
8584
8617
  solanaAddress: string | null;
8585
8618
  };
8619
+ export type SolanaGaslessBuildParams = Omit<SolanaGaslessBuildTxParams, 'connection' | 'sender'> & {
8620
+ connection?: SolanaConnection;
8621
+ sender?: string;
8622
+ };
8623
+ export interface SolanaGaslessConfigResolved {
8624
+ enabled: boolean;
8625
+ relayerBaseUrl: string;
8626
+ apiKey: string;
8627
+ networkChainId: number;
8628
+ defaultPollMs?: number;
8629
+ defaultTimeoutMs?: number;
8630
+ relayerUrl: string;
8631
+ }
8632
+ export declare function useSolanaGasless(): {
8633
+ isEnabled: boolean;
8634
+ config: SolanaGaslessConfigResolved | null;
8635
+ relayerClient: SolanaRelayer | null;
8636
+ buildGaslessTx: (params: SolanaGaslessBuildParams) => Promise<SolanaGaslessBuildTxResult>;
8637
+ signGaslessTx: (params: {
8638
+ transaction: SolanaTransaction;
8639
+ sender?: string;
8640
+ }) => Promise<{
8641
+ signedTransactionBase64: string;
8642
+ signedTransactionHex: string;
8643
+ }>;
8644
+ sendGaslessTx: (params: {
8645
+ signedTransactionBase64: string;
8646
+ lastValidBlockHeight: number;
8647
+ }) => Promise<SolanaGaslessRelaySubmitResponse>;
8648
+ getGaslessTxStatus: (txnId: string) => Promise<SolanaGaslessRelayStatusResponse>;
8649
+ };
8650
+ export declare function useSignAndSendSolanaGaslessTxn(): {
8651
+ signAndSendGaslessTx: (params: SolanaGaslessBuildParams) => Promise<{
8652
+ txnId: string;
8653
+ status: "pending" | "confirmed" | "processing" | "failed";
8654
+ preview: Record<string, unknown>;
8655
+ signedTransactionHex: string;
8656
+ signedTransactionBase64: string;
8657
+ lastValidBlockHeight: number;
8658
+ }>;
8659
+ };
8586
8660
  /**
8587
8661
  * Hook to wait for Solana transaction confirmation.
8588
8662
  */
@@ -8868,7 +8942,6 @@ export declare function useExternalWalletClient(): {
8868
8942
  request?: (parameters: import("viem").CcipRequestParameters) => Promise<import("viem/_types/utils/ccip").CcipRequestReturnType>;
8869
8943
  } | undefined;
8870
8944
  chain: Chain;
8871
- dataSuffix?: import("viem").DataSuffix | undefined;
8872
8945
  experimental_blockTag?: import("viem").BlockTag | undefined;
8873
8946
  key: string;
8874
8947
  name: string;
@@ -13280,7 +13353,6 @@ export declare function useExternalWalletClient(): {
13280
13353
  cacheTime?: undefined;
13281
13354
  ccipRead?: undefined;
13282
13355
  chain?: undefined;
13283
- dataSuffix?: undefined;
13284
13356
  experimental_blockTag?: undefined;
13285
13357
  key?: undefined;
13286
13358
  name?: undefined;
package/dist/src/hooks.js CHANGED
@@ -4,8 +4,10 @@
4
4
  import { useState, useCallback, useEffect, useMemo } from 'react';
5
5
  import { useAbstraxnWallet } from './AbstraxnProvider';
6
6
  import { InvalidDeviceIdError, SignMfaRequiredError } from '@abstraxn/signer-core';
7
- import { createPublicClient, createWalletClient, http, getContract, serializeTransaction, encodeFunctionData, hashTypedData } from 'viem';
7
+ import { createPublicClient, createWalletClient, http, getContract, serializeTransaction, encodeFunctionData, hashTypedData, hashMessage, parseSignature } from 'viem';
8
+ import { toAccount } from 'viem/accounts';
8
9
  import { Connection as SolanaConnection, Transaction as SolanaTransaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL, TransactionInstruction, } from '@solana/web3.js';
10
+ import { SolanaRelayer } from '@abstraxn/solana-relayer';
9
11
  import { useWalletClient as useWagmiWalletClient, useAccount, useConfig, useChainId as useWagmiChainId, useSwitchChain as useWagmiSwitchChain, useSignMessage as useWagmiSignMessage, useReconnect as useWagmiReconnect } from 'wagmi';
10
12
  import { getWalletClient, switchChain } from '@wagmi/core';
11
13
  import { getConnectorMeta } from './connectors';
@@ -72,6 +74,67 @@ export function useWallet() {
72
74
  const { wallet } = useAbstraxnWallet();
73
75
  return wallet;
74
76
  }
77
+ /**
78
+ * Hook to get an AA-compatible owner account for permissionless-compatible SDKs.
79
+ * This does not include bundler/paymaster integration; apps can plug in any provider.
80
+ */
81
+ export function useSmartAccountOwner(options = {}) {
82
+ const { isConnected, address: connectedAddress, signTransactionViaAPI, signTypedTxViaAPI, verifySignMfa, } = useAbstraxnWallet();
83
+ const ownerAddress = (options.address ?? connectedAddress ?? null);
84
+ const smartAccountOwner = useMemo(() => {
85
+ if (!isConnected || !ownerAddress)
86
+ return null;
87
+ return toAccount({
88
+ address: ownerAddress,
89
+ /**
90
+ * ERC-4337 SimpleAccount (permissionless) signs UserOperations via viem `signMessage` with
91
+ * `message: { raw: userOpHash }`. That requires the same digest as viem local accounts:
92
+ * EIP-191 `hashMessage(message)`, then ECDSA sign that 32-byte digest — not UTF-8 text hashing.
93
+ */
94
+ async signMessage({ message }) {
95
+ const digest = hashMessage(message);
96
+ const signature = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, digest, 'PAYLOAD_ENCODING_HEXADECIMAL', 'HASH_FUNCTION_NO_OP').then((res) => res.signature), verifySignMfa);
97
+ return (signature.startsWith('0x') ? signature : `0x${signature}`);
98
+ },
99
+ async signTransaction(transaction) {
100
+ const unsignedTransaction = serializeTransaction(transaction);
101
+ const result = await runWithSignStepUpRetry(() => signTransactionViaAPI(unsignedTransaction, ownerAddress), verifySignMfa);
102
+ return result.signedTransaction;
103
+ },
104
+ async signTypedData(typedData) {
105
+ const digest = hashTypedData(typedData);
106
+ const result = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, digest, 'PAYLOAD_ENCODING_EIP712', 'HASH_FUNCTION_NO_OP'), verifySignMfa);
107
+ const signature = result.signature;
108
+ return (signature.startsWith('0x') ? signature : `0x${signature}`);
109
+ },
110
+ /** EIP-7702: sign delegation authorization (`keccak256(0x05 || rlp(...))`) via backend, same as raw ECDSA over digest. */
111
+ async signAuthorization(parameters) {
112
+ const authorizationAddress = parameters.contractAddress ?? parameters.address;
113
+ const authorizationPayload = JSON.stringify({
114
+ chainId: parameters.chainId,
115
+ address: authorizationAddress,
116
+ nonce: parameters.nonce,
117
+ });
118
+ const sig = await runWithSignStepUpRetry(() => signTypedTxViaAPI(ownerAddress, authorizationPayload, 'PAYLOAD_ENCODING_EIP7702_AUTHORIZATION', 'HASH_FUNCTION_NOT_APPLICABLE').then((res) => res.signature), verifySignMfa);
119
+ const hex = (sig.startsWith('0x') ? sig : `0x${sig}`);
120
+ const { r, s, yParity } = parseSignature(hex);
121
+ return {
122
+ address: authorizationAddress,
123
+ chainId: parameters.chainId,
124
+ nonce: parameters.nonce,
125
+ r,
126
+ s,
127
+ yParity,
128
+ };
129
+ },
130
+ });
131
+ }, [isConnected, ownerAddress, signTransactionViaAPI, signTypedTxViaAPI, verifySignMfa]);
132
+ return {
133
+ smartAccountOwner,
134
+ isReady: !!smartAccountOwner,
135
+ address: ownerAddress,
136
+ };
137
+ }
75
138
  /**
76
139
  * Hook to export wallet private key
77
140
  * Returns the export bundle containing the private key
@@ -414,6 +477,282 @@ export function useDisableMfa() {
414
477
  reset,
415
478
  };
416
479
  }
480
+ const AUTH_METHOD_LINK_PROVIDER_ALLOWLIST = new Set([
481
+ 'google',
482
+ 'discord',
483
+ 'x',
484
+ 'passkey',
485
+ ]);
486
+ const AUTH_METHOD_UNLINK_PROVIDER_ALLOWLIST = new Set([
487
+ 'google',
488
+ 'discord',
489
+ 'x',
490
+ 'email',
491
+ 'passkey',
492
+ ]);
493
+ function normalizeAuthMethodProviderForApi(provider) {
494
+ const normalized = provider.trim().toLowerCase();
495
+ if (normalized === 'otp')
496
+ return 'email';
497
+ return normalized;
498
+ }
499
+ function normalizeAuthMethodProviderForMatch(provider) {
500
+ const normalized = normalizeAuthMethodProviderForApi(provider);
501
+ if (normalized === 'twitter' || normalized === 'x')
502
+ return 'x';
503
+ return normalized;
504
+ }
505
+ function normalizeAuthMethodErrorMessage(message) {
506
+ return message.replace(/^network error:\s*/i, '').trim();
507
+ }
508
+ function toAuthMethodError(error, fallbackMessage) {
509
+ if (error instanceof Error) {
510
+ return new Error(normalizeAuthMethodErrorMessage(error.message || fallbackMessage));
511
+ }
512
+ if (typeof error === 'string' && error.trim() !== '') {
513
+ return new Error(normalizeAuthMethodErrorMessage(error));
514
+ }
515
+ return new Error(fallbackMessage);
516
+ }
517
+ /**
518
+ * Hook for linking/unlinking auth methods without any UI concerns.
519
+ * It exposes state and actions only; consumers own rendering and messaging.
520
+ */
521
+ export function useAuthMethodLinking() {
522
+ const { wallet, whoami, getSupportedAuthMethods, getLinkedAuthMethods, requestOtpForEmailLink, loginWithOTP, } = useAbstraxnWallet();
523
+ const [supportedMethods, setSupportedMethods] = useState([]);
524
+ const [linkedMethods, setLinkedMethods] = useState([]);
525
+ const [queryLoading, setQueryLoading] = useState(false);
526
+ const [actionProvider, setActionProvider] = useState(null);
527
+ const [error, setError] = useState(null);
528
+ const requireConnectedWallet = useCallback(() => {
529
+ if (!wallet?.isConnected) {
530
+ throw new Error('Wallet not connected');
531
+ }
532
+ return wallet;
533
+ }, [wallet]);
534
+ const refreshAuthMethods = useCallback(async (_options) => {
535
+ const activeWallet = requireConnectedWallet();
536
+ setQueryLoading(true);
537
+ setError(null);
538
+ try {
539
+ const supportedRequest = getSupportedAuthMethods != null
540
+ ? getSupportedAuthMethods()
541
+ : activeWallet.getSupportedAuthMethods();
542
+ const linkedRequest = getLinkedAuthMethods != null
543
+ ? getLinkedAuthMethods()
544
+ : activeWallet.getLinkedAuthMethods();
545
+ const [supportedResponse, linkedResponse] = await Promise.all([
546
+ supportedRequest,
547
+ linkedRequest,
548
+ ]);
549
+ const loginProvider = normalizeAuthMethodProviderForMatch(String(whoami?.loginProvider ?? ''));
550
+ const allSupportedMethods = supportedResponse.supportedMethods ?? [];
551
+ const filteredMethods = loginProvider !== ''
552
+ ? allSupportedMethods.filter((method) => normalizeAuthMethodProviderForMatch(method.provider) !== loginProvider)
553
+ : allSupportedMethods;
554
+ setSupportedMethods(filteredMethods);
555
+ setLinkedMethods(linkedResponse.linkedMethods ?? []);
556
+ }
557
+ catch (err) {
558
+ const nextError = toAuthMethodError(err, 'Failed to load auth methods');
559
+ setSupportedMethods([]);
560
+ setLinkedMethods([]);
561
+ setError(nextError.message);
562
+ throw nextError;
563
+ }
564
+ finally {
565
+ setQueryLoading(false);
566
+ }
567
+ }, [getLinkedAuthMethods, getSupportedAuthMethods, requireConnectedWallet, whoami]);
568
+ const isLinked = useCallback((provider) => {
569
+ const normalizedProvider = normalizeAuthMethodProviderForMatch(provider);
570
+ return linkedMethods.some((method) => normalizeAuthMethodProviderForMatch(method.authProvider) === normalizedProvider);
571
+ }, [linkedMethods]);
572
+ const runOAuthPopupLink = useCallback(async (activeWallet, provider) => {
573
+ if (typeof window === 'undefined') {
574
+ throw new Error('OAuth linking requires a browser environment');
575
+ }
576
+ const { authUrl } = await activeWallet.initiateLinkAuth(provider);
577
+ const width = 500;
578
+ const height = 600;
579
+ const left = Math.round((window.screen.width - width) / 2);
580
+ const top = Math.round((window.screen.height - height) / 2);
581
+ const popup = window.open(authUrl, 'abstraxn-auth-link', `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`);
582
+ if (!popup) {
583
+ throw new Error('Popup blocked. Please allow popups for this site.');
584
+ }
585
+ const timeoutMs = 120000;
586
+ await new Promise((resolve, reject) => {
587
+ let settled = false;
588
+ let closeCheckId = null;
589
+ let timeoutId = null;
590
+ const finalizeReject = (reason) => {
591
+ if (settled)
592
+ return;
593
+ settled = true;
594
+ cleanup();
595
+ reject(new Error(reason));
596
+ };
597
+ const cleanup = () => {
598
+ window.removeEventListener('message', onMessage);
599
+ if (closeCheckId != null)
600
+ clearInterval(closeCheckId);
601
+ if (timeoutId != null)
602
+ clearTimeout(timeoutId);
603
+ };
604
+ const onMessage = (event) => {
605
+ if (event.origin !== window.location.origin)
606
+ return;
607
+ if (event.source !== popup)
608
+ return;
609
+ const data = event.data;
610
+ if (!data?.type)
611
+ return;
612
+ const eventProvider = normalizeAuthMethodProviderForMatch(String(data.provider ?? ''));
613
+ const targetProvider = normalizeAuthMethodProviderForMatch(provider);
614
+ if (eventProvider !== targetProvider)
615
+ return;
616
+ if (data.type === 'abstraxn-auth-link-error') {
617
+ finalizeReject(normalizeAuthMethodErrorMessage(String(data.error ?? 'Failed to link auth method')));
618
+ return;
619
+ }
620
+ if (data.type === 'abstraxn-auth-link-done') {
621
+ if (!data.activityBody || data.activityBody.trim() === '') {
622
+ finalizeReject('Invalid linking response');
623
+ return;
624
+ }
625
+ settled = true;
626
+ cleanup();
627
+ activeWallet
628
+ .submitLinkAuthStamped(provider, data.activityBody)
629
+ .then(resolve)
630
+ .catch((submitError) => {
631
+ reject(toAuthMethodError(submitError, 'Failed to finalize auth method link'));
632
+ });
633
+ }
634
+ };
635
+ window.addEventListener('message', onMessage);
636
+ closeCheckId = setInterval(() => {
637
+ if (popup.closed) {
638
+ finalizeReject('Linking was cancelled');
639
+ }
640
+ }, 200);
641
+ timeoutId = setTimeout(() => {
642
+ finalizeReject('Linking timed out. Please try again.');
643
+ }, timeoutMs);
644
+ });
645
+ }, [wallet]);
646
+ const linkProvider = useCallback(async (provider) => {
647
+ const activeWallet = requireConnectedWallet();
648
+ const normalizedProvider = provider.toLowerCase();
649
+ if (!AUTH_METHOD_LINK_PROVIDER_ALLOWLIST.has(normalizedProvider)) {
650
+ throw new Error(`Unsupported provider for linking: ${provider}`);
651
+ }
652
+ setActionProvider(normalizedProvider);
653
+ setError(null);
654
+ try {
655
+ if (normalizedProvider === 'passkey') {
656
+ await activeWallet.linkPasskey();
657
+ }
658
+ else {
659
+ await runOAuthPopupLink(activeWallet, normalizedProvider);
660
+ }
661
+ await refreshAuthMethods({ force: true });
662
+ }
663
+ catch (err) {
664
+ const nextError = toAuthMethodError(err, 'Failed to link auth method');
665
+ setError(nextError.message);
666
+ throw nextError;
667
+ }
668
+ finally {
669
+ setActionProvider(null);
670
+ }
671
+ }, [refreshAuthMethods, requireConnectedWallet, runOAuthPopupLink]);
672
+ const requestEmailLinkOtp = useCallback(async (email) => {
673
+ const activeWallet = requireConnectedWallet();
674
+ const trimmedEmail = email.trim();
675
+ if (!trimmedEmail) {
676
+ throw new Error('Email is required');
677
+ }
678
+ setActionProvider('email');
679
+ setError(null);
680
+ try {
681
+ if (requestOtpForEmailLink) {
682
+ return await requestOtpForEmailLink(trimmedEmail);
683
+ }
684
+ if (loginWithOTP) {
685
+ return await loginWithOTP(trimmedEmail);
686
+ }
687
+ // Fallback to core method directly if context methods are unavailable.
688
+ return await activeWallet.requestOtpForEmailLink(trimmedEmail);
689
+ }
690
+ catch (err) {
691
+ const nextError = toAuthMethodError(err, 'Failed to send email verification code');
692
+ setError(nextError.message);
693
+ throw nextError;
694
+ }
695
+ finally {
696
+ setActionProvider(null);
697
+ }
698
+ }, [loginWithOTP, requestOtpForEmailLink, requireConnectedWallet]);
699
+ const confirmEmailLink = useCallback(async (otpId, otpCode) => {
700
+ const activeWallet = requireConnectedWallet();
701
+ const trimmedOtpId = otpId.trim();
702
+ const trimmedOtpCode = otpCode.trim();
703
+ if (!trimmedOtpId || !trimmedOtpCode) {
704
+ throw new Error('otpId and otpCode are required');
705
+ }
706
+ setActionProvider('email');
707
+ setError(null);
708
+ try {
709
+ await activeWallet.linkEmail(trimmedOtpId, trimmedOtpCode);
710
+ await refreshAuthMethods({ force: true });
711
+ }
712
+ catch (err) {
713
+ const nextError = toAuthMethodError(err, 'Failed to link email');
714
+ setError(nextError.message);
715
+ throw nextError;
716
+ }
717
+ finally {
718
+ setActionProvider(null);
719
+ }
720
+ }, [refreshAuthMethods, requireConnectedWallet]);
721
+ const unlinkProvider = useCallback(async (provider) => {
722
+ const activeWallet = requireConnectedWallet();
723
+ const normalizedProvider = normalizeAuthMethodProviderForApi(provider);
724
+ if (!AUTH_METHOD_UNLINK_PROVIDER_ALLOWLIST.has(normalizedProvider)) {
725
+ throw new Error(`Unsupported provider for unlinking: ${provider}`);
726
+ }
727
+ setActionProvider(normalizedProvider);
728
+ setError(null);
729
+ try {
730
+ await activeWallet.unlinkAuthMethod(normalizedProvider);
731
+ await refreshAuthMethods({ force: true });
732
+ }
733
+ catch (err) {
734
+ const nextError = toAuthMethodError(err, 'Failed to unlink auth method');
735
+ setError(nextError.message);
736
+ throw nextError;
737
+ }
738
+ finally {
739
+ setActionProvider(null);
740
+ }
741
+ }, [refreshAuthMethods, requireConnectedWallet]);
742
+ return {
743
+ supportedMethods,
744
+ linkedMethods,
745
+ loading: queryLoading || actionProvider !== null,
746
+ actionProvider,
747
+ error,
748
+ refreshAuthMethods,
749
+ isLinked,
750
+ linkProvider,
751
+ requestEmailLinkOtp,
752
+ confirmEmailLink,
753
+ unlinkProvider,
754
+ };
755
+ }
417
756
  /**
418
757
  * Hook to access external wallet functionality
419
758
  * Returns external wallet connection state and methods
@@ -1179,6 +1518,145 @@ export function useSignAndSendSolanaTxn(connection) {
1179
1518
  }, [connection, isConnected, whoami, signSolanaTransactionViaAPI, verifySignMfa]);
1180
1519
  return { signAndSendSolanaTxn, isConnected, solanaAddress: whoami?.solanaAddress ?? null };
1181
1520
  }
1521
+ function buildSolanaRelayerUrl(relayerBaseUrl, networkChainId, apiKey) {
1522
+ const parsed = new URL(relayerBaseUrl);
1523
+ const hasTxPath = /\/tx\/\d+\/?$/.test(parsed.pathname);
1524
+ const hasChainOnlyPath = /\/\d+\/?$/.test(parsed.pathname);
1525
+ if (!hasTxPath && !hasChainOnlyPath) {
1526
+ const basePath = parsed.pathname.replace(/\/$/, "");
1527
+ const withApiV1 = /\/api\/v1$/.test(basePath)
1528
+ ? basePath
1529
+ : `${basePath}/api/v1`;
1530
+ parsed.pathname = `${withApiV1}/tx/${networkChainId}/`;
1531
+ }
1532
+ parsed.searchParams.set("apikey", apiKey);
1533
+ return parsed.toString();
1534
+ }
1535
+ function hexToBase64SignedTransaction(signedHex) {
1536
+ const hex = signedHex.startsWith('0x') ? signedHex.slice(2) : signedHex;
1537
+ const bytes = new Uint8Array(hex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? []);
1538
+ if (typeof btoa === 'function') {
1539
+ let binary = '';
1540
+ for (let i = 0; i < bytes.length; i++)
1541
+ binary += String.fromCharCode(bytes[i]);
1542
+ return btoa(binary);
1543
+ }
1544
+ return Buffer.from(bytes).toString('base64');
1545
+ }
1546
+ export function useSolanaGasless() {
1547
+ const { isConnected, whoami, solanaConnection, signSolanaTransactionViaAPI, verifySignMfa, solanaGaslessConfig, } = useAbstraxnWallet();
1548
+ const providerConnection = solanaConnection?.connection ?? null;
1549
+ const resolvedConfig = useMemo(() => {
1550
+ if (!solanaGaslessConfig?.enabled)
1551
+ return null;
1552
+ const relayerBaseUrl = solanaGaslessConfig.relayerBaseUrl?.trim();
1553
+ const apiKey = solanaGaslessConfig.apiKey?.trim();
1554
+ const networkChainId = solanaGaslessConfig.networkChainId;
1555
+ if (!relayerBaseUrl || !apiKey || !networkChainId)
1556
+ return null;
1557
+ const base = relayerBaseUrl.replace(/\/$/, '');
1558
+ const relayerUrl = buildSolanaRelayerUrl(base, networkChainId, apiKey);
1559
+ return {
1560
+ enabled: true,
1561
+ relayerBaseUrl: base,
1562
+ apiKey,
1563
+ networkChainId,
1564
+ defaultPollMs: solanaGaslessConfig.defaultPollMs,
1565
+ defaultTimeoutMs: solanaGaslessConfig.defaultTimeoutMs,
1566
+ relayerUrl,
1567
+ };
1568
+ }, [solanaGaslessConfig]);
1569
+ const relayerClient = useMemo(() => {
1570
+ if (!resolvedConfig)
1571
+ return null;
1572
+ return new SolanaRelayer({
1573
+ relayerUrl: resolvedConfig.relayerUrl,
1574
+ defaultPollMs: resolvedConfig.defaultPollMs,
1575
+ defaultTimeoutMs: resolvedConfig.defaultTimeoutMs,
1576
+ });
1577
+ }, [resolvedConfig]);
1578
+ const ensureReady = useCallback(() => {
1579
+ if (!isConnected)
1580
+ throw new Error('Wallet is not connected');
1581
+ if (!resolvedConfig) {
1582
+ throw new Error('Solana gasless is not configured. Set config.solanaGasless with enabled, relayerBaseUrl, apiKey and networkChainId.');
1583
+ }
1584
+ if (!relayerClient)
1585
+ throw new Error('Failed to initialize Solana relayer client');
1586
+ if (!signSolanaTransactionViaAPI) {
1587
+ throw new Error('Solana transaction signing is not supported by this wallet');
1588
+ }
1589
+ const sender = whoami?.solanaAddress ?? null;
1590
+ if (!sender)
1591
+ throw new Error('Solana address is required for gasless flow');
1592
+ return { sender };
1593
+ }, [isConnected, relayerClient, resolvedConfig, signSolanaTransactionViaAPI, whoami]);
1594
+ const buildGaslessTx = useCallback(async (params) => {
1595
+ const { sender } = ensureReady();
1596
+ const connection = params.connection ?? providerConnection;
1597
+ if (!connection)
1598
+ throw new Error('Solana connection is required');
1599
+ const { connection: _connection, sender: _sender, ...rest } = params;
1600
+ return await relayerClient.buildTx({
1601
+ connection,
1602
+ sender: _sender ?? sender,
1603
+ ...rest,
1604
+ });
1605
+ }, [ensureReady, providerConnection, relayerClient]);
1606
+ const signGaslessTx = useCallback(async (params) => {
1607
+ const { sender } = ensureReady();
1608
+ const unsignedBytes = params.transaction.serialize({ requireAllSignatures: false });
1609
+ const unsignedHex = Array.from(unsignedBytes)
1610
+ .map((b) => b.toString(16).padStart(2, '0'))
1611
+ .join('');
1612
+ const signWith = params.sender ?? sender;
1613
+ const { signedTransaction } = await runWithSignStepUpRetry(() => signSolanaTransactionViaAPI(unsignedHex, signWith), verifySignMfa);
1614
+ return {
1615
+ signedTransactionHex: signedTransaction,
1616
+ signedTransactionBase64: hexToBase64SignedTransaction(signedTransaction),
1617
+ };
1618
+ }, [ensureReady, signSolanaTransactionViaAPI, verifySignMfa]);
1619
+ const sendGaslessTx = useCallback(async (params) => {
1620
+ ensureReady();
1621
+ return await relayerClient.sendTx({
1622
+ signedTransaction: params.signedTransactionBase64,
1623
+ lastValidBlockHeight: params.lastValidBlockHeight,
1624
+ });
1625
+ }, [ensureReady, relayerClient]);
1626
+ const getGaslessTxStatus = useCallback(async (txnId) => {
1627
+ ensureReady();
1628
+ return await relayerClient.getTxStatus({ txnId });
1629
+ }, [ensureReady, relayerClient]);
1630
+ return {
1631
+ isEnabled: !!resolvedConfig,
1632
+ config: resolvedConfig,
1633
+ relayerClient,
1634
+ buildGaslessTx,
1635
+ signGaslessTx,
1636
+ sendGaslessTx,
1637
+ getGaslessTxStatus,
1638
+ };
1639
+ }
1640
+ export function useSignAndSendSolanaGaslessTxn() {
1641
+ const { buildGaslessTx, signGaslessTx, sendGaslessTx } = useSolanaGasless();
1642
+ const signAndSendGaslessTx = useCallback(async (params) => {
1643
+ const built = await buildGaslessTx(params);
1644
+ const signed = await signGaslessTx({ transaction: built.tx, sender: params.sender });
1645
+ const relay = await sendGaslessTx({
1646
+ signedTransactionBase64: signed.signedTransactionBase64,
1647
+ lastValidBlockHeight: built.lastValidBlockHeight,
1648
+ });
1649
+ return {
1650
+ txnId: relay.txnId,
1651
+ status: relay.status,
1652
+ preview: built.preview,
1653
+ signedTransactionHex: signed.signedTransactionHex,
1654
+ signedTransactionBase64: signed.signedTransactionBase64,
1655
+ lastValidBlockHeight: built.lastValidBlockHeight,
1656
+ };
1657
+ }, [buildGaslessTx, sendGaslessTx, signGaslessTx]);
1658
+ return { signAndSendGaslessTx };
1659
+ }
1182
1660
  /**
1183
1661
  * Hook to wait for Solana transaction confirmation.
1184
1662
  */