@openzeppelin/ui-builder-adapter-stellar 0.16.0 → 1.1.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 +87 -0
- package/dist/index.cjs +6235 -3679
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6206 -3643
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/access-control/actions.ts +214 -0
- package/src/access-control/feature-detection.ts +226 -0
- package/src/access-control/index.ts +54 -0
- package/src/access-control/indexer-client.ts +1297 -0
- package/src/access-control/onchain-reader.ts +450 -0
- package/src/access-control/service.ts +1347 -0
- package/src/access-control/validation.ts +256 -0
- package/src/adapter.ts +21 -0
- package/src/configuration/network-services.ts +130 -9
- package/src/networks/mainnet.ts +1 -0
- package/src/networks/testnet.ts +1 -0
- package/src/query/handler.ts +6 -4
- package/src/sac/xdr.ts +1 -2
- package/src/transaction/eoa.ts +8 -1
- package/src/transaction/relayer.ts +8 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address Validation Module for Access Control
|
|
3
|
+
*
|
|
4
|
+
* Centralizes address validation for access control operations.
|
|
5
|
+
* Uses Stellar-specific validation from the validation module and
|
|
6
|
+
* shared normalization utilities from @openzeppelin/ui-builder-utils.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ConfigurationInvalid } from '@openzeppelin/ui-builder-types';
|
|
10
|
+
import { normalizeAddress } from '@openzeppelin/ui-builder-utils';
|
|
11
|
+
|
|
12
|
+
import { isValidAccountAddress, isValidContractAddress } from '../validation/address';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates a Stellar contract address
|
|
16
|
+
*
|
|
17
|
+
* @param address The contract address to validate
|
|
18
|
+
* @param paramName Optional parameter name for error messages (defaults to 'contractAddress')
|
|
19
|
+
* @throws ConfigurationInvalid if the address is invalid
|
|
20
|
+
*/
|
|
21
|
+
export function validateContractAddress(address: string, paramName = 'contractAddress'): void {
|
|
22
|
+
if (!address || typeof address !== 'string' || address.trim() === '') {
|
|
23
|
+
throw new ConfigurationInvalid(
|
|
24
|
+
`${paramName} is required and must be a non-empty string`,
|
|
25
|
+
address,
|
|
26
|
+
paramName
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!isValidContractAddress(address)) {
|
|
31
|
+
throw new ConfigurationInvalid(
|
|
32
|
+
`Invalid Stellar contract address: ${address}. Contract addresses must start with 'C' and be valid StrKey format.`,
|
|
33
|
+
address,
|
|
34
|
+
paramName
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validates a Stellar account address (for role grants, ownership transfers, etc.)
|
|
41
|
+
*
|
|
42
|
+
* @param address The account address to validate
|
|
43
|
+
* @param paramName Optional parameter name for error messages (defaults to 'account')
|
|
44
|
+
* @throws ConfigurationInvalid if the address is invalid
|
|
45
|
+
*/
|
|
46
|
+
export function validateAccountAddress(address: string, paramName = 'account'): void {
|
|
47
|
+
if (!address || typeof address !== 'string' || address.trim() === '') {
|
|
48
|
+
throw new ConfigurationInvalid(
|
|
49
|
+
`${paramName} is required and must be a non-empty string`,
|
|
50
|
+
address,
|
|
51
|
+
paramName
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isValidAccountAddress(address)) {
|
|
56
|
+
throw new ConfigurationInvalid(
|
|
57
|
+
`Invalid Stellar account address: ${address}. Account addresses must start with 'G' and be valid Ed25519 public keys.`,
|
|
58
|
+
address,
|
|
59
|
+
paramName
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validates a Stellar address that can be either an account or contract address
|
|
66
|
+
* (e.g., for ownership transfers where the new owner can be either type)
|
|
67
|
+
*
|
|
68
|
+
* @param address The address to validate (account or contract)
|
|
69
|
+
* @param paramName Optional parameter name for error messages (defaults to 'address')
|
|
70
|
+
* @throws ConfigurationInvalid if the address is invalid
|
|
71
|
+
*/
|
|
72
|
+
export function validateAddress(address: string, paramName = 'address'): void {
|
|
73
|
+
if (!address || typeof address !== 'string' || address.trim() === '') {
|
|
74
|
+
throw new ConfigurationInvalid(
|
|
75
|
+
`${paramName} is required and must be a non-empty string`,
|
|
76
|
+
address,
|
|
77
|
+
paramName
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if it's a valid account address OR contract address
|
|
82
|
+
if (!isValidAccountAddress(address) && !isValidContractAddress(address)) {
|
|
83
|
+
throw new ConfigurationInvalid(
|
|
84
|
+
`Invalid Stellar address: ${address}. Address must be a valid account address (starts with 'G') or contract address (starts with 'C').`,
|
|
85
|
+
address,
|
|
86
|
+
paramName
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validates both contract and account addresses for operations
|
|
93
|
+
*
|
|
94
|
+
* @param contractAddress The contract address
|
|
95
|
+
* @param accountAddress The account address
|
|
96
|
+
* @throws ConfigurationInvalid if either address is invalid
|
|
97
|
+
*/
|
|
98
|
+
export function validateAddresses(contractAddress: string, accountAddress: string): void {
|
|
99
|
+
validateContractAddress(contractAddress);
|
|
100
|
+
validateAccountAddress(accountAddress);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalizes a Stellar address using shared utils
|
|
105
|
+
* This is useful for case-insensitive and whitespace-insensitive comparison
|
|
106
|
+
*
|
|
107
|
+
* @param address The address to normalize
|
|
108
|
+
* @returns The normalized address
|
|
109
|
+
*/
|
|
110
|
+
export function normalizeStellarAddress(address: string): string {
|
|
111
|
+
return normalizeAddress(address);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Maximum length for a Soroban Symbol (role identifier)
|
|
116
|
+
* Soroban symbols are limited to 32 characters
|
|
117
|
+
*/
|
|
118
|
+
const MAX_ROLE_ID_LENGTH = 32;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Valid pattern for Soroban Symbol characters
|
|
122
|
+
* Symbols can contain alphanumeric characters and underscores
|
|
123
|
+
*/
|
|
124
|
+
const VALID_ROLE_ID_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates a role identifier (Soroban Symbol)
|
|
128
|
+
*
|
|
129
|
+
* Role IDs must be:
|
|
130
|
+
* - Non-empty strings
|
|
131
|
+
* - Max 32 characters (Soroban Symbol limit)
|
|
132
|
+
* - Start with a letter or underscore
|
|
133
|
+
* - Contain only alphanumeric characters and underscores
|
|
134
|
+
*
|
|
135
|
+
* @param roleId The role identifier to validate
|
|
136
|
+
* @param paramName Optional parameter name for error messages
|
|
137
|
+
* @throws ConfigurationInvalid if the role ID is invalid
|
|
138
|
+
*/
|
|
139
|
+
export function validateRoleId(roleId: string, paramName = 'roleId'): void {
|
|
140
|
+
if (!roleId || typeof roleId !== 'string') {
|
|
141
|
+
throw new ConfigurationInvalid(
|
|
142
|
+
`${paramName} is required and must be a non-empty string`,
|
|
143
|
+
roleId,
|
|
144
|
+
paramName
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const trimmed = roleId.trim();
|
|
149
|
+
if (trimmed === '') {
|
|
150
|
+
throw new ConfigurationInvalid(
|
|
151
|
+
`${paramName} cannot be empty or whitespace-only`,
|
|
152
|
+
roleId,
|
|
153
|
+
paramName
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (trimmed.length > MAX_ROLE_ID_LENGTH) {
|
|
158
|
+
throw new ConfigurationInvalid(
|
|
159
|
+
`${paramName} exceeds maximum length of ${MAX_ROLE_ID_LENGTH} characters: "${trimmed}" (${trimmed.length} chars)`,
|
|
160
|
+
roleId,
|
|
161
|
+
paramName
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!VALID_ROLE_ID_PATTERN.test(trimmed)) {
|
|
166
|
+
throw new ConfigurationInvalid(
|
|
167
|
+
`${paramName} contains invalid characters: "${trimmed}". Role IDs must start with a letter or underscore and contain only alphanumeric characters and underscores.`,
|
|
168
|
+
roleId,
|
|
169
|
+
paramName
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validates an array of role identifiers
|
|
176
|
+
*
|
|
177
|
+
* @param roleIds The array of role identifiers to validate
|
|
178
|
+
* @param paramName Optional parameter name for error messages
|
|
179
|
+
* @throws ConfigurationInvalid if any role ID is invalid or if the array is invalid
|
|
180
|
+
* @returns The validated and deduplicated array of role IDs
|
|
181
|
+
*/
|
|
182
|
+
export function validateRoleIds(roleIds: string[], paramName = 'roleIds'): string[] {
|
|
183
|
+
if (!Array.isArray(roleIds)) {
|
|
184
|
+
throw new ConfigurationInvalid(`${paramName} must be an array`, String(roleIds), paramName);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate each role ID
|
|
188
|
+
for (let i = 0; i < roleIds.length; i++) {
|
|
189
|
+
validateRoleId(roleIds[i], `${paramName}[${i}]`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Deduplicate and return
|
|
193
|
+
return [...new Set(roleIds.map((r) => r.trim()))];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Result of expiration ledger validation
|
|
198
|
+
*
|
|
199
|
+
* Returned by {@link validateExpirationLedger} to indicate whether
|
|
200
|
+
* a proposed expiration ledger is valid for a two-step ownership transfer.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const currentLedger = await getCurrentLedger(networkConfig);
|
|
205
|
+
* const result = validateExpirationLedger(expirationLedger, currentLedger);
|
|
206
|
+
* if (!result.valid) {
|
|
207
|
+
* throw new ConfigurationInvalid(result.error!, String(expirationLedger), 'expirationLedger');
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export interface ExpirationValidationResult {
|
|
212
|
+
/** Whether the expiration ledger is valid (must be strictly greater than current ledger) */
|
|
213
|
+
valid: boolean;
|
|
214
|
+
/** The current ledger sequence used for comparison */
|
|
215
|
+
currentLedger: number;
|
|
216
|
+
/** Human-readable error message if validation failed */
|
|
217
|
+
error?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validates an expiration ledger against the current ledger sequence
|
|
222
|
+
*
|
|
223
|
+
* For two-step Ownable contracts, the expiration ledger must be strictly greater
|
|
224
|
+
* than the current ledger (per FR-020: expirationLedger == currentLedger is invalid).
|
|
225
|
+
*
|
|
226
|
+
* @param expirationLedger The proposed expiration ledger sequence
|
|
227
|
+
* @param currentLedger The current ledger sequence number
|
|
228
|
+
* @returns Validation result with valid flag, currentLedger, and optional error message
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const currentLedger = await getCurrentLedger(networkConfig);
|
|
233
|
+
* const result = validateExpirationLedger(expirationLedger, currentLedger);
|
|
234
|
+
* if (!result.valid) {
|
|
235
|
+
* throw new ConfigurationInvalid(result.error, 'expirationLedger');
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function validateExpirationLedger(
|
|
240
|
+
expirationLedger: number,
|
|
241
|
+
currentLedger: number
|
|
242
|
+
): ExpirationValidationResult {
|
|
243
|
+
// Per FR-020: expirationLedger must be strictly greater than currentLedger
|
|
244
|
+
if (expirationLedger <= currentLedger) {
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
currentLedger,
|
|
248
|
+
error: `Expiration ledger ${expirationLedger} has already passed or equals current ledger. Current ledger is ${currentLedger}. Expiration must be strictly greater than current ledger.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
valid: true,
|
|
254
|
+
currentLedger,
|
|
255
|
+
};
|
|
256
|
+
}
|
package/src/adapter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
AccessControlService,
|
|
4
5
|
AvailableUiKit,
|
|
5
6
|
Connector,
|
|
6
7
|
ContractAdapter,
|
|
@@ -28,6 +29,8 @@ import type {
|
|
|
28
29
|
import { isStellarNetworkConfig } from '@openzeppelin/ui-builder-types';
|
|
29
30
|
import { logger } from '@openzeppelin/ui-builder-utils';
|
|
30
31
|
|
|
32
|
+
import { getCurrentLedger } from './access-control/onchain-reader';
|
|
33
|
+
import { createStellarAccessControlService } from './access-control/service';
|
|
31
34
|
import {
|
|
32
35
|
getStellarNetworkServiceForms,
|
|
33
36
|
testStellarNetworkServiceConnection,
|
|
@@ -83,6 +86,7 @@ import {
|
|
|
83
86
|
export class StellarAdapter implements ContractAdapter {
|
|
84
87
|
readonly networkConfig: StellarNetworkConfig;
|
|
85
88
|
readonly initialAppServiceKitName: UiKitConfiguration['kitName'];
|
|
89
|
+
private readonly accessControlService: AccessControlService;
|
|
86
90
|
|
|
87
91
|
constructor(networkConfig: StellarNetworkConfig) {
|
|
88
92
|
if (!isStellarNetworkConfig(networkConfig)) {
|
|
@@ -90,6 +94,9 @@ export class StellarAdapter implements ContractAdapter {
|
|
|
90
94
|
}
|
|
91
95
|
this.networkConfig = networkConfig;
|
|
92
96
|
|
|
97
|
+
// Initialize Access Control Service
|
|
98
|
+
this.accessControlService = createStellarAccessControlService(networkConfig);
|
|
99
|
+
|
|
93
100
|
// Set the network config in the wallet UI kit manager
|
|
94
101
|
stellarUiKitManager.setNetworkConfig(networkConfig);
|
|
95
102
|
|
|
@@ -418,6 +425,13 @@ export class StellarAdapter implements ContractAdapter {
|
|
|
418
425
|
return null;
|
|
419
426
|
}
|
|
420
427
|
|
|
428
|
+
/**
|
|
429
|
+
* @inheritdoc
|
|
430
|
+
*/
|
|
431
|
+
async getCurrentBlock(): Promise<number> {
|
|
432
|
+
return getCurrentLedger(this.networkConfig);
|
|
433
|
+
}
|
|
434
|
+
|
|
421
435
|
/**
|
|
422
436
|
* @inheritdoc
|
|
423
437
|
*/
|
|
@@ -615,6 +629,13 @@ export class StellarAdapter implements ContractAdapter {
|
|
|
615
629
|
lastTransaction: 'Last Transaction',
|
|
616
630
|
};
|
|
617
631
|
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* @inheritdoc
|
|
635
|
+
*/
|
|
636
|
+
public getAccessControlService(): AccessControlService {
|
|
637
|
+
return this.accessControlService;
|
|
638
|
+
}
|
|
618
639
|
}
|
|
619
640
|
|
|
620
641
|
// Also export as default to ensure compatibility with various import styles
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { NetworkServiceForm, UserRpcProviderConfig } from '@openzeppelin/ui-builder-types';
|
|
2
|
+
import { isValidUrl } from '@openzeppelin/ui-builder-utils';
|
|
2
3
|
|
|
3
4
|
import { testStellarRpcConnection, validateStellarRpcEndpoint } from './rpc';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Returns the network service forms for Stellar networks.
|
|
7
|
-
* Defines the UI configuration for the RPC
|
|
8
|
+
* Defines the UI configuration for the RPC and Indexer services.
|
|
9
|
+
*
|
|
10
|
+
* @param exclude Optional array of service IDs to exclude from the returned forms
|
|
11
|
+
* @returns Array of network service forms
|
|
8
12
|
*/
|
|
9
|
-
export function getStellarNetworkServiceForms(): NetworkServiceForm[] {
|
|
10
|
-
|
|
13
|
+
export function getStellarNetworkServiceForms(exclude: string[] = []): NetworkServiceForm[] {
|
|
14
|
+
const forms: NetworkServiceForm[] = [
|
|
11
15
|
{
|
|
12
16
|
id: 'rpc',
|
|
13
17
|
label: 'RPC Provider',
|
|
@@ -23,7 +27,37 @@ export function getStellarNetworkServiceForms(): NetworkServiceForm[] {
|
|
|
23
27
|
},
|
|
24
28
|
],
|
|
25
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'indexer',
|
|
32
|
+
label: 'Indexer',
|
|
33
|
+
description: 'Optional GraphQL indexer endpoint for historical access control data',
|
|
34
|
+
supportsConnectionTest: true,
|
|
35
|
+
fields: [
|
|
36
|
+
{
|
|
37
|
+
id: 'stellar-indexer-uri',
|
|
38
|
+
name: 'indexerUri',
|
|
39
|
+
type: 'text',
|
|
40
|
+
label: 'Indexer GraphQL HTTP Endpoint',
|
|
41
|
+
placeholder: 'https://indexer.example.com/graphql',
|
|
42
|
+
validation: { required: false, pattern: '^https?://.+' },
|
|
43
|
+
width: 'full',
|
|
44
|
+
helperText: 'Optional. Used for querying historical access control events.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'stellar-indexer-ws-uri',
|
|
48
|
+
name: 'indexerWsUri',
|
|
49
|
+
type: 'text',
|
|
50
|
+
label: 'Indexer GraphQL WebSocket Endpoint',
|
|
51
|
+
placeholder: 'wss://indexer.example.com/graphql',
|
|
52
|
+
validation: { required: false, pattern: '^wss?://.+' },
|
|
53
|
+
width: 'full',
|
|
54
|
+
helperText: 'Optional. Used for real-time subscriptions.',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
26
58
|
];
|
|
59
|
+
|
|
60
|
+
return forms.filter((form) => !exclude.includes(form.id));
|
|
27
61
|
}
|
|
28
62
|
|
|
29
63
|
/**
|
|
@@ -33,9 +67,37 @@ export async function validateStellarNetworkServiceConfig(
|
|
|
33
67
|
serviceId: string,
|
|
34
68
|
values: Record<string, unknown>
|
|
35
69
|
): Promise<boolean> {
|
|
36
|
-
if (serviceId
|
|
37
|
-
|
|
38
|
-
|
|
70
|
+
if (serviceId === 'rpc') {
|
|
71
|
+
const cfg = {
|
|
72
|
+
url: String(values.sorobanRpcUrl || ''),
|
|
73
|
+
isCustom: true,
|
|
74
|
+
} as UserRpcProviderConfig;
|
|
75
|
+
return validateStellarRpcEndpoint(cfg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (serviceId === 'indexer') {
|
|
79
|
+
// Validate indexerUri if provided
|
|
80
|
+
if (values.indexerUri !== undefined && values.indexerUri !== null && values.indexerUri !== '') {
|
|
81
|
+
if (!isValidUrl(String(values.indexerUri))) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate indexerWsUri if provided
|
|
87
|
+
if (
|
|
88
|
+
values.indexerWsUri !== undefined &&
|
|
89
|
+
values.indexerWsUri !== null &&
|
|
90
|
+
values.indexerWsUri !== ''
|
|
91
|
+
) {
|
|
92
|
+
if (!isValidUrl(String(values.indexerWsUri))) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
39
101
|
}
|
|
40
102
|
|
|
41
103
|
/**
|
|
@@ -45,7 +107,66 @@ export async function testStellarNetworkServiceConnection(
|
|
|
45
107
|
serviceId: string,
|
|
46
108
|
values: Record<string, unknown>
|
|
47
109
|
): Promise<{ success: boolean; latency?: number; error?: string }> {
|
|
48
|
-
if (serviceId
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
if (serviceId === 'rpc') {
|
|
111
|
+
const cfg = {
|
|
112
|
+
url: String(values.sorobanRpcUrl || ''),
|
|
113
|
+
isCustom: true,
|
|
114
|
+
} as UserRpcProviderConfig;
|
|
115
|
+
return testStellarRpcConnection(cfg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (serviceId === 'indexer') {
|
|
119
|
+
const indexerUri = values.indexerUri;
|
|
120
|
+
|
|
121
|
+
// If no indexer URI is provided, indexer is optional - return success (nothing to test)
|
|
122
|
+
if (!indexerUri || typeof indexerUri !== 'string' || indexerUri.trim() === '') {
|
|
123
|
+
return { success: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!isValidUrl(indexerUri)) {
|
|
127
|
+
return { success: false, error: 'Invalid indexer URI format' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
// Perform a simple GraphQL introspection query to test connectivity
|
|
133
|
+
const response = await fetch(indexerUri, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
query: '{ __typename }',
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const latency = Date.now() - startTime;
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
latency,
|
|
149
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
if (data.errors) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
latency,
|
|
158
|
+
error: `GraphQL errors: ${JSON.stringify(data.errors)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { success: true, latency };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { success: true };
|
|
51
172
|
}
|
package/src/networks/mainnet.ts
CHANGED
|
@@ -16,4 +16,5 @@ export const stellarPublic: StellarNetworkConfig = {
|
|
|
16
16
|
networkPassphrase: 'Public Global Stellar Network ; September 2015',
|
|
17
17
|
explorerUrl: 'https://stellar.expert/explorer/public',
|
|
18
18
|
iconComponent: NetworkStellar,
|
|
19
|
+
// indexerUri and indexerWsUri will be added here when stable mainnet indexer endpoints are available
|
|
19
20
|
};
|
package/src/networks/testnet.ts
CHANGED
|
@@ -16,4 +16,5 @@ export const stellarTestnet: StellarNetworkConfig = {
|
|
|
16
16
|
networkPassphrase: 'Test SDF Network ; September 2015',
|
|
17
17
|
explorerUrl: 'https://stellar.expert/explorer/testnet',
|
|
18
18
|
iconComponent: NetworkStellar,
|
|
19
|
+
// indexerUri and indexerWsUri will be added here when stable testnet indexer endpoints are available
|
|
19
20
|
};
|
package/src/query/handler.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
Address,
|
|
4
4
|
BASE_FEE,
|
|
5
5
|
Contract,
|
|
6
|
-
Keypair,
|
|
7
6
|
nativeToScVal,
|
|
8
7
|
rpc as StellarRpc,
|
|
9
8
|
TransactionBuilder,
|
|
@@ -61,9 +60,12 @@ async function createSimulationTransaction(
|
|
|
61
60
|
): Promise<TransactionBuilder> {
|
|
62
61
|
try {
|
|
63
62
|
// Create a dummy source account for simulation
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
// For simulations, we only need a valid public key - no private key required.
|
|
64
|
+
// Using a hardcoded test public key avoids Keypair.random() issues in test environments
|
|
65
|
+
// where @noble/curves crypto may not work correctly.
|
|
66
|
+
// This is a valid Stellar public key (the "zero" key derived from 32 zero bytes).
|
|
67
|
+
const SIMULATION_PUBLIC_KEY = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
|
|
68
|
+
const sourceAccount = new Account(SIMULATION_PUBLIC_KEY, '0');
|
|
67
69
|
|
|
68
70
|
// Create contract instance
|
|
69
71
|
const contract = new Contract(contractAddress);
|
package/src/sac/xdr.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
import stellarXdrJsonPackage from '@stellar/stellar-xdr-json/package.json' with { type: 'json' };
|
|
2
3
|
import { parse, stringify } from 'lossless-json';
|
|
3
4
|
|
|
4
|
-
import stellarXdrJsonPackage from '@stellar/stellar-xdr-json/package.json' assert { type: 'json' };
|
|
5
|
-
|
|
6
5
|
import { logger } from '@openzeppelin/ui-builder-utils';
|
|
7
6
|
|
|
8
7
|
/**
|
package/src/transaction/eoa.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from '@openzeppelin/ui-builder-types';
|
|
15
15
|
import { logger, userRpcConfigService } from '@openzeppelin/ui-builder-utils';
|
|
16
16
|
|
|
17
|
+
import { CALLER_PLACEHOLDER } from '../access-control/actions';
|
|
17
18
|
import { valueToScVal } from '../transform/input-parser';
|
|
18
19
|
import { getStellarWalletConnectionStatus, signTransaction } from '../wallet/connection';
|
|
19
20
|
import { ExecutionStrategy } from './execution-strategy';
|
|
@@ -93,8 +94,14 @@ export class EoaExecutionStrategy implements ExecutionStrategy {
|
|
|
93
94
|
networkPassphrase: stellarConfig.networkPassphrase,
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
// Replace CALLER_PLACEHOLDER with the connected wallet address
|
|
98
|
+
// This supports OpenZeppelin Stellar access control functions that require a caller parameter
|
|
99
|
+
const resolvedArgs = txData.args.map((arg) =>
|
|
100
|
+
arg === CALLER_PLACEHOLDER ? connectedAddress : arg
|
|
101
|
+
);
|
|
102
|
+
|
|
96
103
|
// Add the contract call operation (convert args to ScVal with comprehensive type support)
|
|
97
|
-
const scValArgs =
|
|
104
|
+
const scValArgs = resolvedArgs.map((arg, index) => {
|
|
98
105
|
const argType = txData.argTypes[index];
|
|
99
106
|
const argSchema = txData.argSchema?.[index]; // Pass schema for struct field type resolution
|
|
100
107
|
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
} from '@openzeppelin/ui-builder-types';
|
|
28
28
|
import { logger } from '@openzeppelin/ui-builder-utils';
|
|
29
29
|
|
|
30
|
+
import { CALLER_PLACEHOLDER } from '../access-control/actions';
|
|
30
31
|
import { valueToScVal } from '../transform/input-parser';
|
|
31
32
|
import { getStellarWalletConnectionStatus, signTransaction } from '../wallet/connection';
|
|
32
33
|
import { ExecutionStrategy } from './execution-strategy';
|
|
@@ -371,7 +372,13 @@ export class RelayerExecutionStrategy implements ExecutionStrategy {
|
|
|
371
372
|
networkPassphrase: stellarConfig.networkPassphrase,
|
|
372
373
|
});
|
|
373
374
|
|
|
374
|
-
|
|
375
|
+
// Replace CALLER_PLACEHOLDER with the connected wallet address
|
|
376
|
+
// This supports OpenZeppelin Stellar access control functions that require a caller parameter
|
|
377
|
+
const resolvedArgs = txData.args.map((arg) =>
|
|
378
|
+
arg === CALLER_PLACEHOLDER ? connectedAddress : arg
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const scValArgs = resolvedArgs.map((arg, index) => {
|
|
375
382
|
const argType = txData.argTypes[index];
|
|
376
383
|
const argSchema = txData.argSchema?.[index];
|
|
377
384
|
return valueToScVal(arg, argType, argSchema);
|