@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/ui-builder-adapter-stellar",
3
- "version": "0.16.0",
3
+ "version": "1.1.0",
4
4
  "description": "Stellar Adapter for UI Builder",
5
5
  "keywords": [
6
6
  "openzeppelin",
@@ -42,9 +42,9 @@
42
42
  "lossless-json": "^4.0.2",
43
43
  "lucide-react": "^0.510.0",
44
44
  "react-hook-form": "^7.62.0",
45
- "@openzeppelin/ui-builder-types": "0.16.0",
46
- "@openzeppelin/ui-builder-ui": "0.16.0",
47
- "@openzeppelin/ui-builder-utils": "^0.16.0"
45
+ "@openzeppelin/ui-builder-types": "1.0.0",
46
+ "@openzeppelin/ui-builder-ui": "1.1.0",
47
+ "@openzeppelin/ui-builder-utils": "^1.1.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/lodash": "^4.17.20",
@@ -79,6 +79,8 @@
79
79
  "lint:fix": "eslint . --fix",
80
80
  "lint:adapters": "node ../../lint-adapters.cjs",
81
81
  "test": "vitest run --passWithNoTests",
82
+ "test:unit": "vitest run --passWithNoTests --exclude '**/indexer-integration.test.ts'",
83
+ "test:integration": "vitest run --passWithNoTests test/access-control/indexer-integration.test.ts",
82
84
  "test:types": "vitest run src/contract/__tests__/complete-type-coverage.test.ts",
83
85
  "typecheck": "tsc --noEmit",
84
86
  "validate-stellar-types": "node scripts/check-stellar-sdk-types.js",
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Access Control Actions Module
3
+ *
4
+ * Assembles transaction data for access control operations (grant/revoke roles, transfer ownership)
5
+ * on Stellar (Soroban) contracts. These actions prepare transaction data that can be executed
6
+ * via the standard Stellar transaction execution flow.
7
+ */
8
+
9
+ import { logger } from '@openzeppelin/ui-builder-utils';
10
+
11
+ import type { StellarTransactionData } from '../transaction/formatter';
12
+
13
+ /**
14
+ * Special placeholder value that gets replaced with the connected wallet address
15
+ * at transaction execution time. Used when the caller parameter should be the
16
+ * transaction sender (connected wallet).
17
+ *
18
+ * @see EoaExecutionStrategy - replaces this placeholder before building the transaction
19
+ * @see RelayerExecutionStrategy - also replaces this placeholder before building the transaction
20
+ */
21
+ export const CALLER_PLACEHOLDER = '__CALLER__';
22
+
23
+ /**
24
+ * Assembles transaction data for granting a role to an account
25
+ *
26
+ * Note: OpenZeppelin Stellar AccessControl (v0.5.x) requires a `caller` parameter for authorization.
27
+ * The caller is the address authorizing the role grant (must have admin privileges).
28
+ * Use CALLER_PLACEHOLDER to use the connected wallet address as the caller.
29
+ *
30
+ * @param contractAddress The contract address
31
+ * @param roleId The role identifier (Symbol)
32
+ * @param account The account address to grant the role to
33
+ * @param caller The address authorizing the grant (defaults to CALLER_PLACEHOLDER for connected wallet)
34
+ * @returns Transaction data ready for execution
35
+ */
36
+ export function assembleGrantRoleAction(
37
+ contractAddress: string,
38
+ roleId: string,
39
+ account: string,
40
+ caller: string = CALLER_PLACEHOLDER
41
+ ): StellarTransactionData {
42
+ logger.info(
43
+ 'assembleGrantRoleAction',
44
+ `Assembling grant_role action for ${roleId} to ${account} (caller: ${caller})`
45
+ );
46
+
47
+ // Arguments for grant_role(caller: Address, account: Address, role: Symbol) - v0.5.x signature
48
+ // Note: args are raw values that will be converted to ScVal by the transaction execution flow
49
+ // The caller parameter is required by OpenZeppelin Stellar AccessControl for authorization
50
+ return {
51
+ contractAddress,
52
+ functionName: 'grant_role',
53
+ args: [caller, account, roleId],
54
+ argTypes: ['Address', 'Address', 'Symbol'],
55
+ argSchema: undefined,
56
+ transactionOptions: {},
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Assembles transaction data for revoking a role from an account
62
+ *
63
+ * Note: OpenZeppelin Stellar AccessControl (v0.5.x) requires a `caller` parameter for authorization.
64
+ * The caller is the address authorizing the role revocation (must have admin privileges).
65
+ * Use CALLER_PLACEHOLDER to use the connected wallet address as the caller.
66
+ *
67
+ * @param contractAddress The contract address
68
+ * @param roleId The role identifier (Symbol)
69
+ * @param account The account address to revoke the role from
70
+ * @param caller The address authorizing the revocation (defaults to CALLER_PLACEHOLDER for connected wallet)
71
+ * @returns Transaction data ready for execution
72
+ */
73
+ export function assembleRevokeRoleAction(
74
+ contractAddress: string,
75
+ roleId: string,
76
+ account: string,
77
+ caller: string = CALLER_PLACEHOLDER
78
+ ): StellarTransactionData {
79
+ logger.info(
80
+ 'assembleRevokeRoleAction',
81
+ `Assembling revoke_role action for ${roleId} from ${account} (caller: ${caller})`
82
+ );
83
+
84
+ // Arguments for revoke_role(caller: Address, account: Address, role: Symbol) - v0.5.x signature
85
+ // Note: args are raw values that will be converted to ScVal by the transaction execution flow
86
+ // The caller parameter is required by OpenZeppelin Stellar AccessControl for authorization
87
+ return {
88
+ contractAddress,
89
+ functionName: 'revoke_role',
90
+ args: [caller, account, roleId],
91
+ argTypes: ['Address', 'Address', 'Symbol'],
92
+ argSchema: undefined,
93
+ transactionOptions: {},
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Assembles transaction data for transferring ownership of a contract
99
+ *
100
+ * For two-step Ownable contracts, this initiates a transfer that must be accepted
101
+ * by the pending owner before the expiration ledger.
102
+ *
103
+ * @param contractAddress The contract address
104
+ * @param newOwner The new owner address
105
+ * @param liveUntilLedger The ledger sequence by which the transfer must be accepted
106
+ * @returns Transaction data ready for execution
107
+ */
108
+ export function assembleTransferOwnershipAction(
109
+ contractAddress: string,
110
+ newOwner: string,
111
+ liveUntilLedger: number
112
+ ): StellarTransactionData {
113
+ logger.info(
114
+ 'assembleTransferOwnershipAction',
115
+ `Assembling transfer_ownership action to ${newOwner} with expiration at ledger ${liveUntilLedger}`
116
+ );
117
+
118
+ // Arguments for transfer_ownership(new_owner: Address, live_until_ledger: u32)
119
+ // Note: args are raw values that will be converted to ScVal by the transaction execution flow
120
+ return {
121
+ contractAddress,
122
+ functionName: 'transfer_ownership',
123
+ args: [newOwner, liveUntilLedger],
124
+ argTypes: ['Address', 'u32'],
125
+ argSchema: undefined,
126
+ transactionOptions: {},
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Assembles transaction data for accepting a pending ownership transfer
132
+ *
133
+ * For two-step Ownable contracts, this completes a pending transfer initiated by
134
+ * the current owner. Must be called by the pending owner before the expiration ledger.
135
+ *
136
+ * @param contractAddress The contract address
137
+ * @returns Transaction data ready for execution
138
+ */
139
+ export function assembleAcceptOwnershipAction(contractAddress: string): StellarTransactionData {
140
+ logger.info(
141
+ 'assembleAcceptOwnershipAction',
142
+ `Assembling accept_ownership action for ${contractAddress}`
143
+ );
144
+
145
+ // accept_ownership() has no arguments - caller must be the pending owner
146
+ return {
147
+ contractAddress,
148
+ functionName: 'accept_ownership',
149
+ args: [],
150
+ argTypes: [],
151
+ argSchema: undefined,
152
+ transactionOptions: {},
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Assembles transaction data for initiating an admin role transfer
158
+ *
159
+ * For two-step AccessControl contracts, this initiates an admin transfer that must
160
+ * be accepted by the pending admin before the expiration ledger.
161
+ *
162
+ * @param contractAddress The contract address
163
+ * @param newAdmin The new admin address
164
+ * @param liveUntilLedger The ledger sequence by which the transfer must be accepted
165
+ * @returns Transaction data ready for execution
166
+ */
167
+ export function assembleTransferAdminRoleAction(
168
+ contractAddress: string,
169
+ newAdmin: string,
170
+ liveUntilLedger: number
171
+ ): StellarTransactionData {
172
+ logger.info(
173
+ 'assembleTransferAdminRoleAction',
174
+ `Assembling transfer_admin_role action to ${newAdmin} with expiration at ledger ${liveUntilLedger}`
175
+ );
176
+
177
+ // Arguments for transfer_admin_role(new_admin: Address, live_until_ledger: u32)
178
+ // Note: args are raw values that will be converted to ScVal by the transaction execution flow
179
+ return {
180
+ contractAddress,
181
+ functionName: 'transfer_admin_role',
182
+ args: [newAdmin, liveUntilLedger],
183
+ argTypes: ['Address', 'u32'],
184
+ argSchema: undefined,
185
+ transactionOptions: {},
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Assembles transaction data for accepting a pending admin transfer
191
+ *
192
+ * For two-step AccessControl contracts, this completes a pending admin transfer
193
+ * initiated by the current admin. Must be called by the pending admin before the
194
+ * expiration ledger.
195
+ *
196
+ * @param contractAddress The contract address
197
+ * @returns Transaction data ready for execution
198
+ */
199
+ export function assembleAcceptAdminTransferAction(contractAddress: string): StellarTransactionData {
200
+ logger.info(
201
+ 'assembleAcceptAdminTransferAction',
202
+ `Assembling accept_admin_transfer action for ${contractAddress}`
203
+ );
204
+
205
+ // accept_admin_transfer() has no arguments - caller must be the pending admin
206
+ return {
207
+ contractAddress,
208
+ functionName: 'accept_admin_transfer',
209
+ args: [],
210
+ argTypes: [],
211
+ argSchema: undefined,
212
+ transactionOptions: {},
213
+ };
214
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Feature Detection Module
3
+ *
4
+ * Detects access control capabilities of Stellar (Soroban) contracts by analyzing
5
+ * their function interfaces. Supports detection of Ownable and AccessControl patterns
6
+ * from OpenZeppelin Stellar Contracts.
7
+ */
8
+
9
+ import type { AccessControlCapabilities, ContractSchema } from '@openzeppelin/ui-builder-types';
10
+ import { UnsupportedContractFeatures } from '@openzeppelin/ui-builder-types';
11
+
12
+ /**
13
+ * Known OpenZeppelin Ownable function signatures
14
+ */
15
+ const OWNABLE_FUNCTIONS = {
16
+ required: ['get_owner'],
17
+ optional: ['transfer_ownership', 'accept_ownership', 'renounce_ownership'],
18
+ } as const;
19
+
20
+ /**
21
+ * Known OpenZeppelin AccessControl function signatures
22
+ */
23
+ const ACCESS_CONTROL_FUNCTIONS = {
24
+ required: ['has_role', 'grant_role', 'revoke_role'],
25
+ optional: [
26
+ 'get_role_admin',
27
+ 'set_role_admin',
28
+ 'get_admin',
29
+ 'transfer_admin_role',
30
+ 'accept_admin_transfer',
31
+ 'renounce_admin',
32
+ 'renounce_role',
33
+ ],
34
+ } as const;
35
+
36
+ /**
37
+ * Functions that indicate role enumeration support
38
+ */
39
+ const ENUMERATION_FUNCTIONS = ['get_role_member_count', 'get_role_member'] as const;
40
+
41
+ /**
42
+ * Detects access control capabilities of a Stellar contract
43
+ *
44
+ * @param contractSchema The contract schema to analyze
45
+ * @param indexerAvailable Whether an indexer is configured and available
46
+ * @returns Detected capabilities
47
+ */
48
+ export function detectAccessControlCapabilities(
49
+ contractSchema: ContractSchema,
50
+ indexerAvailable = false
51
+ ): AccessControlCapabilities {
52
+ const functionNames = new Set(contractSchema.functions.map((fn) => fn.name));
53
+
54
+ // Detect Ownable
55
+ const hasOwnable = OWNABLE_FUNCTIONS.required.every((fnName) => functionNames.has(fnName));
56
+
57
+ // Detect two-step Ownable (has accept_ownership function)
58
+ const hasTwoStepOwnable = hasOwnable && functionNames.has('accept_ownership');
59
+
60
+ // Detect AccessControl
61
+ const hasAccessControl = ACCESS_CONTROL_FUNCTIONS.required.every((fnName) =>
62
+ functionNames.has(fnName)
63
+ );
64
+
65
+ // Detect two-step admin transfer (has accept_admin_transfer function)
66
+ const hasTwoStepAdmin = hasAccessControl && functionNames.has('accept_admin_transfer');
67
+
68
+ // Detect enumerable roles
69
+ const hasEnumerableRoles = ENUMERATION_FUNCTIONS.every((fnName) => functionNames.has(fnName));
70
+
71
+ // History is only available when indexer is configured
72
+ const supportsHistory = indexerAvailable;
73
+
74
+ // Verify the contract against OZ interfaces
75
+ const verifiedAgainstOZInterfaces = verifyOZInterface(
76
+ functionNames,
77
+ hasOwnable,
78
+ hasAccessControl,
79
+ hasTwoStepOwnable,
80
+ hasTwoStepAdmin
81
+ );
82
+
83
+ // Collect notes about detected capabilities
84
+ const notes: string[] = [];
85
+
86
+ if (hasOwnable) {
87
+ if (hasTwoStepOwnable) {
88
+ notes.push('OpenZeppelin two-step Ownable interface detected (with accept_ownership)');
89
+ } else {
90
+ notes.push('OpenZeppelin Ownable interface detected');
91
+ }
92
+ }
93
+
94
+ if (hasAccessControl) {
95
+ if (hasTwoStepAdmin) {
96
+ notes.push(
97
+ 'OpenZeppelin two-step AccessControl interface detected (with accept_admin_transfer)'
98
+ );
99
+ } else {
100
+ notes.push('OpenZeppelin AccessControl interface detected');
101
+ }
102
+ }
103
+
104
+ if (hasEnumerableRoles) {
105
+ notes.push('Role enumeration supported (get_role_member_count, get_role_member)');
106
+ } else if (hasAccessControl) {
107
+ notes.push('Role enumeration not available - requires event reconstruction');
108
+ }
109
+
110
+ if (!indexerAvailable && (hasOwnable || hasAccessControl)) {
111
+ notes.push('History queries unavailable without indexer configuration');
112
+ }
113
+
114
+ if (!hasOwnable && !hasAccessControl) {
115
+ notes.push('No OpenZeppelin access control interfaces detected');
116
+ }
117
+
118
+ return {
119
+ hasOwnable,
120
+ hasTwoStepOwnable,
121
+ hasAccessControl,
122
+ hasTwoStepAdmin,
123
+ hasEnumerableRoles,
124
+ supportsHistory,
125
+ verifiedAgainstOZInterfaces,
126
+ notes: notes.length > 0 ? notes : undefined,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Verifies that the contract conforms to OpenZeppelin interfaces
132
+ *
133
+ * @param functionNames Set of function names in the contract
134
+ * @param hasOwnable Whether Ownable was detected
135
+ * @param hasAccessControl Whether AccessControl was detected
136
+ * @param hasTwoStepOwnable Whether two-step Ownable was detected
137
+ * @param hasTwoStepAdmin Whether two-step admin was detected
138
+ * @returns True if verified against OZ interfaces
139
+ */
140
+ function verifyOZInterface(
141
+ functionNames: Set<string>,
142
+ hasOwnable: boolean,
143
+ hasAccessControl: boolean,
144
+ hasTwoStepOwnable = false,
145
+ hasTwoStepAdmin = false
146
+ ): boolean {
147
+ // If no OZ patterns detected, not applicable
148
+ if (!hasOwnable && !hasAccessControl) {
149
+ return false;
150
+ }
151
+
152
+ // Verify Ownable optional functions
153
+ // For two-step Ownable, require at least 3 of 4 optional functions
154
+ // For basic Ownable, at least 2 of 3 (excluding accept_ownership)
155
+ if (hasOwnable) {
156
+ const ownableOptionalCount = OWNABLE_FUNCTIONS.optional.filter((fnName) =>
157
+ functionNames.has(fnName)
158
+ ).length;
159
+
160
+ if (hasTwoStepOwnable) {
161
+ // Two-step Ownable should have at least 3 of 4 optional functions
162
+ // (transfer_ownership, accept_ownership, renounce_ownership, and accept_ownership is guaranteed)
163
+ if (ownableOptionalCount < 3) {
164
+ return false;
165
+ }
166
+ } else {
167
+ // Basic Ownable should have at least 2 of 3 optional functions
168
+ if (ownableOptionalCount < 2) {
169
+ return false;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Verify AccessControl optional functions
175
+ // For two-step admin, require at least 5 of 7 optional functions
176
+ // For basic AccessControl, at least 4 of 7 should be present
177
+ if (hasAccessControl) {
178
+ const accessControlOptionalCount = ACCESS_CONTROL_FUNCTIONS.optional.filter((fnName) =>
179
+ functionNames.has(fnName)
180
+ ).length;
181
+
182
+ if (hasTwoStepAdmin) {
183
+ // Two-step admin should have at least 5 of 7 optional functions
184
+ // (transfer_admin_role, accept_admin_transfer guaranteed, plus others)
185
+ if (accessControlOptionalCount < 5) {
186
+ return false;
187
+ }
188
+ } else {
189
+ // Basic AccessControl should have at least 4 of 7 optional functions
190
+ if (accessControlOptionalCount < 4) {
191
+ return false;
192
+ }
193
+ }
194
+ }
195
+
196
+ return true;
197
+ }
198
+
199
+ /**
200
+ * Validates that a contract supports access control operations
201
+ *
202
+ * @param capabilities The detected capabilities
203
+ * @param contractAddress Optional contract address for error context
204
+ * @throws UnsupportedContractFeatures if contract doesn't support required operations
205
+ */
206
+ export function validateAccessControlSupport(
207
+ capabilities: AccessControlCapabilities,
208
+ contractAddress?: string
209
+ ): asserts capabilities is
210
+ | (AccessControlCapabilities & { hasOwnable: true })
211
+ | (AccessControlCapabilities & { hasAccessControl: true }) {
212
+ if (!capabilities.hasOwnable && !capabilities.hasAccessControl) {
213
+ throw new UnsupportedContractFeatures(
214
+ 'Contract does not implement OpenZeppelin Ownable or AccessControl interfaces',
215
+ contractAddress,
216
+ ['Ownable', 'AccessControl']
217
+ );
218
+ }
219
+
220
+ if (!capabilities.verifiedAgainstOZInterfaces) {
221
+ throw new UnsupportedContractFeatures(
222
+ 'Contract interfaces do not conform to OpenZeppelin standards',
223
+ contractAddress
224
+ );
225
+ }
226
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Access Control Module
3
+ *
4
+ * Exports access control functionality for Stellar (Soroban) contracts including
5
+ * capability detection, on-chain data reading, action assembly, validation, indexer client,
6
+ * and the AccessControlService.
7
+ *
8
+ * ## Two-Step Ownable Support
9
+ *
10
+ * This module provides full support for OpenZeppelin's two-step Ownable pattern:
11
+ * - {@link StellarAccessControlService.getOwnership} - Returns ownership state (owned/pending/expired/renounced)
12
+ * - {@link StellarAccessControlService.transferOwnership} - Initiates two-step transfer with expiration
13
+ * - {@link StellarAccessControlService.acceptOwnership} - Accepts pending ownership transfer
14
+ * - {@link getCurrentLedger} - Gets current ledger sequence for expiration calculation
15
+ * - {@link validateExpirationLedger} - Validates expiration ledger before submission
16
+ *
17
+ * ## Two-Step Admin Transfer Support
18
+ *
19
+ * This module provides full support for OpenZeppelin's two-step admin transfer pattern:
20
+ * - {@link StellarAccessControlService.getAdminInfo} - Returns admin state (active/pending/expired/renounced)
21
+ * - {@link StellarAccessControlService.transferAdminRole} - Initiates two-step admin transfer with expiration
22
+ * - {@link StellarAccessControlService.acceptAdminTransfer} - Accepts pending admin transfer
23
+ * - {@link AdminTransferInitiatedEvent} - Pending admin transfer event from indexer
24
+ *
25
+ * ## Action Assembly
26
+ *
27
+ * - {@link assembleGrantRoleAction} - Prepares grant_role transaction
28
+ * - {@link assembleRevokeRoleAction} - Prepares revoke_role transaction
29
+ * - {@link assembleTransferOwnershipAction} - Prepares transfer_ownership transaction with expiration
30
+ * - {@link assembleAcceptOwnershipAction} - Prepares accept_ownership transaction
31
+ * - {@link assembleTransferAdminRoleAction} - Prepares transfer_admin_role transaction with expiration
32
+ * - {@link assembleAcceptAdminTransferAction} - Prepares accept_admin_transfer transaction
33
+ *
34
+ * ## Feature Detection
35
+ *
36
+ * - {@link detectAccessControlCapabilities} - Detects Ownable/AccessControl support
37
+ * - `hasTwoStepOwnable` capability flag indicates two-step ownership transfer support
38
+ * - `hasTwoStepAdmin` capability flag indicates two-step admin transfer support
39
+ *
40
+ * ## Indexer Client
41
+ *
42
+ * - {@link StellarIndexerClient} - Queries historical events and pending transfers
43
+ * - {@link OwnershipTransferStartedEvent} - Pending ownership transfer event from indexer
44
+ * - {@link AdminTransferInitiatedEvent} - Pending admin transfer event from indexer
45
+ *
46
+ * @module access-control
47
+ */
48
+
49
+ export * from './actions';
50
+ export * from './feature-detection';
51
+ export * from './indexer-client';
52
+ export * from './onchain-reader';
53
+ export * from './service';
54
+ export * from './validation';