@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.
- package/README.md +272 -0
- package/dist/config.cjs +21 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +8 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +8 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +20 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.cjs +7564 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +261 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +263 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7529 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metadata.cjs +22 -0
- package/dist/metadata.cjs.map +1 -0
- package/dist/metadata.d.cts +7 -0
- package/dist/metadata.d.cts.map +1 -0
- package/dist/metadata.d.mts +7 -0
- package/dist/metadata.d.mts.map +1 -0
- package/dist/metadata.mjs +21 -0
- package/dist/metadata.mjs.map +1 -0
- package/dist/networks-BrV516-R.d.cts +15 -0
- package/dist/networks-BrV516-R.d.cts.map +1 -0
- package/dist/networks-C0MmhJcu.d.mts +15 -0
- package/dist/networks-C0MmhJcu.d.mts.map +1 -0
- package/dist/networks-DgUFSTiC.cjs +76 -0
- package/dist/networks-DgUFSTiC.cjs.map +1 -0
- package/dist/networks-QbEPbaGT.mjs +46 -0
- package/dist/networks-QbEPbaGT.mjs.map +1 -0
- package/dist/networks.cjs +8 -0
- package/dist/networks.d.cts +2 -0
- package/dist/networks.d.mts +2 -0
- package/dist/networks.mjs +3 -0
- package/dist/vite-config.cjs +43 -0
- package/dist/vite-config.cjs.map +1 -0
- package/dist/vite-config.d.cts +35 -0
- package/dist/vite-config.d.cts.map +1 -0
- package/dist/vite-config.d.mts +35 -0
- package/dist/vite-config.d.mts.map +1 -0
- package/dist/vite-config.mjs +42 -0
- package/dist/vite-config.mjs.map +1 -0
- package/package.json +114 -0
- package/src/__tests__/getDefaultServiceConfig.test.ts +105 -0
- package/src/access-control/actions.ts +214 -0
- package/src/access-control/feature-detection.ts +238 -0
- package/src/access-control/index.ts +54 -0
- package/src/access-control/indexer-client.ts +1474 -0
- package/src/access-control/onchain-reader.ts +446 -0
- package/src/access-control/service.ts +1431 -0
- package/src/access-control/validation.ts +256 -0
- package/src/adapter.ts +659 -0
- package/src/config.ts +43 -0
- package/src/configuration/__tests__/explorer.test.ts +80 -0
- package/src/configuration/__tests__/rpc.test.ts +355 -0
- package/src/configuration/execution.ts +83 -0
- package/src/configuration/explorer.ts +105 -0
- package/src/configuration/index.ts +5 -0
- package/src/configuration/network-services.ts +210 -0
- package/src/configuration/rpc.ts +270 -0
- package/src/configuration.ts +2 -0
- package/src/contract/__tests__/complete-type-coverage.test.ts +78 -0
- package/src/contract/index.ts +3 -0
- package/src/contract/loader.ts +498 -0
- package/src/contract/transformer.ts +1 -0
- package/src/contract/type.ts +65 -0
- package/src/index.ts +23 -0
- package/src/mapping/constants.ts +89 -0
- package/src/mapping/enum-metadata.ts +237 -0
- package/src/mapping/field-generator.ts +296 -0
- package/src/mapping/index.ts +5 -0
- package/src/mapping/struct-fields.ts +106 -0
- package/src/mapping/tuple-components.ts +43 -0
- package/src/mapping/type-coverage-validator.ts +151 -0
- package/src/mapping/type-mapper.ts +203 -0
- package/src/metadata.ts +16 -0
- package/src/networks/README.md +84 -0
- package/src/networks/index.ts +19 -0
- package/src/networks/mainnet.ts +20 -0
- package/src/networks/testnet.ts +20 -0
- package/src/networks.ts +2 -0
- package/src/query/handler.ts +411 -0
- package/src/query/index.ts +4 -0
- package/src/query/view-checker.ts +32 -0
- package/src/sac/spec-cache.ts +68 -0
- package/src/sac/spec-source.ts +35 -0
- package/src/sac/xdr.ts +101 -0
- package/src/transaction/components/AdvancedInfo.tsx +34 -0
- package/src/transaction/components/FeeConfiguration.tsx +41 -0
- package/src/transaction/components/StellarRelayerOptions.tsx +60 -0
- package/src/transaction/components/TransactionTiming.tsx +77 -0
- package/src/transaction/components/index.ts +5 -0
- package/src/transaction/components/useStellarRelayerOptions.ts +114 -0
- package/src/transaction/eoa.ts +229 -0
- package/src/transaction/execution-strategy.ts +33 -0
- package/src/transaction/formatter.ts +296 -0
- package/src/transaction/index.ts +4 -0
- package/src/transaction/relayer.ts +575 -0
- package/src/transaction/sender.ts +156 -0
- package/src/transform/index.ts +4 -0
- package/src/transform/input-parser.ts +9 -0
- package/src/transform/output-formatter.ts +133 -0
- package/src/transform/parsers/complex-parser.ts +157 -0
- package/src/transform/parsers/generic-parser.ts +171 -0
- package/src/transform/parsers/index.ts +86 -0
- package/src/transform/parsers/primitive-parser.ts +123 -0
- package/src/transform/parsers/scval-converter.ts +405 -0
- package/src/transform/parsers/struct-parser.ts +324 -0
- package/src/transform/parsers/types.ts +35 -0
- package/src/types/__tests__/artifacts.test.ts +89 -0
- package/src/types/artifacts.ts +19 -0
- package/src/utils/__tests__/artifacts.test.ts +77 -0
- package/src/utils/artifacts.ts +30 -0
- package/src/utils/formatting.ts +122 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/input-parsing.ts +336 -0
- package/src/utils/safe-type-parser.ts +303 -0
- package/src/utils/stellar-types.ts +35 -0
- package/src/utils/type-detection.ts +163 -0
- package/src/utils/xdr-ordering.ts +36 -0
- package/src/validation/__tests__/address.test.ts +267 -0
- package/src/validation/address.ts +136 -0
- package/src/validation/eoa.ts +33 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/relayer.ts +13 -0
- package/src/vite-config.ts +67 -0
- package/src/wallet/README.md +93 -0
- package/src/wallet/__tests__/connection.test.ts +72 -0
- package/src/wallet/components/StellarWalletUiRoot.tsx +161 -0
- package/src/wallet/components/account/AccountDisplay.tsx +50 -0
- package/src/wallet/components/connect/ConnectButton.tsx +100 -0
- package/src/wallet/components/connect/ConnectorDialog.tsx +125 -0
- package/src/wallet/components/index.ts +3 -0
- package/src/wallet/connection.ts +151 -0
- package/src/wallet/context/StellarWalletContext.ts +32 -0
- package/src/wallet/context/index.ts +4 -0
- package/src/wallet/context/useStellarWalletContext.ts +17 -0
- package/src/wallet/hooks/facade-hooks.ts +31 -0
- package/src/wallet/hooks/index.ts +7 -0
- package/src/wallet/hooks/useStellarAccount.ts +27 -0
- package/src/wallet/hooks/useStellarConnect.ts +60 -0
- package/src/wallet/hooks/useStellarDisconnect.ts +47 -0
- package/src/wallet/hooks/useUiKitConfig.ts +40 -0
- package/src/wallet/implementation/wallets-kit-implementation.ts +379 -0
- package/src/wallet/index.ts +11 -0
- package/src/wallet/services/__tests__/configResolutionService.test.ts +163 -0
- package/src/wallet/services/configResolutionService.ts +65 -0
- package/src/wallet/stellar-wallets-kit/StellarWalletsKitConnectButton.tsx +82 -0
- package/src/wallet/stellar-wallets-kit/__mocks__/@creit.tech/stellar-wallets-kit.ts +48 -0
- package/src/wallet/stellar-wallets-kit/__tests__/export-service.test.ts +93 -0
- package/src/wallet/stellar-wallets-kit/__tests__/stellarUiKitManager.test.ts +0 -0
- package/src/wallet/stellar-wallets-kit/config-generator.ts +75 -0
- package/src/wallet/stellar-wallets-kit/export-service.ts +19 -0
- package/src/wallet/stellar-wallets-kit/index.ts +3 -0
- package/src/wallet/stellar-wallets-kit/stellarUiKitManager.ts +235 -0
- package/src/wallet/types.ts +19 -0
- package/src/wallet/utils/__tests__/filterWalletComponents.test.ts +150 -0
- package/src/wallet/utils/__tests__/uiKitService.test.ts +189 -0
- package/src/wallet/utils/filterWalletComponents.ts +89 -0
- package/src/wallet/utils/index.ts +3 -0
- package/src/wallet/utils/stellarWalletImplementationManager.ts +118 -0
- package/src/wallet/utils/uiKitService.ts +74 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NetworkServiceForm,
|
|
3
|
+
StellarNetworkConfig,
|
|
4
|
+
UserRpcProviderConfig,
|
|
5
|
+
} from '@openzeppelin/ui-types';
|
|
6
|
+
import { isValidUrl } from '@openzeppelin/ui-utils';
|
|
7
|
+
|
|
8
|
+
import { testStellarRpcConnection, validateStellarRpcEndpoint } from './rpc';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the default service configuration values for a given service ID.
|
|
12
|
+
* Used for proactive health checks when no user overrides are configured.
|
|
13
|
+
*
|
|
14
|
+
* @param networkConfig The network configuration
|
|
15
|
+
* @param serviceId The service identifier (e.g., 'rpc', 'access-control-indexer')
|
|
16
|
+
* @returns The default configuration values, or null if not available
|
|
17
|
+
*/
|
|
18
|
+
export function getStellarDefaultServiceConfig(
|
|
19
|
+
networkConfig: StellarNetworkConfig,
|
|
20
|
+
serviceId: string
|
|
21
|
+
): Record<string, unknown> | null {
|
|
22
|
+
switch (serviceId) {
|
|
23
|
+
case 'rpc':
|
|
24
|
+
if (networkConfig.sorobanRpcUrl) {
|
|
25
|
+
return { sorobanRpcUrl: networkConfig.sorobanRpcUrl };
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
case 'access-control-indexer':
|
|
29
|
+
// Access control indexer is optional for Stellar - only return if both URLs are configured
|
|
30
|
+
if (networkConfig.indexerUri && networkConfig.indexerWsUri) {
|
|
31
|
+
return {
|
|
32
|
+
indexerUri: networkConfig.indexerUri,
|
|
33
|
+
indexerWsUri: networkConfig.indexerWsUri,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the network service forms for Stellar networks.
|
|
43
|
+
* Defines the UI configuration for the RPC and Access Control Indexer services.
|
|
44
|
+
*
|
|
45
|
+
* @param exclude Optional array of service IDs to exclude from the returned forms
|
|
46
|
+
* @returns Array of network service forms
|
|
47
|
+
*/
|
|
48
|
+
export function getStellarNetworkServiceForms(exclude: string[] = []): NetworkServiceForm[] {
|
|
49
|
+
const forms: NetworkServiceForm[] = [
|
|
50
|
+
{
|
|
51
|
+
id: 'rpc',
|
|
52
|
+
label: 'RPC Provider',
|
|
53
|
+
fields: [
|
|
54
|
+
{
|
|
55
|
+
id: 'stellar-rpc-url',
|
|
56
|
+
name: 'sorobanRpcUrl',
|
|
57
|
+
type: 'text',
|
|
58
|
+
label: 'Soroban RPC URL',
|
|
59
|
+
placeholder: 'https://soroban.stellar.org',
|
|
60
|
+
validation: { required: true, pattern: '^https?://.+' },
|
|
61
|
+
width: 'full',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'access-control-indexer',
|
|
67
|
+
label: 'Access Control Indexer',
|
|
68
|
+
description:
|
|
69
|
+
'Optional GraphQL indexer endpoint for historical access control data. Overrides the default indexer URL for this network.',
|
|
70
|
+
supportsConnectionTest: true,
|
|
71
|
+
requiredFeature: 'access_control_indexer',
|
|
72
|
+
fields: [
|
|
73
|
+
{
|
|
74
|
+
id: 'stellar-access-control-indexer-uri',
|
|
75
|
+
name: 'indexerUri',
|
|
76
|
+
type: 'text',
|
|
77
|
+
label: 'Access Control Indexer GraphQL Endpoint',
|
|
78
|
+
placeholder: 'https://indexer.example.com/graphql',
|
|
79
|
+
validation: { required: false, pattern: '^https?://.+' },
|
|
80
|
+
width: 'full',
|
|
81
|
+
helperText:
|
|
82
|
+
'Optional. Used for querying historical access control events and role discovery.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'stellar-access-control-indexer-ws-uri',
|
|
86
|
+
name: 'indexerWsUri',
|
|
87
|
+
type: 'text',
|
|
88
|
+
label: 'Access Control Indexer GraphQL WebSocket Endpoint',
|
|
89
|
+
placeholder: 'wss://indexer.example.com/graphql',
|
|
90
|
+
validation: { required: false, pattern: '^wss?://.+' },
|
|
91
|
+
width: 'full',
|
|
92
|
+
helperText: 'Optional. Used for real-time subscriptions.',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return forms.filter((form) => !exclude.includes(form.id));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates a network service configuration for Stellar networks.
|
|
103
|
+
*/
|
|
104
|
+
export async function validateStellarNetworkServiceConfig(
|
|
105
|
+
serviceId: string,
|
|
106
|
+
values: Record<string, unknown>
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
if (serviceId === 'rpc') {
|
|
109
|
+
const cfg = {
|
|
110
|
+
url: String(values.sorobanRpcUrl || ''),
|
|
111
|
+
isCustom: true,
|
|
112
|
+
} as UserRpcProviderConfig;
|
|
113
|
+
return validateStellarRpcEndpoint(cfg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (serviceId === 'access-control-indexer') {
|
|
117
|
+
// Validate indexerUri if provided
|
|
118
|
+
if (values.indexerUri !== undefined && values.indexerUri !== null && values.indexerUri !== '') {
|
|
119
|
+
if (!isValidUrl(String(values.indexerUri))) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate indexerWsUri if provided
|
|
125
|
+
if (
|
|
126
|
+
values.indexerWsUri !== undefined &&
|
|
127
|
+
values.indexerWsUri !== null &&
|
|
128
|
+
values.indexerWsUri !== ''
|
|
129
|
+
) {
|
|
130
|
+
if (!isValidUrl(String(values.indexerWsUri))) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Tests a network service connection for Stellar networks.
|
|
143
|
+
*/
|
|
144
|
+
export async function testStellarNetworkServiceConnection(
|
|
145
|
+
serviceId: string,
|
|
146
|
+
values: Record<string, unknown>
|
|
147
|
+
): Promise<{ success: boolean; latency?: number; error?: string }> {
|
|
148
|
+
if (serviceId === 'rpc') {
|
|
149
|
+
const cfg = {
|
|
150
|
+
url: String(values.sorobanRpcUrl || ''),
|
|
151
|
+
isCustom: true,
|
|
152
|
+
} as UserRpcProviderConfig;
|
|
153
|
+
return testStellarRpcConnection(cfg);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (serviceId === 'access-control-indexer') {
|
|
157
|
+
const indexerUri = values.indexerUri;
|
|
158
|
+
|
|
159
|
+
// If no indexer URI is provided, indexer is optional - return success (nothing to test)
|
|
160
|
+
if (!indexerUri || typeof indexerUri !== 'string' || indexerUri.trim() === '') {
|
|
161
|
+
return { success: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isValidUrl(indexerUri)) {
|
|
165
|
+
return { success: false, error: 'Invalid indexer URI format' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
// Perform a simple GraphQL introspection query to test connectivity
|
|
171
|
+
const response = await fetch(indexerUri, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
query: '{ __typename }',
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const latency = Date.now() - startTime;
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
latency,
|
|
187
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
if (data.errors) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
latency,
|
|
196
|
+
error: `GraphQL errors: ${JSON.stringify(data.errors)}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { success: true, latency };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { success: true };
|
|
210
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { StellarNetworkConfig, UserRpcProviderConfig } from '@openzeppelin/ui-types';
|
|
2
|
+
import { appConfigService, isValidUrl, logger, userRpcConfigService } from '@openzeppelin/ui-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a complete RPC URL from a user RPC provider configuration.
|
|
6
|
+
* For Stellar (Soroban), this just returns the URL as-is since
|
|
7
|
+
* users are providing complete RPC URLs including any API keys.
|
|
8
|
+
*
|
|
9
|
+
* @param config The user RPC provider configuration
|
|
10
|
+
* @returns The RPC URL
|
|
11
|
+
*/
|
|
12
|
+
export function buildRpcUrl(config: UserRpcProviderConfig): string {
|
|
13
|
+
return config.url;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the RPC URL for a given Stellar network configuration.
|
|
18
|
+
* Priority order:
|
|
19
|
+
* 1. User-provided RPC configuration (from UserRpcConfigService)
|
|
20
|
+
* 2. RPC URL override from AppConfigService
|
|
21
|
+
* 3. Default sorobanRpcUrl from the network configuration
|
|
22
|
+
*
|
|
23
|
+
* @param networkConfig - The Stellar network configuration.
|
|
24
|
+
* @returns The resolved RPC URL string.
|
|
25
|
+
* @throws If no RPC URL can be resolved (neither user config, override, nor default is present and valid).
|
|
26
|
+
*/
|
|
27
|
+
export function resolveRpcUrl(networkConfig: StellarNetworkConfig): string {
|
|
28
|
+
const logSystem = 'StellarRpcResolver';
|
|
29
|
+
const networkId = networkConfig.id;
|
|
30
|
+
|
|
31
|
+
// First priority: Check user-provided RPC configuration
|
|
32
|
+
const userRpcConfig = userRpcConfigService.getUserRpcConfig(networkId);
|
|
33
|
+
if (userRpcConfig) {
|
|
34
|
+
const userRpcUrl = buildRpcUrl(userRpcConfig);
|
|
35
|
+
if (isValidUrl(userRpcUrl)) {
|
|
36
|
+
logger.info(
|
|
37
|
+
logSystem,
|
|
38
|
+
`Using user-configured Soroban RPC URL for network ${networkId}: ${userRpcConfig.name || 'Custom'}`
|
|
39
|
+
);
|
|
40
|
+
return userRpcUrl;
|
|
41
|
+
} else {
|
|
42
|
+
logger.warn(
|
|
43
|
+
logSystem,
|
|
44
|
+
`User-configured Soroban RPC URL for ${networkId} is invalid: ${userRpcUrl}. Falling back.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Second priority: Check AppConfigService for an override
|
|
50
|
+
const rpcOverrideSetting = appConfigService.getRpcEndpointOverride(networkId);
|
|
51
|
+
let rpcUrlFromOverride: string | undefined;
|
|
52
|
+
|
|
53
|
+
if (typeof rpcOverrideSetting === 'string') {
|
|
54
|
+
rpcUrlFromOverride = rpcOverrideSetting;
|
|
55
|
+
} else if (typeof rpcOverrideSetting === 'object' && rpcOverrideSetting) {
|
|
56
|
+
// Check if it's a UserRpcProviderConfig
|
|
57
|
+
if ('url' in rpcOverrideSetting && 'isCustom' in rpcOverrideSetting) {
|
|
58
|
+
const userConfig = rpcOverrideSetting as UserRpcProviderConfig;
|
|
59
|
+
rpcUrlFromOverride = buildRpcUrl(userConfig);
|
|
60
|
+
} else if ('http' in rpcOverrideSetting) {
|
|
61
|
+
// It's an RpcEndpointConfig
|
|
62
|
+
rpcUrlFromOverride = rpcOverrideSetting.http;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (rpcUrlFromOverride) {
|
|
67
|
+
logger.info(
|
|
68
|
+
logSystem,
|
|
69
|
+
`Using overridden Soroban RPC URL for network ${networkId}: ${rpcUrlFromOverride}`
|
|
70
|
+
);
|
|
71
|
+
if (isValidUrl(rpcUrlFromOverride)) {
|
|
72
|
+
return rpcUrlFromOverride;
|
|
73
|
+
} else {
|
|
74
|
+
logger.warn(
|
|
75
|
+
logSystem,
|
|
76
|
+
`Overridden Soroban RPC URL for ${networkId} is invalid: ${rpcUrlFromOverride}. Falling back.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Third priority: Fallback to the sorobanRpcUrl in the networkConfig
|
|
82
|
+
if (networkConfig.sorobanRpcUrl && isValidUrl(networkConfig.sorobanRpcUrl)) {
|
|
83
|
+
logger.debug(
|
|
84
|
+
logSystem,
|
|
85
|
+
`Using default Soroban RPC URL for network ${networkId}: ${networkConfig.sorobanRpcUrl}`
|
|
86
|
+
);
|
|
87
|
+
return networkConfig.sorobanRpcUrl;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger.error(
|
|
91
|
+
logSystem,
|
|
92
|
+
`No valid Soroban RPC URL could be resolved for network ${networkId}. Checked user config, override, and networkConfig.sorobanRpcUrl.`
|
|
93
|
+
);
|
|
94
|
+
throw new Error(
|
|
95
|
+
`No valid Soroban RPC URL configured for network ${networkConfig.name} (ID: ${networkId}).`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates an RPC endpoint configuration for Stellar networks.
|
|
101
|
+
* @param rpcConfig - The RPC provider configuration to validate
|
|
102
|
+
* @returns True if the configuration is valid, false otherwise
|
|
103
|
+
*/
|
|
104
|
+
export function validateStellarRpcEndpoint(rpcConfig: UserRpcProviderConfig): boolean {
|
|
105
|
+
try {
|
|
106
|
+
// Check if it's a valid URL (our validator already ensures HTTP/HTTPS)
|
|
107
|
+
if (!isValidUrl(rpcConfig.url)) {
|
|
108
|
+
logger.error('validateStellarRpcEndpoint', `Invalid RPC URL format: ${rpcConfig.url}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Additional Stellar-specific validation could be added here
|
|
113
|
+
// For example, checking if the URL follows known provider patterns
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error('validateStellarRpcEndpoint', 'Error validating RPC endpoint:', error);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tests the connection to a Stellar (Soroban) RPC endpoint with a timeout.
|
|
124
|
+
* Uses the Soroban RPC getHealth method to test connectivity.
|
|
125
|
+
* @param rpcConfig - The RPC provider configuration to test
|
|
126
|
+
* @param timeoutMs - Timeout in milliseconds (default: 5000ms)
|
|
127
|
+
* @returns Connection test results including success status, latency, and any errors
|
|
128
|
+
*/
|
|
129
|
+
export async function testStellarRpcConnection(
|
|
130
|
+
rpcConfig: UserRpcProviderConfig,
|
|
131
|
+
timeoutMs: number = 5000
|
|
132
|
+
): Promise<{
|
|
133
|
+
success: boolean;
|
|
134
|
+
latency?: number;
|
|
135
|
+
error?: string;
|
|
136
|
+
}> {
|
|
137
|
+
if (!rpcConfig.url) {
|
|
138
|
+
return { success: false, error: 'Soroban RPC URL is required' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create an AbortController for timeout
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
|
|
148
|
+
// Use fetch to make a JSON-RPC call to test the connection
|
|
149
|
+
// Using getHealth method which is standard for Soroban RPC endpoints
|
|
150
|
+
const response = await fetch(rpcConfig.url, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
jsonrpc: '2.0',
|
|
157
|
+
id: 1,
|
|
158
|
+
method: 'getHealth',
|
|
159
|
+
}),
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
return { success: false, error: `HTTP error: ${response.status}` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
const latency = Date.now() - startTime;
|
|
169
|
+
|
|
170
|
+
// Check for JSON-RPC error
|
|
171
|
+
if (data.error) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: `Soroban RPC error: ${data.error.message || 'Unknown RPC error'}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// For Soroban RPC getHealth, a successful response should contain result
|
|
179
|
+
if (!data.result) {
|
|
180
|
+
// Try fallback method - getLatestLedger
|
|
181
|
+
return await testWithFallbackMethod(rpcConfig, controller.signal, startTime);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if the health status indicates the service is healthy
|
|
185
|
+
const healthStatus = data.result.status;
|
|
186
|
+
if (healthStatus && healthStatus !== 'healthy') {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: `Soroban RPC service unhealthy: ${healthStatus}`,
|
|
190
|
+
latency,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { success: true, latency };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error('testStellarRpcConnection', 'Connection test failed:', error);
|
|
197
|
+
|
|
198
|
+
// Check if the error was due to timeout
|
|
199
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: `Connection timeout after ${timeoutMs}ms`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Try fallback method if primary test failed
|
|
207
|
+
try {
|
|
208
|
+
return await testWithFallbackMethod(rpcConfig, controller.signal, Date.now());
|
|
209
|
+
} catch {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
error: error instanceof Error ? error.message : 'Connection failed',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
// Clear the timeout
|
|
217
|
+
clearTimeout(timeoutId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fallback method to test Soroban RPC connection using getLatestLedger.
|
|
223
|
+
* This is used when getHealth is not available or fails.
|
|
224
|
+
*/
|
|
225
|
+
async function testWithFallbackMethod(
|
|
226
|
+
rpcConfig: UserRpcProviderConfig,
|
|
227
|
+
signal: AbortSignal,
|
|
228
|
+
startTime: number
|
|
229
|
+
): Promise<{
|
|
230
|
+
success: boolean;
|
|
231
|
+
latency?: number;
|
|
232
|
+
error?: string;
|
|
233
|
+
}> {
|
|
234
|
+
const response = await fetch(rpcConfig.url, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
},
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
jsonrpc: '2.0',
|
|
241
|
+
id: 1,
|
|
242
|
+
method: 'getLatestLedger',
|
|
243
|
+
}),
|
|
244
|
+
signal,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
return { success: false, error: `HTTP error: ${response.status}` };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const data = await response.json();
|
|
252
|
+
const latency = Date.now() - startTime;
|
|
253
|
+
|
|
254
|
+
if (data.error) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
error: `Soroban RPC error: ${data.error.message || 'Unknown RPC error'}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If we get a valid response with ledger info, the connection is working
|
|
262
|
+
if (data.result && data.result.sequence) {
|
|
263
|
+
return { success: true, latency };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: 'Unexpected response format from Soroban RPC endpoint',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as StellarSdk from '@stellar/stellar-sdk';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Comprehensive test to ensure we support ALL ScSpec types from Stellar SDK
|
|
6
|
+
* This test will fail if any new ScSpec types are added to the SDK that we don't handle
|
|
7
|
+
*/
|
|
8
|
+
describe('Complete ScSpec Type Coverage', () => {
|
|
9
|
+
// Get all ScSpec types from the current SDK version
|
|
10
|
+
const ALL_SCSPEC_TYPES = Object.getOwnPropertyNames(StellarSdk.xdr.ScSpecType)
|
|
11
|
+
.filter(
|
|
12
|
+
(name) =>
|
|
13
|
+
name.startsWith('scSpecType') && typeof StellarSdk.xdr.ScSpecType[name] === 'function'
|
|
14
|
+
)
|
|
15
|
+
.sort();
|
|
16
|
+
|
|
17
|
+
describe('All ScSpec types must be handled', () => {
|
|
18
|
+
ALL_SCSPEC_TYPES.forEach((scSpecTypeName) => {
|
|
19
|
+
it(`should handle ${scSpecTypeName}`, () => {
|
|
20
|
+
// Get all static methods from ScSpecType
|
|
21
|
+
const availableTypes = Object.getOwnPropertyNames(StellarSdk.xdr.ScSpecType).filter(
|
|
22
|
+
(name) =>
|
|
23
|
+
name.startsWith('scSpecType') && typeof StellarSdk.xdr.ScSpecType[name] === 'function'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Ensure our list is complete
|
|
27
|
+
expect(availableTypes).toContain(scSpecTypeName);
|
|
28
|
+
|
|
29
|
+
// This test will remind us to update the test when new types are added
|
|
30
|
+
console.log(`✓ ${scSpecTypeName} is accounted for`);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('Runtime type extraction coverage', () => {
|
|
36
|
+
it('should handle all primitive types without throwing', () => {
|
|
37
|
+
// We can't easily mock XDR objects, but we can test our type name mapping
|
|
38
|
+
// This ensures we have mappings for all expected type names
|
|
39
|
+
const expectedTypeNames = [
|
|
40
|
+
'Val',
|
|
41
|
+
'Bool',
|
|
42
|
+
'Void',
|
|
43
|
+
'Error',
|
|
44
|
+
'U32',
|
|
45
|
+
'I32',
|
|
46
|
+
'U64',
|
|
47
|
+
'I64',
|
|
48
|
+
'Timepoint',
|
|
49
|
+
'Duration',
|
|
50
|
+
'U128',
|
|
51
|
+
'I128',
|
|
52
|
+
'U256',
|
|
53
|
+
'I256',
|
|
54
|
+
'Bytes',
|
|
55
|
+
'ScString',
|
|
56
|
+
'ScSymbol',
|
|
57
|
+
'Address',
|
|
58
|
+
'MuxedAddress',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
expectedTypeNames.forEach((typeName) => {
|
|
62
|
+
// These should all have field type mappings or be handled by our type mapper
|
|
63
|
+
console.log(`Expected type: ${typeName}`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('SDK version change detection', () => {
|
|
69
|
+
it('should document all ScSpec types in current SDK version', () => {
|
|
70
|
+
// This test documents all the ScSpec types we're currently handling
|
|
71
|
+
console.log(`SDK has ${ALL_SCSPEC_TYPES.length} ScSpec types:`);
|
|
72
|
+
console.log(ALL_SCSPEC_TYPES.join(', '));
|
|
73
|
+
|
|
74
|
+
// Should have a reasonable number of types (at least 20+)
|
|
75
|
+
expect(ALL_SCSPEC_TYPES.length).toBeGreaterThanOrEqual(20);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|