@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.
- package/CHANGELOG.md +32 -0
- package/README.md +79 -0
- package/dist/src/WalletModal.css +46 -0
- package/dist/src/WalletModal.js +23 -4
- package/dist/src/components/AbstraxnProvider/AbstraxnProvider.js +4 -1
- package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.js +91 -15
- package/dist/src/components/WalletModal/hooks/useSendTransaction.js +1 -1
- package/dist/src/components/WalletModal/utils/formatUtils.d.ts +1 -1
- package/dist/src/components/WalletModal/utils/formatUtils.js +3 -4
- package/dist/src/hooks.d.ts +80 -8
- package/dist/src/hooks.js +479 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/types.d.ts +38 -1
- package/package.json +2 -1
package/dist/src/hooks.d.ts
CHANGED
|
@@ -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
|
*/
|