@openzeppelin/adapter-stellar 1.0.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 (165) hide show
  1. package/README.md +272 -0
  2. package/dist/config.cjs +21 -0
  3. package/dist/config.cjs.map +1 -0
  4. package/dist/config.d.cts +8 -0
  5. package/dist/config.d.cts.map +1 -0
  6. package/dist/config.d.mts +8 -0
  7. package/dist/config.d.mts.map +1 -0
  8. package/dist/config.mjs +20 -0
  9. package/dist/config.mjs.map +1 -0
  10. package/dist/index.cjs +7564 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +261 -0
  13. package/dist/index.d.cts.map +1 -0
  14. package/dist/index.d.mts +263 -0
  15. package/dist/index.d.mts.map +1 -0
  16. package/dist/index.mjs +7529 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/metadata.cjs +22 -0
  19. package/dist/metadata.cjs.map +1 -0
  20. package/dist/metadata.d.cts +7 -0
  21. package/dist/metadata.d.cts.map +1 -0
  22. package/dist/metadata.d.mts +7 -0
  23. package/dist/metadata.d.mts.map +1 -0
  24. package/dist/metadata.mjs +21 -0
  25. package/dist/metadata.mjs.map +1 -0
  26. package/dist/networks-BrV516-R.d.cts +15 -0
  27. package/dist/networks-BrV516-R.d.cts.map +1 -0
  28. package/dist/networks-C0MmhJcu.d.mts +15 -0
  29. package/dist/networks-C0MmhJcu.d.mts.map +1 -0
  30. package/dist/networks-DgUFSTiC.cjs +76 -0
  31. package/dist/networks-DgUFSTiC.cjs.map +1 -0
  32. package/dist/networks-QbEPbaGT.mjs +46 -0
  33. package/dist/networks-QbEPbaGT.mjs.map +1 -0
  34. package/dist/networks.cjs +8 -0
  35. package/dist/networks.d.cts +2 -0
  36. package/dist/networks.d.mts +2 -0
  37. package/dist/networks.mjs +3 -0
  38. package/dist/vite-config.cjs +43 -0
  39. package/dist/vite-config.cjs.map +1 -0
  40. package/dist/vite-config.d.cts +35 -0
  41. package/dist/vite-config.d.cts.map +1 -0
  42. package/dist/vite-config.d.mts +35 -0
  43. package/dist/vite-config.d.mts.map +1 -0
  44. package/dist/vite-config.mjs +42 -0
  45. package/dist/vite-config.mjs.map +1 -0
  46. package/package.json +114 -0
  47. package/src/__tests__/getDefaultServiceConfig.test.ts +105 -0
  48. package/src/access-control/actions.ts +214 -0
  49. package/src/access-control/feature-detection.ts +238 -0
  50. package/src/access-control/index.ts +54 -0
  51. package/src/access-control/indexer-client.ts +1474 -0
  52. package/src/access-control/onchain-reader.ts +446 -0
  53. package/src/access-control/service.ts +1431 -0
  54. package/src/access-control/validation.ts +256 -0
  55. package/src/adapter.ts +659 -0
  56. package/src/config.ts +43 -0
  57. package/src/configuration/__tests__/explorer.test.ts +80 -0
  58. package/src/configuration/__tests__/rpc.test.ts +355 -0
  59. package/src/configuration/execution.ts +83 -0
  60. package/src/configuration/explorer.ts +105 -0
  61. package/src/configuration/index.ts +5 -0
  62. package/src/configuration/network-services.ts +210 -0
  63. package/src/configuration/rpc.ts +270 -0
  64. package/src/configuration.ts +2 -0
  65. package/src/contract/__tests__/complete-type-coverage.test.ts +78 -0
  66. package/src/contract/index.ts +3 -0
  67. package/src/contract/loader.ts +498 -0
  68. package/src/contract/transformer.ts +1 -0
  69. package/src/contract/type.ts +65 -0
  70. package/src/index.ts +23 -0
  71. package/src/mapping/constants.ts +89 -0
  72. package/src/mapping/enum-metadata.ts +237 -0
  73. package/src/mapping/field-generator.ts +296 -0
  74. package/src/mapping/index.ts +5 -0
  75. package/src/mapping/struct-fields.ts +106 -0
  76. package/src/mapping/tuple-components.ts +43 -0
  77. package/src/mapping/type-coverage-validator.ts +151 -0
  78. package/src/mapping/type-mapper.ts +203 -0
  79. package/src/metadata.ts +16 -0
  80. package/src/networks/README.md +84 -0
  81. package/src/networks/index.ts +19 -0
  82. package/src/networks/mainnet.ts +20 -0
  83. package/src/networks/testnet.ts +20 -0
  84. package/src/networks.ts +2 -0
  85. package/src/query/handler.ts +411 -0
  86. package/src/query/index.ts +4 -0
  87. package/src/query/view-checker.ts +32 -0
  88. package/src/sac/spec-cache.ts +68 -0
  89. package/src/sac/spec-source.ts +35 -0
  90. package/src/sac/xdr.ts +101 -0
  91. package/src/transaction/components/AdvancedInfo.tsx +34 -0
  92. package/src/transaction/components/FeeConfiguration.tsx +41 -0
  93. package/src/transaction/components/StellarRelayerOptions.tsx +60 -0
  94. package/src/transaction/components/TransactionTiming.tsx +77 -0
  95. package/src/transaction/components/index.ts +5 -0
  96. package/src/transaction/components/useStellarRelayerOptions.ts +114 -0
  97. package/src/transaction/eoa.ts +229 -0
  98. package/src/transaction/execution-strategy.ts +33 -0
  99. package/src/transaction/formatter.ts +296 -0
  100. package/src/transaction/index.ts +4 -0
  101. package/src/transaction/relayer.ts +575 -0
  102. package/src/transaction/sender.ts +156 -0
  103. package/src/transform/index.ts +4 -0
  104. package/src/transform/input-parser.ts +9 -0
  105. package/src/transform/output-formatter.ts +133 -0
  106. package/src/transform/parsers/complex-parser.ts +157 -0
  107. package/src/transform/parsers/generic-parser.ts +171 -0
  108. package/src/transform/parsers/index.ts +86 -0
  109. package/src/transform/parsers/primitive-parser.ts +123 -0
  110. package/src/transform/parsers/scval-converter.ts +405 -0
  111. package/src/transform/parsers/struct-parser.ts +324 -0
  112. package/src/transform/parsers/types.ts +35 -0
  113. package/src/types/__tests__/artifacts.test.ts +89 -0
  114. package/src/types/artifacts.ts +19 -0
  115. package/src/utils/__tests__/artifacts.test.ts +77 -0
  116. package/src/utils/artifacts.ts +30 -0
  117. package/src/utils/formatting.ts +122 -0
  118. package/src/utils/index.ts +6 -0
  119. package/src/utils/input-parsing.ts +336 -0
  120. package/src/utils/safe-type-parser.ts +303 -0
  121. package/src/utils/stellar-types.ts +35 -0
  122. package/src/utils/type-detection.ts +163 -0
  123. package/src/utils/xdr-ordering.ts +36 -0
  124. package/src/validation/__tests__/address.test.ts +267 -0
  125. package/src/validation/address.ts +136 -0
  126. package/src/validation/eoa.ts +33 -0
  127. package/src/validation/index.ts +3 -0
  128. package/src/validation/relayer.ts +13 -0
  129. package/src/vite-config.ts +67 -0
  130. package/src/wallet/README.md +93 -0
  131. package/src/wallet/__tests__/connection.test.ts +72 -0
  132. package/src/wallet/components/StellarWalletUiRoot.tsx +161 -0
  133. package/src/wallet/components/account/AccountDisplay.tsx +50 -0
  134. package/src/wallet/components/connect/ConnectButton.tsx +100 -0
  135. package/src/wallet/components/connect/ConnectorDialog.tsx +125 -0
  136. package/src/wallet/components/index.ts +3 -0
  137. package/src/wallet/connection.ts +151 -0
  138. package/src/wallet/context/StellarWalletContext.ts +32 -0
  139. package/src/wallet/context/index.ts +4 -0
  140. package/src/wallet/context/useStellarWalletContext.ts +17 -0
  141. package/src/wallet/hooks/facade-hooks.ts +31 -0
  142. package/src/wallet/hooks/index.ts +7 -0
  143. package/src/wallet/hooks/useStellarAccount.ts +27 -0
  144. package/src/wallet/hooks/useStellarConnect.ts +60 -0
  145. package/src/wallet/hooks/useStellarDisconnect.ts +47 -0
  146. package/src/wallet/hooks/useUiKitConfig.ts +40 -0
  147. package/src/wallet/implementation/wallets-kit-implementation.ts +379 -0
  148. package/src/wallet/index.ts +11 -0
  149. package/src/wallet/services/__tests__/configResolutionService.test.ts +163 -0
  150. package/src/wallet/services/configResolutionService.ts +65 -0
  151. package/src/wallet/stellar-wallets-kit/StellarWalletsKitConnectButton.tsx +82 -0
  152. package/src/wallet/stellar-wallets-kit/__mocks__/@creit.tech/stellar-wallets-kit.ts +48 -0
  153. package/src/wallet/stellar-wallets-kit/__tests__/export-service.test.ts +93 -0
  154. package/src/wallet/stellar-wallets-kit/__tests__/stellarUiKitManager.test.ts +0 -0
  155. package/src/wallet/stellar-wallets-kit/config-generator.ts +75 -0
  156. package/src/wallet/stellar-wallets-kit/export-service.ts +19 -0
  157. package/src/wallet/stellar-wallets-kit/index.ts +3 -0
  158. package/src/wallet/stellar-wallets-kit/stellarUiKitManager.ts +235 -0
  159. package/src/wallet/types.ts +19 -0
  160. package/src/wallet/utils/__tests__/filterWalletComponents.test.ts +150 -0
  161. package/src/wallet/utils/__tests__/uiKitService.test.ts +189 -0
  162. package/src/wallet/utils/filterWalletComponents.ts +89 -0
  163. package/src/wallet/utils/index.ts +3 -0
  164. package/src/wallet/utils/stellarWalletImplementationManager.ts +118 -0
  165. package/src/wallet/utils/uiKitService.ts +74 -0
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { UserExplorerConfig } from '@openzeppelin/ui-types';
4
+
5
+ import { validateStellarExplorerConfig } from '../../configuration/explorer';
6
+
7
+ describe('validateStellarExplorerConfig', () => {
8
+ it('should return true for valid configuration with all fields', () => {
9
+ const config: UserExplorerConfig = {
10
+ explorerUrl: 'https://stellarchain.io',
11
+ apiUrl: 'https://api.stellarchain.io',
12
+ apiKey: 'valid-key',
13
+ name: 'Test',
14
+ isCustom: true,
15
+ };
16
+ expect(validateStellarExplorerConfig(config)).toBe(true);
17
+ });
18
+
19
+ it('should return false for invalid explorerUrl', () => {
20
+ const config: UserExplorerConfig = {
21
+ explorerUrl: 'invalid-url',
22
+ apiUrl: 'https://api.stellarchain.io',
23
+ apiKey: 'valid-key',
24
+ name: 'Test',
25
+ isCustom: true,
26
+ };
27
+ expect(validateStellarExplorerConfig(config)).toBe(false);
28
+ });
29
+
30
+ it('should return false for invalid apiUrl', () => {
31
+ const config: UserExplorerConfig = {
32
+ explorerUrl: 'https://stellarchain.io',
33
+ apiUrl: 'invalid-url',
34
+ apiKey: 'valid-key',
35
+ name: 'Test',
36
+ isCustom: true,
37
+ };
38
+ expect(validateStellarExplorerConfig(config)).toBe(false);
39
+ });
40
+
41
+ it('should return true for configuration without apiKey', () => {
42
+ const config: UserExplorerConfig = {
43
+ explorerUrl: 'https://stellarchain.io',
44
+ apiUrl: 'https://api.stellarchain.io',
45
+ name: 'Test',
46
+ isCustom: true,
47
+ };
48
+ expect(validateStellarExplorerConfig(config)).toBe(true);
49
+ });
50
+
51
+ it('should return true for configuration without apiUrl', () => {
52
+ const config: UserExplorerConfig = {
53
+ explorerUrl: 'https://stellarchain.io',
54
+ apiKey: 'valid-key',
55
+ name: 'Test',
56
+ isCustom: true,
57
+ };
58
+ expect(validateStellarExplorerConfig(config)).toBe(true);
59
+ });
60
+
61
+ it('should return false for configuration without explorerUrl', () => {
62
+ const config = {
63
+ apiUrl: 'https://api.stellarchain.io',
64
+ apiKey: 'valid-key',
65
+ name: 'Test',
66
+ isCustom: true,
67
+ } as UserExplorerConfig;
68
+ expect(validateStellarExplorerConfig(config)).toBe(false);
69
+ });
70
+ });
71
+
72
+ /*
73
+ * NOTE: Connection testing removed for Stellar explorers.
74
+ *
75
+ * Unlike EVM, Stellar explorers are only used for generating display URLs,
76
+ * not for critical functionality. Testing website availability provides
77
+ * no functional value since contract loading uses Soroban RPC directly.
78
+ *
79
+ * See configuration/explorer.ts for detailed explanation.
80
+ */
@@ -0,0 +1,355 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { StellarNetworkConfig } from '@openzeppelin/ui-types';
4
+ import { appConfigService } from '@openzeppelin/ui-utils';
5
+
6
+ import { resolveRpcUrl, testStellarRpcConnection, validateStellarRpcEndpoint } from '../rpc';
7
+
8
+ // Helper to create a mock StellarNetworkConfig
9
+ const createMockConfig = (
10
+ id: string,
11
+ sorobanRpcUrl?: string,
12
+ name?: string
13
+ ): StellarNetworkConfig => ({
14
+ id,
15
+ name: name || `Test ${id}`,
16
+ ecosystem: 'stellar',
17
+ network: 'stellar',
18
+ type: 'testnet',
19
+ isTestnet: true,
20
+ exportConstName: id.replace(/-/g, ''),
21
+ horizonUrl: `https://horizon-${id}.stellar.org`,
22
+ sorobanRpcUrl: sorobanRpcUrl || '', // Allow undefined or empty for testing error cases
23
+ networkPassphrase: `Test ${id} Network`,
24
+ explorerUrl: `https://stellar.expert/explorer/${id}`,
25
+ });
26
+
27
+ // Mock the appConfigService from the correct package
28
+ vi.mock('@openzeppelin/ui-utils', async (importOriginal) => {
29
+ const original = await importOriginal<typeof import('@openzeppelin/ui-utils')>();
30
+ return {
31
+ ...original,
32
+ logger: {
33
+ debug: vi.fn(),
34
+ info: vi.fn(),
35
+ warn: vi.fn(),
36
+ error: vi.fn(),
37
+ },
38
+ appConfigService: {
39
+ getRpcEndpointOverride: vi.fn(),
40
+ getConfig: vi.fn().mockReturnValue({ rpcEndpoints: {} }),
41
+ },
42
+ };
43
+ });
44
+
45
+ describe('resolveRpcUrl', () => {
46
+ beforeEach(() => {
47
+ // Reset mocks before each test to ensure test isolation
48
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReset();
49
+ vi.mocked(appConfigService.getConfig).mockReturnValue({ rpcEndpoints: {} });
50
+ });
51
+
52
+ it('should use RPC override from AppConfigService if available (string)', () => {
53
+ const networkId = 'stellar-testnet-override';
54
+ const overrideRpcUrl = 'https://custom-soroban-rpc.example.com';
55
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(overrideRpcUrl);
56
+
57
+ const config = createMockConfig(networkId, 'https://soroban-testnet.stellar.org');
58
+ expect(resolveRpcUrl(config)).toBe(overrideRpcUrl);
59
+ });
60
+
61
+ it('should use RPC override from AppConfigService if available (object with http)', () => {
62
+ const networkId = 'stellar-testnet-object-override';
63
+ const overrideRpcUrl = 'https://custom-soroban-object.example.com';
64
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue({ http: overrideRpcUrl });
65
+
66
+ const config = createMockConfig(networkId, 'https://soroban-testnet.stellar.org');
67
+ expect(resolveRpcUrl(config)).toBe(overrideRpcUrl);
68
+ });
69
+
70
+ it('should fall back to networkConfig.sorobanRpcUrl if no override is available', () => {
71
+ const networkId = 'stellar-testnet-fallback';
72
+ const defaultRpcUrl = 'https://soroban-testnet.stellar.org';
73
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined);
74
+
75
+ const config = createMockConfig(networkId, defaultRpcUrl);
76
+ expect(resolveRpcUrl(config)).toBe(defaultRpcUrl);
77
+ });
78
+
79
+ it('should fall back to networkConfig.sorobanRpcUrl if override is invalid URL', () => {
80
+ const networkId = 'stellar-testnet-invalid-override';
81
+ const defaultRpcUrl = 'https://soroban-testnet.stellar.org';
82
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue('invalid-url');
83
+
84
+ const config = createMockConfig(networkId, defaultRpcUrl);
85
+ expect(resolveRpcUrl(config)).toBe(defaultRpcUrl);
86
+ });
87
+
88
+ it('should throw an error if no valid Soroban RPC URL (override or default) is found', () => {
89
+ const networkId = 'no-valid-soroban-rpc';
90
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined);
91
+ const config = createMockConfig(networkId, undefined);
92
+
93
+ expect(() => resolveRpcUrl(config)).toThrowError(
94
+ `No valid Soroban RPC URL configured for network Test ${networkId} (ID: ${networkId}).`
95
+ );
96
+ });
97
+
98
+ it('should throw an error if default Soroban RPC is invalid and no override is found', () => {
99
+ const networkId = 'invalid-default-soroban-rpc';
100
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined);
101
+ const config = createMockConfig(networkId, 'not-a-url');
102
+
103
+ expect(() => resolveRpcUrl(config)).toThrowError(
104
+ `No valid Soroban RPC URL configured for network Test ${networkId} (ID: ${networkId}).`
105
+ );
106
+ });
107
+
108
+ it('should use override even if default is invalid', () => {
109
+ const networkId = 'override-wins-over-invalid-default';
110
+ const overrideRpcUrl = 'https://good-soroban-override.example.com';
111
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(overrideRpcUrl);
112
+
113
+ const config = createMockConfig(networkId, 'bad-default-url');
114
+ expect(resolveRpcUrl(config)).toBe(overrideRpcUrl);
115
+ });
116
+
117
+ it('should work with mainnet Soroban RPC URL', () => {
118
+ const networkId = 'stellar-mainnet';
119
+ const mainnetRpcUrl = 'https://mainnet.sorobanrpc.com';
120
+ vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined);
121
+
122
+ const config = createMockConfig(networkId, mainnetRpcUrl);
123
+ expect(resolveRpcUrl(config)).toBe(mainnetRpcUrl);
124
+ });
125
+ });
126
+
127
+ describe('validateStellarRpcEndpoint', () => {
128
+ it('should return true for valid Soroban RPC URLs', () => {
129
+ const validConfigs = [
130
+ { url: 'https://soroban-testnet.stellar.org', name: 'Official Testnet', isCustom: false },
131
+ { url: 'https://mainnet.sorobanrpc.com', name: 'Official Mainnet', isCustom: false },
132
+ { url: 'https://stellar.publicnode.com', name: 'Public Node', isCustom: false },
133
+ { url: 'https://custom-soroban.example.com', name: 'Custom', isCustom: true },
134
+ ];
135
+
136
+ validConfigs.forEach((config) => {
137
+ expect(validateStellarRpcEndpoint(config)).toBe(true);
138
+ });
139
+ });
140
+
141
+ it('should return false for invalid URLs', () => {
142
+ const invalidConfigs = [
143
+ { url: 'not-a-url', name: 'Invalid', isCustom: false },
144
+ { url: '', name: 'Empty URL', isCustom: false },
145
+ ];
146
+
147
+ invalidConfigs.forEach((config) => {
148
+ expect(validateStellarRpcEndpoint(config)).toBe(false);
149
+ });
150
+ });
151
+
152
+ it('should return true for HTTP localhost URLs (development)', () => {
153
+ const devConfig = {
154
+ url: 'http://localhost:8000/soroban/rpc',
155
+ name: 'Local Dev',
156
+ isCustom: true,
157
+ };
158
+ expect(validateStellarRpcEndpoint(devConfig)).toBe(true);
159
+ });
160
+
161
+ it('should handle validation errors gracefully', () => {
162
+ // This test verifies the catch block works, but doMock doesn't work in the middle of test execution
163
+ // Instead, we'll test that the function returns false on invalid input
164
+ const config = { url: null as unknown as string, name: 'Test', isCustom: false };
165
+ expect(validateStellarRpcEndpoint(config)).toBe(false);
166
+ });
167
+ });
168
+
169
+ describe('testStellarRpcConnection', () => {
170
+ beforeEach(() => {
171
+ // Reset fetch mock before each test
172
+ global.fetch = vi.fn();
173
+ vi.clearAllTimers();
174
+ vi.useFakeTimers();
175
+
176
+ // Mock Date.now to simulate time passage
177
+ const mockDate = new Date('2023-01-01T00:00:00Z');
178
+ vi.setSystemTime(mockDate);
179
+ });
180
+
181
+ afterEach(() => {
182
+ vi.restoreAllMocks();
183
+ vi.useRealTimers();
184
+ });
185
+
186
+ it('should return success for healthy Soroban RPC endpoint', async () => {
187
+ const mockResponse = {
188
+ ok: true,
189
+ json: vi.fn().mockResolvedValue({
190
+ jsonrpc: '2.0',
191
+ id: 1,
192
+ result: { status: 'healthy' },
193
+ }),
194
+ };
195
+
196
+ // Mock fetch to simulate some latency
197
+ vi.mocked(global.fetch).mockImplementation(async () => {
198
+ // Advance time to simulate network latency
199
+ vi.advanceTimersByTime(100);
200
+ return mockResponse as unknown as Response;
201
+ });
202
+
203
+ const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false };
204
+ const result = await testStellarRpcConnection(config);
205
+
206
+ expect(result.success).toBe(true);
207
+ expect(result.latency).toBeGreaterThan(0);
208
+ expect(result.error).toBeUndefined();
209
+ });
210
+
211
+ it('should return success for valid getLatestLedger response (fallback)', async () => {
212
+ const mockResponse = {
213
+ ok: true,
214
+ json: vi.fn().mockResolvedValue({
215
+ jsonrpc: '2.0',
216
+ id: 1,
217
+ result: { sequence: 12345, hash: 'abc123' },
218
+ }),
219
+ };
220
+
221
+ // Mock fetch to simulate some latency
222
+ vi.mocked(global.fetch).mockImplementation(async () => {
223
+ // Advance time to simulate network latency
224
+ vi.advanceTimersByTime(150);
225
+ return mockResponse as unknown as Response;
226
+ });
227
+
228
+ const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false };
229
+ const result = await testStellarRpcConnection(config);
230
+
231
+ expect(result.success).toBe(true);
232
+ expect(result.latency).toBeGreaterThan(0);
233
+ });
234
+
235
+ it('should return failure for HTTP error responses', async () => {
236
+ const mockResponse = {
237
+ ok: false,
238
+ status: 500,
239
+ };
240
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response);
241
+
242
+ const config = { url: 'https://bad-soroban.example.com', name: 'Bad RPC', isCustom: true };
243
+ const result = await testStellarRpcConnection(config);
244
+
245
+ expect(result.success).toBe(false);
246
+ expect(result.error).toBe('HTTP error: 500');
247
+ });
248
+
249
+ it('should return failure for JSON-RPC error responses', async () => {
250
+ const mockResponse = {
251
+ ok: true,
252
+ json: vi.fn().mockResolvedValue({
253
+ jsonrpc: '2.0',
254
+ id: 1,
255
+ error: { code: -32601, message: 'Method not found' },
256
+ }),
257
+ };
258
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response);
259
+
260
+ const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false };
261
+ const result = await testStellarRpcConnection(config);
262
+
263
+ expect(result.success).toBe(false);
264
+ expect(result.error).toContain('Method not found');
265
+ });
266
+
267
+ it('should return failure for unhealthy service status', async () => {
268
+ const mockResponse = {
269
+ ok: true,
270
+ json: vi.fn().mockResolvedValue({
271
+ jsonrpc: '2.0',
272
+ id: 1,
273
+ result: { status: 'unhealthy' },
274
+ }),
275
+ };
276
+
277
+ // Mock fetch to simulate some latency
278
+ vi.mocked(global.fetch).mockImplementation(async () => {
279
+ // Advance time to simulate network latency
280
+ vi.advanceTimersByTime(120);
281
+ return mockResponse as unknown as Response;
282
+ });
283
+
284
+ const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false };
285
+ const result = await testStellarRpcConnection(config);
286
+
287
+ expect(result.success).toBe(false);
288
+ expect(result.error).toContain('unhealthy');
289
+ expect(result.latency).toBeGreaterThan(0);
290
+ });
291
+
292
+ it('should handle connection timeout', async () => {
293
+ // Create a mock that immediately rejects with AbortError
294
+ vi.mocked(global.fetch).mockRejectedValue(
295
+ (() => {
296
+ const error = new Error('The operation was aborted');
297
+ error.name = 'AbortError';
298
+ return error;
299
+ })()
300
+ );
301
+
302
+ const config = { url: 'https://slow-soroban.example.com', name: 'Slow RPC', isCustom: true };
303
+ const result = await testStellarRpcConnection(config, 1000);
304
+
305
+ expect(result.success).toBe(false);
306
+ expect(result.error).toContain('timeout');
307
+ });
308
+
309
+ it('should return failure for missing URL', async () => {
310
+ const config = { url: '', name: 'Empty URL', isCustom: false };
311
+ const result = await testStellarRpcConnection(config);
312
+
313
+ expect(result.success).toBe(false);
314
+ expect(result.error).toBe('Soroban RPC URL is required');
315
+ });
316
+
317
+ it('should handle network errors gracefully', async () => {
318
+ vi.mocked(global.fetch).mockRejectedValue(new Error('Network error'));
319
+
320
+ const config = {
321
+ url: 'https://unreachable-soroban.example.com',
322
+ name: 'Unreachable',
323
+ isCustom: true,
324
+ };
325
+ const result = await testStellarRpcConnection(config);
326
+
327
+ expect(result.success).toBe(false);
328
+ expect(result.error).toBe('Network error');
329
+ });
330
+
331
+ it('should use custom timeout value', async () => {
332
+ const mockResponse = {
333
+ ok: true,
334
+ json: vi.fn().mockResolvedValue({
335
+ jsonrpc: '2.0',
336
+ id: 1,
337
+ result: { status: 'healthy' },
338
+ }),
339
+ };
340
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response);
341
+
342
+ const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false };
343
+ const result = await testStellarRpcConnection(config, 10000); // 10 second timeout
344
+
345
+ expect(result.success).toBe(true);
346
+
347
+ // Verify fetch was called with the signal
348
+ expect(global.fetch).toHaveBeenCalledWith(
349
+ expect.any(String),
350
+ expect.objectContaining({
351
+ signal: expect.any(AbortSignal),
352
+ })
353
+ );
354
+ });
355
+ });
@@ -0,0 +1,83 @@
1
+ import type {
2
+ EoaExecutionConfig,
3
+ ExecutionConfig,
4
+ ExecutionMethodDetail,
5
+ MultisigExecutionConfig,
6
+ RelayerExecutionConfig,
7
+ } from '@openzeppelin/ui-types';
8
+ import { logger } from '@openzeppelin/ui-utils';
9
+
10
+ import { validateEoaConfig, validateRelayerConfig } from '../validation';
11
+ import { StellarWalletConnectionStatus } from '../wallet/types';
12
+
13
+ const SYSTEM_LOG_TAG = 'adapter-stellar-execution-config';
14
+
15
+ /**
16
+ * Returns details for execution methods supported by the Stellar adapter.
17
+ */
18
+ export async function getStellarSupportedExecutionMethods(): Promise<ExecutionMethodDetail[]> {
19
+ logger.warn(
20
+ 'adapter-stellar-execution-config',
21
+ 'getStellarSupportedExecutionMethods is using placeholder implementation.'
22
+ );
23
+ // TODO: Implement actual supported methods for Stellar (e.g., EOA, Relayer).
24
+ return Promise.resolve([
25
+ {
26
+ type: 'eoa',
27
+ name: 'EOA (External Account)',
28
+ description: 'Execute using a standard Stellar account address.',
29
+ },
30
+ {
31
+ type: 'relayer',
32
+ name: 'OpenZeppelin Relayer',
33
+ description: 'Execute via a OpenZeppelin open source transaction relayer service.',
34
+ disabled: false,
35
+ },
36
+ {
37
+ type: 'multisig',
38
+ name: 'Stellar Multisig', // Example for future
39
+ description: 'Execute via a Stellar multisignature configuration.',
40
+ disabled: true,
41
+ },
42
+ ]);
43
+ }
44
+
45
+ /**
46
+ * Validates Multisig execution configuration (placeholder).
47
+ */
48
+ async function _validateMultisigConfig(
49
+ _config: MultisigExecutionConfig,
50
+ _walletStatus: StellarWalletConnectionStatus
51
+ ): Promise<true | string> {
52
+ logger.info(SYSTEM_LOG_TAG, 'Multisig execution config validation: Not yet fully implemented.');
53
+ // TODO: Add validation for Stellar multisig configuration, required signers, etc.
54
+ return true; // Placeholder
55
+ }
56
+
57
+ /**
58
+ * Validates the complete execution configuration object against the
59
+ * requirements and capabilities of the Stellar adapter.
60
+ */
61
+ export async function validateStellarExecutionConfig(
62
+ config: ExecutionConfig,
63
+ walletStatus: StellarWalletConnectionStatus
64
+ ): Promise<true | string> {
65
+ logger.info(SYSTEM_LOG_TAG, 'Validating Stellar execution config:', { config, walletStatus });
66
+
67
+ switch (config.method) {
68
+ case 'eoa':
69
+ return validateEoaConfig(config as EoaExecutionConfig, walletStatus);
70
+ case 'relayer':
71
+ return validateRelayerConfig(config as RelayerExecutionConfig);
72
+ case 'multisig':
73
+ return _validateMultisigConfig(config as MultisigExecutionConfig, walletStatus);
74
+ default: {
75
+ const unknownMethod = (config as ExecutionConfig).method;
76
+ logger.warn(
77
+ SYSTEM_LOG_TAG,
78
+ `Unsupported execution method type encountered: ${unknownMethod}`
79
+ );
80
+ return `Unsupported execution method type: ${unknownMethod}`;
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Stellar Explorer Configuration
3
+ *
4
+ * DESIGN NOTE: This module provides minimal explorer functionality compared to EVM adapters.
5
+ * Stellar explorers are used only for generating display URLs, unlike EVM where explorers
6
+ * are critical infrastructure for ABI fetching. See comments below for detailed explanation.
7
+ */
8
+
9
+ import { NetworkConfig } from '@openzeppelin/ui-types';
10
+ import type { UserExplorerConfig } from '@openzeppelin/ui-types';
11
+
12
+ import { isValidContractAddress } from '../validation';
13
+
14
+ /**
15
+ * Gets a blockchain explorer URL for an address on Stellar.
16
+ * Uses the explorerUrl from the network configuration.
17
+ *
18
+ * @param address The address to get the explorer URL for
19
+ * @param networkConfig The network configuration object.
20
+ * @returns A URL to view the address on the configured Stellar explorer, or null.
21
+ */
22
+ export function getStellarExplorerAddressUrl(
23
+ address: string,
24
+ networkConfig: NetworkConfig
25
+ ): string | null {
26
+ if (!address || !networkConfig.explorerUrl) {
27
+ return null;
28
+ }
29
+ // Use /contract for Soroban contract IDs, otherwise /account
30
+ const baseUrl = networkConfig.explorerUrl.replace(/\/+$/, '');
31
+ const path = isValidContractAddress(address) ? 'contract' : 'account';
32
+ return `${baseUrl}/${path}/${encodeURIComponent(address)}`;
33
+ }
34
+
35
+ /**
36
+ * Gets a blockchain explorer URL for a transaction on Stellar.
37
+ * Uses the explorerUrl from the network configuration.
38
+ *
39
+ * @param txHash - The hash of the transaction to get the explorer URL for
40
+ * @param networkConfig The network configuration object.
41
+ * @returns A URL to view the transaction on the configured Stellar explorer, or null.
42
+ */
43
+ export function getStellarExplorerTxUrl(
44
+ txHash: string,
45
+ networkConfig: NetworkConfig
46
+ ): string | null {
47
+ if (!txHash || !networkConfig.explorerUrl) {
48
+ return null;
49
+ }
50
+ // Construct the URL, assuming a standard /tx/ path for Stellar explorers
51
+ const baseUrl = networkConfig.explorerUrl.replace(/\/+$/, '');
52
+ return `${baseUrl}/tx/${encodeURIComponent(txHash)}`;
53
+ }
54
+
55
+ /**
56
+ * Validates a Stellar explorer configuration.
57
+ *
58
+ * NOTE: This validation is minimal compared to EVM - only checks URL formats.
59
+ * No API key validation or connection testing since Stellar explorers are
60
+ * display-only (not used for contract ABI fetching like in EVM).
61
+ */
62
+ export function validateStellarExplorerConfig(explorerConfig: UserExplorerConfig): boolean {
63
+ // Validate URLs if provided
64
+ if (explorerConfig.explorerUrl) {
65
+ try {
66
+ new URL(explorerConfig.explorerUrl);
67
+ } catch {
68
+ return false;
69
+ }
70
+ } else {
71
+ // explorerUrl is required
72
+ return false;
73
+ }
74
+
75
+ if (explorerConfig.apiUrl) {
76
+ try {
77
+ new URL(explorerConfig.apiUrl);
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ // Basic API key validation (not empty)
84
+ if (explorerConfig.apiKey !== undefined && explorerConfig.apiKey.trim().length === 0) {
85
+ return false;
86
+ }
87
+
88
+ return true;
89
+ }
90
+
91
+ /*
92
+ * NOTE: Unlike EVM adapters, Stellar does not implement explorer connection testing.
93
+ *
94
+ * DESIGN DECISION: Stellar explorers are used only for generating display URLs,
95
+ * not for critical functionality like ABI fetching (which EVM requires).
96
+ *
97
+ * Key differences from EVM:
98
+ * - EVM: Explorers provide essential APIs for contract ABI fetching
99
+ * - Stellar: Explorers are display-only; contract loading uses Soroban RPC directly
100
+ * - EVM: Multiple explorer providers with varying API formats requiring validation
101
+ * - Stellar: Standardized ecosystem using Horizon API underneath
102
+ *
103
+ * Therefore, testing explorer "connectivity" would only verify website availability,
104
+ * which provides no functional value and adds unnecessary complexity.
105
+ */
@@ -0,0 +1,5 @@
1
+ // Barrel file for configuration modules
2
+ export * from './execution';
3
+ export * from './explorer';
4
+ export * from './network-services';
5
+ export * from './rpc';