@openzeppelin/ui-builder-adapter-evm 1.2.0 → 1.4.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.
Files changed (105) hide show
  1. package/README.md +33 -18
  2. package/dist/index.cjs +4651 -4448
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +12 -226
  5. package/dist/index.d.ts +12 -226
  6. package/dist/index.js +4737 -4535
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -6
  9. package/src/__tests__/adapter-parsing.test.ts +4 -3
  10. package/src/__tests__/getDefaultServiceConfig.test.ts +185 -0
  11. package/src/__tests__/mocks/mock-network-configs.ts +1 -1
  12. package/src/__tests__/provenanceLinks.test.ts +6 -4
  13. package/src/__tests__/providerSelection.test.ts +5 -4
  14. package/src/__tests__/timeouts.test.ts +5 -3
  15. package/src/__tests__/wallet-connect.test.ts +2 -2
  16. package/src/adapter.ts +61 -107
  17. package/src/configuration/execution.ts +1 -52
  18. package/src/configuration/index.ts +2 -3
  19. package/src/configuration/network-services.ts +47 -60
  20. package/src/index.ts +22 -13
  21. package/src/networks/index.ts +2 -1
  22. package/src/networks/mainnet.ts +1 -1
  23. package/src/networks/testnet.ts +1 -1
  24. package/src/query/adapter-query.ts +72 -0
  25. package/src/query/index.ts +2 -2
  26. package/src/transaction/components/useEvmRelayerOptions.ts +5 -3
  27. package/src/transaction/index.ts +1 -5
  28. package/src/types/artifacts.ts +5 -30
  29. package/src/types/providers.ts +7 -18
  30. package/src/wallet/components/EvmWalletUiRoot.tsx +1 -1
  31. package/src/wallet/evmUiKitManager.ts +26 -129
  32. package/src/wallet/hooks/index.ts +0 -1
  33. package/src/wallet/implementation/wagmi-implementation.ts +45 -577
  34. package/src/wallet/index.ts +2 -3
  35. package/src/wallet/rainbowkit/__tests__/export-service.test.ts +1 -2
  36. package/src/wallet/rainbowkit/componentFactory.ts +10 -8
  37. package/src/wallet/rainbowkit/components.tsx +16 -133
  38. package/src/wallet/rainbowkit/index.ts +27 -5
  39. package/src/wallet/utils/__tests__/uiKitService.test.ts +5 -1
  40. package/src/wallet/utils/connection.ts +8 -52
  41. package/src/wallet/utils/index.ts +0 -2
  42. package/src/wallet/utils/uiKitService.ts +7 -3
  43. package/src/wallet/utils/walletImplementationManager.ts +5 -4
  44. package/src/wallet/utils.ts +1 -65
  45. package/src/abi/__tests__/etherscan-v2.test.ts +0 -117
  46. package/src/abi/__tests__/transformer.test.ts +0 -342
  47. package/src/abi/comparison.ts +0 -389
  48. package/src/abi/etherscan-v2.ts +0 -243
  49. package/src/abi/etherscan.ts +0 -158
  50. package/src/abi/index.ts +0 -7
  51. package/src/abi/loader.ts +0 -415
  52. package/src/abi/sourcify.ts +0 -75
  53. package/src/abi/transformer.ts +0 -163
  54. package/src/abi/types.ts +0 -101
  55. package/src/configuration/__tests__/explorer.test.ts +0 -174
  56. package/src/configuration/__tests__/rpc.test.ts +0 -176
  57. package/src/configuration/explorer.ts +0 -243
  58. package/src/configuration/rpc.ts +0 -257
  59. package/src/mapping/__tests__/field-generator.test.ts +0 -137
  60. package/src/mapping/__tests__/type-mapper.test.ts +0 -139
  61. package/src/mapping/constants.ts +0 -57
  62. package/src/mapping/field-generator.ts +0 -115
  63. package/src/mapping/index.ts +0 -4
  64. package/src/mapping/type-mapper.ts +0 -80
  65. package/src/proxy/detection.ts +0 -465
  66. package/src/query/handler.ts +0 -227
  67. package/src/query/view-checker.ts +0 -10
  68. package/src/transaction/eoa.ts +0 -98
  69. package/src/transaction/execution-strategy.ts +0 -33
  70. package/src/transaction/formatter.ts +0 -101
  71. package/src/transaction/relayer.ts +0 -380
  72. package/src/transaction/sender.ts +0 -185
  73. package/src/transform/index.ts +0 -3
  74. package/src/transform/input-parser.ts +0 -177
  75. package/src/transform/output-formatter.ts +0 -64
  76. package/src/types/__tests__/artifacts.test.ts +0 -105
  77. package/src/types.ts +0 -92
  78. package/src/utils/__tests__/artifacts.test.ts +0 -81
  79. package/src/utils/artifacts.ts +0 -30
  80. package/src/utils/formatting.ts +0 -25
  81. package/src/utils/gas.ts +0 -17
  82. package/src/utils/index.ts +0 -6
  83. package/src/utils/json.ts +0 -19
  84. package/src/utils/validation.ts +0 -10
  85. package/src/validation/eoa.ts +0 -33
  86. package/src/validation/index.ts +0 -2
  87. package/src/validation/relayer.ts +0 -13
  88. package/src/wallet/__tests__/utils.test.ts +0 -149
  89. package/src/wallet/components/account/AccountDisplay.tsx +0 -52
  90. package/src/wallet/components/connect/ConnectButton.tsx +0 -125
  91. package/src/wallet/components/connect/ConnectorDialog.tsx +0 -140
  92. package/src/wallet/components/index.ts +0 -4
  93. package/src/wallet/components/network/NetworkSwitcher.tsx +0 -90
  94. package/src/wallet/context/index.ts +0 -1
  95. package/src/wallet/context/wagmi-context.tsx +0 -7
  96. package/src/wallet/hooks/useIsWagmiProviderInitialized.ts +0 -11
  97. package/src/wallet/rainbowkit/config-generator.ts +0 -56
  98. package/src/wallet/rainbowkit/config-service.ts +0 -169
  99. package/src/wallet/rainbowkit/export-service.ts +0 -18
  100. package/src/wallet/rainbowkit/rainbowkitAssetManager.ts +0 -74
  101. package/src/wallet/rainbowkit/types.ts +0 -74
  102. package/src/wallet/rainbowkit/utils.ts +0 -96
  103. package/src/wallet/services/configResolutionService.ts +0 -65
  104. package/src/wallet/utils/SafeWagmiComponent.tsx +0 -72
  105. package/src/wallet/utils/filterWalletComponents.ts +0 -89
@@ -1,81 +0,0 @@
1
- /**
2
- * Unit tests for EVM contract artifacts utility functions
3
- */
4
- import { describe, expect, it } from 'vitest';
5
-
6
- import { validateAndConvertEvmArtifacts } from '../artifacts';
7
-
8
- describe('validateAndConvertEvmArtifacts', () => {
9
- it('should convert string address to artifacts object', () => {
10
- const address = '0x1234567890123456789012345678901234567890';
11
-
12
- const result = validateAndConvertEvmArtifacts(address);
13
-
14
- expect(result).toEqual({
15
- contractAddress: address,
16
- });
17
- });
18
-
19
- it('should return valid artifacts object as-is', () => {
20
- const artifacts = {
21
- contractAddress: '0x1234567890123456789012345678901234567890',
22
- contractDefinition: '[{"type":"function","name":"test"}]',
23
- };
24
-
25
- const result = validateAndConvertEvmArtifacts(artifacts);
26
-
27
- expect(result).toBe(artifacts);
28
- });
29
-
30
- it('should return artifacts with proxy detection options', () => {
31
- const artifacts = {
32
- contractAddress: '0x1234567890123456789012345678901234567890',
33
- __proxyDetectionOptions: {
34
- skipProxyDetection: true,
35
- },
36
- };
37
-
38
- const result = validateAndConvertEvmArtifacts(artifacts);
39
-
40
- expect(result).toBe(artifacts);
41
- });
42
-
43
- it('should throw error for invalid artifacts object', () => {
44
- const invalidArtifacts = {
45
- someOtherProperty: 'value',
46
- };
47
-
48
- expect(() => validateAndConvertEvmArtifacts(invalidArtifacts)).toThrow(
49
- 'Invalid contract artifacts provided. Expected an object with contractAddress property.'
50
- );
51
- });
52
-
53
- it('should throw error for artifacts with non-string contractAddress', () => {
54
- const invalidArtifacts = {
55
- contractAddress: 123,
56
- };
57
-
58
- expect(() => validateAndConvertEvmArtifacts(invalidArtifacts)).toThrow(
59
- 'Invalid contract artifacts provided. Expected an object with contractAddress property.'
60
- );
61
- });
62
-
63
- it('should accept artifacts with extra properties', () => {
64
- const artifacts = {
65
- contractAddress: '0x1234567890123456789012345678901234567890',
66
- extraProperty: 'should be ignored',
67
- };
68
-
69
- const result = validateAndConvertEvmArtifacts(artifacts);
70
-
71
- expect(result).toEqual(artifacts);
72
- });
73
-
74
- it('should handle empty string address', () => {
75
- const result = validateAndConvertEvmArtifacts('');
76
-
77
- expect(result).toEqual({
78
- contractAddress: '',
79
- });
80
- });
81
- });
@@ -1,30 +0,0 @@
1
- /**
2
- * Utility functions for EVM contract artifacts validation and conversion
3
- */
4
- import type { EvmContractArtifacts } from '../types/artifacts';
5
- import { isEvmContractArtifacts } from '../types/artifacts';
6
-
7
- /**
8
- * Validates and converts generic source input to EvmContractArtifacts
9
- *
10
- * @param source - Generic contract source (string address or artifacts object)
11
- * @returns Validated EvmContractArtifacts
12
- * @throws Error if the source is invalid
13
- */
14
- export function validateAndConvertEvmArtifacts(
15
- source: string | Record<string, unknown>
16
- ): EvmContractArtifacts {
17
- if (typeof source === 'string') {
18
- // If source is a string, assume it's a contract address
19
- return { contractAddress: source };
20
- }
21
-
22
- // Validate that the object has the required structure
23
- if (!isEvmContractArtifacts(source)) {
24
- throw new Error(
25
- 'Invalid contract artifacts provided. Expected an object with contractAddress property.'
26
- );
27
- }
28
-
29
- return source;
30
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Format a method name for display (e.g., from camelCase to Title Case).
3
- */
4
- export function formatMethodName(name: string): string {
5
- if (!name) return '';
6
- return name
7
- .replace(/([A-Z])/g, ' $1')
8
- .replace(/^./, (str) => str.toUpperCase())
9
- .trim();
10
- }
11
-
12
- /**
13
- * Format an input name for display (e.g., from camelCase or snake_case to Title Case).
14
- * Provides a default name based on type if the original name is empty.
15
- */
16
- export function formatInputName(name: string, type: string): string {
17
- if (!name || name === '') {
18
- return `Parameter (${type})`; // Use type if name is missing
19
- }
20
- return name
21
- .replace(/([A-Z])/g, ' $1') // Add space before capitals
22
- .replace(/_/g, ' ') // Replace underscores with spaces
23
- .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter
24
- .trim();
25
- }
package/src/utils/gas.ts DELETED
@@ -1,17 +0,0 @@
1
- import { formatGwei, parseGwei } from 'viem';
2
-
3
- /**
4
- * Convert wei values to gwei for display using viem
5
- */
6
- export const weiToGwei = (wei?: number): number | undefined => {
7
- if (!wei) return undefined;
8
- return parseFloat(formatGwei(BigInt(wei)));
9
- };
10
-
11
- /**
12
- * Convert gwei values to wei using viem
13
- */
14
- export const gweiToWei = (gwei?: number): number | undefined => {
15
- if (!gwei) return undefined;
16
- return Number(parseGwei(gwei.toString()));
17
- };
@@ -1,6 +0,0 @@
1
- // Barrel file for utils module
2
- export * from './artifacts';
3
- export * from './json';
4
- export * from './formatting';
5
- export * from './validation';
6
- export * from './gas';
package/src/utils/json.ts DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * Custom JSON stringifier that handles BigInt values by converting them to strings.
3
- * @param value The value to stringify.
4
- * @param space Adds indentation, white space, and line break characters to the return-value JSON text for readability.
5
- * @returns A JSON string representing the given value.
6
- */
7
- export function stringifyWithBigInt(value: unknown, space?: number | string): string {
8
- const replacer = (_key: string, val: unknown) => {
9
- // Check if the value is a BigInt
10
- if (typeof val === 'bigint') {
11
- // Convert BigInt to string
12
- return val.toString();
13
- }
14
- // Return the value unchanged for other types
15
- return val;
16
- };
17
-
18
- return JSON.stringify(value, replacer, space);
19
- }
@@ -1,10 +0,0 @@
1
- import { isAddress } from 'viem';
2
-
3
- /**
4
- * Validates if a string is a valid EVM address.
5
- * @param address The address string to validate.
6
- * @returns True if the address is valid, false otherwise.
7
- */
8
- export function isValidEvmAddress(address: string): boolean {
9
- return isAddress(address);
10
- }
@@ -1,33 +0,0 @@
1
- import { EoaExecutionConfig } from '@openzeppelin/ui-types';
2
- import { logger } from '@openzeppelin/ui-utils';
3
-
4
- import { isValidEvmAddress } from '../utils';
5
- import { EvmWalletConnectionStatus } from '../wallet/types';
6
-
7
- const SYSTEM_LOG_TAG = 'EoaValidator';
8
-
9
- export async function validateEoaConfig(
10
- config: EoaExecutionConfig,
11
- walletStatus: EvmWalletConnectionStatus
12
- ): Promise<true | string> {
13
- if (!config.allowAny) {
14
- if (!config.specificAddress) {
15
- return "EOA execution selected, but no specific address was provided when 'allowAny' is false.";
16
- }
17
- if (!isValidEvmAddress(config.specificAddress)) {
18
- return `Invalid specific EOA address format: ${config.specificAddress}`;
19
- }
20
- if (walletStatus.isConnected && walletStatus.address) {
21
- if (walletStatus.address.toLowerCase() !== config.specificAddress.toLowerCase()) {
22
- return `Connected wallet address (${walletStatus.address}) does not match the required specific EOA address (${config.specificAddress}). Please connect the correct wallet.`;
23
- }
24
- } else if (walletStatus.isConnected && !walletStatus.address) {
25
- logger.warn(
26
- SYSTEM_LOG_TAG,
27
- 'Wallet is connected but address is unavailable for EOA validation.'
28
- );
29
- return 'Connected wallet address is not available for validation against specific EOA.';
30
- }
31
- }
32
- return true;
33
- }
@@ -1,2 +0,0 @@
1
- export * from './eoa';
2
- export * from './relayer';
@@ -1,13 +0,0 @@
1
- import { RelayerExecutionConfig } from '@openzeppelin/ui-types';
2
-
3
- export async function validateRelayerConfig(
4
- config: RelayerExecutionConfig
5
- ): Promise<true | string> {
6
- if (!config.serviceUrl) {
7
- return 'Relayer execution selected, but no service URL was provided.';
8
- }
9
- if (!config.relayer?.relayerId) {
10
- return 'Relayer execution selected, but no relayer was chosen from the list.';
11
- }
12
- return true;
13
- }
@@ -1,149 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- import { logger } from '@openzeppelin/ui-utils';
4
-
5
- import { resolveAndInitializeKitConfig } from '../utils';
6
-
7
- // Mock the logger to prevent console output during tests and allow spying
8
- vi.mock('@openzeppelin/ui-utils', () => ({
9
- logger: {
10
- debug: vi.fn(),
11
- info: vi.fn(),
12
- warn: vi.fn(),
13
- error: vi.fn(),
14
- },
15
- }));
16
-
17
- describe('resolveAndInitializeKitConfig', () => {
18
- const mockLoadConfigModule = vi.fn();
19
-
20
- beforeEach(() => {
21
- // Reset mocks before each test
22
- mockLoadConfigModule.mockReset();
23
- vi.clearAllMocks();
24
- });
25
-
26
- it('should return null if no kitName, programmaticConfig, or loadConfigModule is provided', async () => {
27
- const result = await resolveAndInitializeKitConfig(undefined, undefined, undefined);
28
- expect(result).toBeNull();
29
- expect(logger.debug).toHaveBeenCalledWith(
30
- 'resolveAndInitializeKitConfig',
31
- 'Resolving native config for kit: none',
32
- { hasProgrammaticKitConfig: false, hasLoadConfigModule: false }
33
- );
34
- expect(logger.debug).toHaveBeenCalledWith(
35
- 'resolveAndInitializeKitConfig',
36
- 'No native or programmatic kitConfig provided for none. Returning null.'
37
- );
38
- });
39
-
40
- it('should return programmaticKitConfig if kitName is provided but loadConfigModule is not', async () => {
41
- const programmaticConfig = { settingA: 'valueA' };
42
- const result = await resolveAndInitializeKitConfig('rainbowkit', programmaticConfig, undefined);
43
- expect(result).toEqual(programmaticConfig);
44
- expect(mockLoadConfigModule).not.toHaveBeenCalled();
45
- });
46
-
47
- it('should return null if kitName is provided, loadConfigModule is not, and no programmaticKitConfig', async () => {
48
- const result = await resolveAndInitializeKitConfig('rainbowkit', undefined, undefined);
49
- expect(result).toBeNull();
50
- });
51
-
52
- it('should not call loadConfigModule if kitName is "custom"', async () => {
53
- const programmaticConfig = { settingB: 'valueB' };
54
- const result = await resolveAndInitializeKitConfig(
55
- 'custom',
56
- programmaticConfig,
57
- mockLoadConfigModule
58
- );
59
- expect(result).toEqual(programmaticConfig);
60
- expect(mockLoadConfigModule).not.toHaveBeenCalled();
61
- });
62
-
63
- it('should not call loadConfigModule if kitName is "none"', async () => {
64
- const result = await resolveAndInitializeKitConfig('none', undefined, mockLoadConfigModule);
65
- expect(result).toBeNull();
66
- expect(mockLoadConfigModule).not.toHaveBeenCalled();
67
- });
68
-
69
- describe('with kitName and loadConfigModule provided', () => {
70
- const kitName = 'rainbowkit';
71
- const conventionalPath = `./config/wallet/${kitName}.config.ts`;
72
-
73
- it('should return userNativeConfig if loadConfigModule succeeds and no programmaticConfig', async () => {
74
- const nativeConfig = { nativeSetting: 'nativeValue' };
75
- mockLoadConfigModule.mockResolvedValue(nativeConfig);
76
- const result = await resolveAndInitializeKitConfig(kitName, undefined, mockLoadConfigModule);
77
- expect(result).toEqual(nativeConfig);
78
- expect(mockLoadConfigModule).toHaveBeenCalledWith(conventionalPath);
79
- });
80
-
81
- it('should merge userNativeConfig and programmaticKitConfig, with programmatic taking precedence', async () => {
82
- const nativeConfig = { common: 'native', nativeOnly: 'present' };
83
- const programmaticConfig = { common: 'programmatic', progOnly: 'here' };
84
- mockLoadConfigModule.mockResolvedValue(nativeConfig);
85
- const result = await resolveAndInitializeKitConfig(
86
- kitName,
87
- programmaticConfig,
88
- mockLoadConfigModule
89
- );
90
- expect(result).toEqual({
91
- common: 'programmatic',
92
- nativeOnly: 'present',
93
- progOnly: 'here',
94
- });
95
- expect(mockLoadConfigModule).toHaveBeenCalledWith(conventionalPath);
96
- });
97
-
98
- it('should return programmaticKitConfig if loadConfigModule returns null', async () => {
99
- const programmaticConfig = { settingC: 'valueC' };
100
- mockLoadConfigModule.mockResolvedValue(null);
101
- const result = await resolveAndInitializeKitConfig(
102
- kitName,
103
- programmaticConfig,
104
- mockLoadConfigModule
105
- );
106
- expect(result).toEqual(programmaticConfig);
107
- expect(mockLoadConfigModule).toHaveBeenCalledWith(conventionalPath);
108
- });
109
-
110
- it('should return null if loadConfigModule returns null and no programmaticConfig', async () => {
111
- mockLoadConfigModule.mockResolvedValue(null);
112
- const result = await resolveAndInitializeKitConfig(kitName, undefined, mockLoadConfigModule);
113
- expect(result).toBeNull();
114
- });
115
-
116
- it('should return programmaticKitConfig if loadConfigModule throws an error', async () => {
117
- const programmaticConfig = { settingD: 'valueD' };
118
- mockLoadConfigModule.mockImplementation(async () => {
119
- throw new Error('File system error');
120
- });
121
-
122
- await expect(
123
- resolveAndInitializeKitConfig(kitName, programmaticConfig, mockLoadConfigModule)
124
- ).resolves.toEqual(programmaticConfig);
125
-
126
- expect(mockLoadConfigModule).toHaveBeenCalledWith(conventionalPath);
127
- });
128
-
129
- it('should return null if loadConfigModule throws and no programmaticConfig', async () => {
130
- mockLoadConfigModule.mockImplementation(async () => {
131
- throw new Error('Another error');
132
- });
133
-
134
- await expect(
135
- resolveAndInitializeKitConfig(kitName, undefined, mockLoadConfigModule)
136
- ).resolves.toBeNull();
137
-
138
- expect(mockLoadConfigModule).toHaveBeenCalledWith(conventionalPath);
139
- });
140
- });
141
-
142
- it('should correctly use kitName in conventional path for loading', async () => {
143
- const kitName = 'anotherkit';
144
- const expectedPath = `./config/wallet/${kitName}.config.ts`;
145
- mockLoadConfigModule.mockResolvedValue({ some: 'data' });
146
- await resolveAndInitializeKitConfig(kitName, undefined, mockLoadConfigModule);
147
- expect(mockLoadConfigModule).toHaveBeenCalledWith(expectedPath);
148
- });
149
- });
@@ -1,52 +0,0 @@
1
- import { LogOut } from 'lucide-react';
2
- import React from 'react';
3
-
4
- import { Button } from '@openzeppelin/ui-components';
5
- import { useDerivedAccountStatus, useDerivedDisconnect } from '@openzeppelin/ui-react';
6
- import type { BaseComponentProps } from '@openzeppelin/ui-types';
7
- import { cn, truncateMiddle } from '@openzeppelin/ui-utils';
8
-
9
- import { SafeWagmiComponent } from '../../utils/SafeWagmiComponent';
10
-
11
- /**
12
- * A component that displays the connected account address and chain ID.
13
- * Also includes a disconnect button.
14
- */
15
- export const CustomAccountDisplay: React.FC<BaseComponentProps> = ({ className }) => {
16
- // Use the SafeWagmiComponent with null fallback
17
- return (
18
- <SafeWagmiComponent fallback={null}>
19
- <AccountDisplayContent className={className} />
20
- </SafeWagmiComponent>
21
- );
22
- };
23
-
24
- // Inner component that uses derived hooks
25
- const AccountDisplayContent: React.FC<{ className?: string }> = ({ className }) => {
26
- const { isConnected, address, chainId } = useDerivedAccountStatus();
27
- const { disconnect } = useDerivedDisconnect();
28
-
29
- if (!isConnected || !address || !disconnect) {
30
- return null;
31
- }
32
-
33
- return (
34
- <div className={cn('flex items-center gap-2', className)}>
35
- <div className="flex flex-col">
36
- <span className="text-xs font-medium">{truncateMiddle(address, 4, 4)}</span>
37
- <span className="text-[9px] text-muted-foreground -mt-0.5">
38
- {chainId ? `Chain ID: ${chainId}` : 'Chain ID: N/A'}
39
- </span>
40
- </div>
41
- <Button
42
- onClick={() => disconnect()}
43
- variant="ghost"
44
- size="icon"
45
- className="size-6 p-0"
46
- title="Disconnect wallet"
47
- >
48
- <LogOut className="size-3.5" />
49
- </Button>
50
- </div>
51
- );
52
- };
@@ -1,125 +0,0 @@
1
- import { Loader2, Wallet } from 'lucide-react';
2
- import React, { useEffect, useState } from 'react';
3
-
4
- import { Button } from '@openzeppelin/ui-components';
5
- import { useDerivedAccountStatus, useDerivedConnectStatus } from '@openzeppelin/ui-react';
6
- import type { BaseComponentProps } from '@openzeppelin/ui-types';
7
- import { cn } from '@openzeppelin/ui-utils';
8
-
9
- import { SafeWagmiComponent } from '../../utils/SafeWagmiComponent';
10
- import { ConnectorDialog } from './ConnectorDialog';
11
-
12
- /**
13
- * A button that allows users to connect their wallet.
14
- * Opens a dialog to select from available connectors.
15
- * @param hideWhenConnected - Whether to hide the button when wallet is connected (default: true)
16
- */
17
- export interface ConnectButtonProps extends BaseComponentProps {
18
- hideWhenConnected?: boolean;
19
- }
20
-
21
- export const CustomConnectButton: React.FC<ConnectButtonProps> = ({
22
- className,
23
- hideWhenConnected = true,
24
- }) => {
25
- const [dialogOpen, setDialogOpen] = useState(false);
26
-
27
- const unavailableButton = (
28
- <div className={cn('flex items-center', className)}>
29
- <Button disabled={true} variant="outline" size="sm" className="h-8 px-2 text-xs">
30
- <Wallet className="size-3.5 mr-1" />
31
- Wallet Unavailable
32
- </Button>
33
- </div>
34
- );
35
-
36
- return (
37
- <SafeWagmiComponent fallback={unavailableButton}>
38
- <ConnectButtonContent
39
- className={className}
40
- dialogOpen={dialogOpen}
41
- setDialogOpen={setDialogOpen}
42
- hideWhenConnected={hideWhenConnected}
43
- />
44
- </SafeWagmiComponent>
45
- );
46
- };
47
-
48
- const ConnectButtonContent: React.FC<{
49
- className?: string;
50
- dialogOpen: boolean;
51
- setDialogOpen: (open: boolean) => void;
52
- hideWhenConnected: boolean;
53
- }> = ({ className, dialogOpen, setDialogOpen, hideWhenConnected }) => {
54
- const { isConnected } = useDerivedAccountStatus();
55
- const { isConnecting: isHookConnecting, error: connectError } = useDerivedConnectStatus();
56
-
57
- // Local state to indicate the button has been clicked and dialog is open, awaiting user selection
58
- const [isManuallyInitiated, setIsManuallyInitiated] = useState(false);
59
-
60
- useEffect(() => {
61
- if (isConnected && hideWhenConnected) {
62
- setDialogOpen(false);
63
- setIsManuallyInitiated(false); // Reset if dialog closes due to connection
64
- }
65
- }, [isConnected, hideWhenConnected, setDialogOpen]);
66
-
67
- // If dialog is closed, reset manual initiation state
68
- useEffect(() => {
69
- if (!dialogOpen) {
70
- setIsManuallyInitiated(false);
71
- }
72
- }, [dialogOpen]);
73
-
74
- // If wagmi hook reports it's connecting, we no longer need our manual pending state
75
- useEffect(() => {
76
- if (isHookConnecting) {
77
- setIsManuallyInitiated(false);
78
- }
79
- }, [isHookConnecting]);
80
-
81
- const handleConnectClick = () => {
82
- if (!isConnected) {
83
- setIsManuallyInitiated(true); // User clicked, show pending on button
84
- setDialogOpen(true);
85
- }
86
- };
87
-
88
- if (isConnected && hideWhenConnected) {
89
- return null;
90
- }
91
-
92
- // Button shows loading if either hook reports connecting OR if user just clicked to open dialog
93
- const showButtonLoading = isHookConnecting || isManuallyInitiated;
94
-
95
- return (
96
- <div className={cn('flex items-center', className)}>
97
- <Button
98
- onClick={handleConnectClick}
99
- disabled={showButtonLoading || isConnected}
100
- variant="outline"
101
- size="sm"
102
- className="h-8 px-2 text-xs"
103
- title={isConnected ? 'Connected' : connectError?.message || 'Connect Wallet'}
104
- >
105
- {showButtonLoading ? (
106
- <Loader2 className="size-3.5 animate-spin mr-1" />
107
- ) : (
108
- <Wallet className="size-3.5 mr-1" />
109
- )}
110
- {showButtonLoading ? 'Connecting...' : 'Connect Wallet'}
111
- </Button>
112
-
113
- <ConnectorDialog
114
- open={dialogOpen}
115
- onOpenChange={(open) => {
116
- setDialogOpen(open);
117
- // If dialog is closed manually by user before selection, reset manual initiation
118
- if (!open) {
119
- setIsManuallyInitiated(false);
120
- }
121
- }}
122
- />
123
- </div>
124
- );
125
- };