@openzeppelin/ui-builder-adapter-stellar 1.3.0 → 1.5.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": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Stellar Adapter for UI Builder",
5
5
  "keywords": [
6
6
  "openzeppelin",
@@ -34,10 +34,10 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@creit.tech/stellar-wallets-kit": "^1.8.0",
37
- "@openzeppelin/relayer-sdk": "1.4.0",
38
- "@openzeppelin/ui-components": "^1.0.4",
39
- "@openzeppelin/ui-types": "^1.2.0",
40
- "@openzeppelin/ui-utils": "^1.1.0",
37
+ "@openzeppelin/relayer-sdk": "1.9.0",
38
+ "@openzeppelin/ui-components": "^1.2.0",
39
+ "@openzeppelin/ui-types": "^1.6.0",
40
+ "@openzeppelin/ui-utils": "^1.2.0",
41
41
  "@stellar/stellar-sdk": "^14.1.1",
42
42
  "@stellar/stellar-xdr-json": "^23.0.0",
43
43
  "@web3icons/react": "^4.0.19",
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { StellarNetworkConfig } from '@openzeppelin/ui-types';
4
+
5
+ import { getStellarDefaultServiceConfig } from '../configuration/network-services';
6
+
7
+ /**
8
+ * Tests for getStellarDefaultServiceConfig function.
9
+ *
10
+ * Note: We test the pure function directly instead of instantiating StellarAdapter
11
+ * to avoid loading heavy Stellar SDK dependencies (WASM modules, etc.) which can
12
+ * cause memory issues during test execution.
13
+ */
14
+ describe('getStellarDefaultServiceConfig', () => {
15
+ const createMockNetworkConfig = (
16
+ overrides: Partial<StellarNetworkConfig> = {}
17
+ ): StellarNetworkConfig =>
18
+ ({
19
+ id: 'stellar-testnet',
20
+ exportConstName: 'stellarTestnet',
21
+ name: 'Stellar Testnet',
22
+ ecosystem: 'stellar',
23
+ network: 'testnet',
24
+ type: 'testnet',
25
+ isTestnet: true,
26
+ sorobanRpcUrl: 'https://soroban-testnet.stellar.org',
27
+ networkPassphrase: 'Test SDF Network ; September 2015',
28
+ ...overrides,
29
+ }) as StellarNetworkConfig;
30
+
31
+ describe('rpc service', () => {
32
+ it('should return RPC config when sorobanRpcUrl is present', () => {
33
+ const networkConfig = createMockNetworkConfig();
34
+
35
+ const result = getStellarDefaultServiceConfig(networkConfig, 'rpc');
36
+
37
+ expect(result).toEqual({
38
+ sorobanRpcUrl: 'https://soroban-testnet.stellar.org',
39
+ });
40
+ });
41
+
42
+ it('should return null when sorobanRpcUrl is missing', () => {
43
+ const networkConfig = createMockNetworkConfig({
44
+ sorobanRpcUrl: undefined,
45
+ });
46
+
47
+ const result = getStellarDefaultServiceConfig(networkConfig, 'rpc');
48
+
49
+ expect(result).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('indexer service', () => {
54
+ it('should return indexer config when both URLs are present', () => {
55
+ const networkConfig = createMockNetworkConfig({
56
+ indexerUri: 'https://indexer.stellar.example/graphql',
57
+ indexerWsUri: 'wss://indexer.stellar.example/graphql',
58
+ });
59
+
60
+ const result = getStellarDefaultServiceConfig(networkConfig, 'indexer');
61
+
62
+ expect(result).toEqual({
63
+ indexerUri: 'https://indexer.stellar.example/graphql',
64
+ indexerWsUri: 'wss://indexer.stellar.example/graphql',
65
+ });
66
+ });
67
+
68
+ it('should return null when indexerUri is missing', () => {
69
+ const networkConfig = createMockNetworkConfig({
70
+ indexerWsUri: 'wss://indexer.stellar.example/graphql',
71
+ });
72
+
73
+ const result = getStellarDefaultServiceConfig(networkConfig, 'indexer');
74
+
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it('should return null when indexerWsUri is missing', () => {
79
+ const networkConfig = createMockNetworkConfig({
80
+ indexerUri: 'https://indexer.stellar.example/graphql',
81
+ });
82
+
83
+ const result = getStellarDefaultServiceConfig(networkConfig, 'indexer');
84
+
85
+ expect(result).toBeNull();
86
+ });
87
+
88
+ it('should return null when neither indexer URL is present', () => {
89
+ const networkConfig = createMockNetworkConfig();
90
+
91
+ const result = getStellarDefaultServiceConfig(networkConfig, 'indexer');
92
+
93
+ expect(result).toBeNull();
94
+ });
95
+ });
96
+
97
+ describe('unknown service', () => {
98
+ it('should return null for unknown service IDs', () => {
99
+ const networkConfig = createMockNetworkConfig();
100
+
101
+ expect(getStellarDefaultServiceConfig(networkConfig, 'explorer')).toBeNull();
102
+ expect(getStellarDefaultServiceConfig(networkConfig, 'unknown')).toBeNull();
103
+ });
104
+ });
105
+ });
@@ -80,21 +80,31 @@ function getUserIndexerEndpoints(networkId: string): IndexerEndpointConfig | und
80
80
  interface IndexerHistoryEntry {
81
81
  id: string;
82
82
  role?: string; // Nullable for Ownership/Admin events
83
- account: string;
84
- type:
83
+ /** Account address - required for role events */
84
+ account?: string;
85
+ /** Event type */
86
+ eventType:
85
87
  | 'ROLE_GRANTED'
86
88
  | 'ROLE_REVOKED'
89
+ | 'ROLE_ADMIN_CHANGED'
87
90
  | 'OWNERSHIP_TRANSFER_COMPLETED'
88
91
  | 'OWNERSHIP_TRANSFER_STARTED'
92
+ | 'OWNERSHIP_RENOUNCED'
89
93
  | 'ADMIN_TRANSFER_INITIATED'
90
- | 'ADMIN_TRANSFER_COMPLETED';
94
+ | 'ADMIN_TRANSFER_COMPLETED'
95
+ | 'ADMIN_RENOUNCED';
91
96
  txHash: string;
92
97
  timestamp: string;
93
- blockHeight: string;
94
- /** Ledger sequence of the event */
95
- ledger?: string;
96
- /** Admin/owner who initiated the transfer (for OWNERSHIP_TRANSFER_STARTED, ADMIN_TRANSFER_INITIATED) */
97
- admin?: string;
98
+ /** Block/ledger number */
99
+ blockNumber: string;
100
+ /** Previous owner (for OWNERSHIP_TRANSFER_STARTED, OWNERSHIP_TRANSFER_COMPLETED) */
101
+ previousOwner?: string;
102
+ /** New owner (for OWNERSHIP_TRANSFER_STARTED, OWNERSHIP_TRANSFER_COMPLETED, OWNERSHIP_RENOUNCED) */
103
+ newOwner?: string;
104
+ /** Previous admin (for ADMIN_TRANSFER_INITIATED, ADMIN_TRANSFER_COMPLETED) */
105
+ previousAdmin?: string;
106
+ /** New admin (for ADMIN_TRANSFER_INITIATED, ADMIN_TRANSFER_COMPLETED, ADMIN_RENOUNCED) */
107
+ newAdmin?: string;
98
108
  /** Expiration ledger for pending transfers (OWNERSHIP_TRANSFER_STARTED, ADMIN_TRANSFER_INITIATED) */
99
109
  liveUntilLedger?: number;
100
110
  }
@@ -565,11 +575,13 @@ export class StellarIndexerClient {
565
575
  // Since we order by TIMESTAMP_DESC, we take the first occurrence per account
566
576
  const grantMap = new Map<string, GrantInfo>();
567
577
  for (const entry of result.data.accessControlEvents.nodes) {
568
- if (!grantMap.has(entry.account)) {
569
- grantMap.set(entry.account, {
578
+ // For role grants, account is always present
579
+ const account = entry.account || '';
580
+ if (account && !grantMap.has(account)) {
581
+ grantMap.set(account, {
570
582
  timestamp: entry.timestamp,
571
583
  txId: entry.txHash,
572
- ledger: parseInt(entry.blockHeight, 10),
584
+ ledger: parseInt(entry.blockNumber, 10),
573
585
  });
574
586
  }
575
587
  }
@@ -714,11 +726,19 @@ export class StellarIndexerClient {
714
726
  }
715
727
 
716
728
  // No completion - validate required fields before returning pending transfer info
717
- // The admin field is required for OWNERSHIP_TRANSFER_STARTED events
718
- if (!latestInitiation.admin) {
729
+ if (!latestInitiation.previousOwner) {
730
+ logger.warn(
731
+ LOG_SYSTEM,
732
+ `Indexer returned OWNERSHIP_TRANSFER_STARTED event without previousOwner field for ${contractAddress}. ` +
733
+ `This indicates incomplete indexer data. Treating as no valid pending transfer.`
734
+ );
735
+ return null;
736
+ }
737
+
738
+ if (!latestInitiation.newOwner) {
719
739
  logger.warn(
720
740
  LOG_SYSTEM,
721
- `Indexer returned OWNERSHIP_TRANSFER_STARTED event without admin field for ${contractAddress}. ` +
741
+ `Indexer returned OWNERSHIP_TRANSFER_STARTED event without newOwner field for ${contractAddress}. ` +
722
742
  `This indicates incomplete indexer data. Treating as no valid pending transfer.`
723
743
  );
724
744
  return null;
@@ -739,15 +759,15 @@ export class StellarIndexerClient {
739
759
 
740
760
  logger.info(
741
761
  LOG_SYSTEM,
742
- `Found pending ownership transfer for ${contractAddress}: pending owner=${latestInitiation.account}, expires at ledger ${latestInitiation.liveUntilLedger}`
762
+ `Found pending ownership transfer for ${contractAddress}: pending owner=${latestInitiation.newOwner}, expires at ledger ${latestInitiation.liveUntilLedger}`
743
763
  );
744
764
 
745
765
  return {
746
- previousOwner: latestInitiation.admin,
747
- pendingOwner: latestInitiation.account,
766
+ previousOwner: latestInitiation.previousOwner,
767
+ pendingOwner: latestInitiation.newOwner,
748
768
  txHash: latestInitiation.txHash,
749
769
  timestamp: latestInitiation.timestamp,
750
- ledger: parseInt(latestInitiation.ledger || latestInitiation.blockHeight, 10),
770
+ ledger: parseInt(latestInitiation.blockNumber, 10),
751
771
  liveUntilLedger: latestInitiation.liveUntilLedger,
752
772
  };
753
773
  } catch (error) {
@@ -891,11 +911,19 @@ export class StellarIndexerClient {
891
911
  }
892
912
 
893
913
  // No completion - validate required fields before returning pending transfer info
894
- // The admin field is required for ADMIN_TRANSFER_INITIATED events
895
- if (!latestInitiation.admin) {
914
+ if (!latestInitiation.previousAdmin) {
896
915
  logger.warn(
897
916
  LOG_SYSTEM,
898
- `Indexer returned ADMIN_TRANSFER_INITIATED event without admin field for ${contractAddress}. ` +
917
+ `Indexer returned ADMIN_TRANSFER_INITIATED event without previousAdmin field for ${contractAddress}. ` +
918
+ `This indicates incomplete indexer data. Treating as no valid pending transfer.`
919
+ );
920
+ return null;
921
+ }
922
+
923
+ if (!latestInitiation.newAdmin) {
924
+ logger.warn(
925
+ LOG_SYSTEM,
926
+ `Indexer returned ADMIN_TRANSFER_INITIATED event without newAdmin field for ${contractAddress}. ` +
899
927
  `This indicates incomplete indexer data. Treating as no valid pending transfer.`
900
928
  );
901
929
  return null;
@@ -916,15 +944,15 @@ export class StellarIndexerClient {
916
944
 
917
945
  logger.info(
918
946
  LOG_SYSTEM,
919
- `Found pending admin transfer for ${contractAddress}: pending admin=${latestInitiation.account}, expires at ledger ${latestInitiation.liveUntilLedger}`
947
+ `Found pending admin transfer for ${contractAddress}: pending admin=${latestInitiation.newAdmin}, expires at ledger ${latestInitiation.liveUntilLedger}`
920
948
  );
921
949
 
922
950
  return {
923
- previousAdmin: latestInitiation.admin,
924
- pendingAdmin: latestInitiation.account,
951
+ previousAdmin: latestInitiation.previousAdmin,
952
+ pendingAdmin: latestInitiation.newAdmin,
925
953
  txHash: latestInitiation.txHash,
926
954
  timestamp: latestInitiation.timestamp,
927
- ledger: parseInt(latestInitiation.ledger || latestInitiation.blockHeight, 10),
955
+ ledger: parseInt(latestInitiation.blockNumber, 10),
928
956
  liveUntilLedger: latestInitiation.liveUntilLedger,
929
957
  };
930
958
  } catch (error) {
@@ -949,11 +977,6 @@ export class StellarIndexerClient {
949
977
  * Note: The OpenZeppelin Stellar contract emits `ownership_transfer` event
950
978
  * which is indexed as `OWNERSHIP_TRANSFER_STARTED`.
951
979
  *
952
- * Schema mapping:
953
- * - `account`: pending new owner
954
- * - `admin`: current owner who initiated the transfer
955
- * - `ledger`: block height of the event
956
- * - `liveUntilLedger`: expiration ledger for the pending transfer
957
980
  */
958
981
  private buildOwnershipTransferStartedQuery(): string {
959
982
  return `
@@ -961,19 +984,18 @@ export class StellarIndexerClient {
961
984
  accessControlEvents(
962
985
  filter: {
963
986
  contract: { equalTo: $contract }
964
- type: { equalTo: OWNERSHIP_TRANSFER_STARTED }
987
+ eventType: { equalTo: OWNERSHIP_TRANSFER_STARTED }
965
988
  }
966
989
  orderBy: TIMESTAMP_DESC
967
990
  first: 1
968
991
  ) {
969
992
  nodes {
970
993
  id
971
- account
972
- admin
994
+ previousOwner
995
+ newOwner
973
996
  txHash
974
997
  timestamp
975
- ledger
976
- blockHeight
998
+ blockNumber
977
999
  liveUntilLedger
978
1000
  }
979
1001
  }
@@ -990,7 +1012,7 @@ export class StellarIndexerClient {
990
1012
  accessControlEvents(
991
1013
  filter: {
992
1014
  contract: { equalTo: $contract }
993
- type: { equalTo: OWNERSHIP_TRANSFER_COMPLETED }
1015
+ eventType: { equalTo: OWNERSHIP_TRANSFER_COMPLETED }
994
1016
  timestamp: { greaterThan: $afterTimestamp }
995
1017
  }
996
1018
  orderBy: TIMESTAMP_DESC
@@ -1011,12 +1033,6 @@ export class StellarIndexerClient {
1011
1033
  *
1012
1034
  * Note: The OpenZeppelin Stellar contract emits `admin_transfer_initiated` event
1013
1035
  * which is indexed as `ADMIN_TRANSFER_INITIATED`.
1014
- *
1015
- * Schema mapping:
1016
- * - `account`: pending new admin
1017
- * - `admin`: current admin who initiated the transfer
1018
- * - `ledger`: block height of the event
1019
- * - `liveUntilLedger`: expiration ledger for the pending transfer
1020
1036
  */
1021
1037
  private buildAdminTransferInitiatedQuery(): string {
1022
1038
  return `
@@ -1024,19 +1040,18 @@ export class StellarIndexerClient {
1024
1040
  accessControlEvents(
1025
1041
  filter: {
1026
1042
  contract: { equalTo: $contract }
1027
- type: { equalTo: ADMIN_TRANSFER_INITIATED }
1043
+ eventType: { equalTo: ADMIN_TRANSFER_INITIATED }
1028
1044
  }
1029
1045
  orderBy: TIMESTAMP_DESC
1030
1046
  first: 1
1031
1047
  ) {
1032
1048
  nodes {
1033
1049
  id
1034
- account
1035
- admin
1050
+ previousAdmin
1051
+ newAdmin
1036
1052
  txHash
1037
1053
  timestamp
1038
- ledger
1039
- blockHeight
1054
+ blockNumber
1040
1055
  liveUntilLedger
1041
1056
  }
1042
1057
  }
@@ -1053,7 +1068,7 @@ export class StellarIndexerClient {
1053
1068
  accessControlEvents(
1054
1069
  filter: {
1055
1070
  contract: { equalTo: $contract }
1056
- type: { equalTo: ADMIN_TRANSFER_COMPLETED }
1071
+ eventType: { equalTo: ADMIN_TRANSFER_COMPLETED }
1057
1072
  timestamp: { greaterThan: $afterTimestamp }
1058
1073
  }
1059
1074
  orderBy: TIMESTAMP_DESC
@@ -1166,17 +1181,18 @@ export class StellarIndexerClient {
1166
1181
 
1167
1182
  /**
1168
1183
  * Maps internal changeType to GraphQL EventType enum
1169
- * GraphQL enum values: ROLE_GRANTED, ROLE_REVOKED, OWNERSHIP_TRANSFER_STARTED,
1170
- * OWNERSHIP_TRANSFER_COMPLETED, ADMIN_TRANSFER_INITIATED, ADMIN_TRANSFER_COMPLETED
1171
1184
  */
1172
1185
  private mapChangeTypeToGraphQLEnum(changeType: HistoryChangeType): string {
1173
1186
  const mapping: Record<HistoryChangeType, string> = {
1174
1187
  GRANTED: 'ROLE_GRANTED',
1175
1188
  REVOKED: 'ROLE_REVOKED',
1189
+ ROLE_ADMIN_CHANGED: 'ROLE_ADMIN_CHANGED',
1176
1190
  OWNERSHIP_TRANSFER_STARTED: 'OWNERSHIP_TRANSFER_STARTED',
1177
1191
  OWNERSHIP_TRANSFER_COMPLETED: 'OWNERSHIP_TRANSFER_COMPLETED',
1192
+ OWNERSHIP_RENOUNCED: 'OWNERSHIP_RENOUNCED',
1178
1193
  ADMIN_TRANSFER_INITIATED: 'ADMIN_TRANSFER_INITIATED',
1179
1194
  ADMIN_TRANSFER_COMPLETED: 'ADMIN_TRANSFER_COMPLETED',
1195
+ ADMIN_RENOUNCED: 'ADMIN_RENOUNCED',
1180
1196
  UNKNOWN: 'UNKNOWN',
1181
1197
  };
1182
1198
  return mapping[changeType];
@@ -1190,7 +1206,7 @@ export class StellarIndexerClient {
1190
1206
  const accountFilter = options?.account ? ', account: { equalTo: $account }' : '';
1191
1207
  // Type filter uses inline enum value (consistent with buildLatestGrantsQuery pattern)
1192
1208
  const typeFilter = options?.changeType
1193
- ? `, type: { equalTo: ${this.mapChangeTypeToGraphQLEnum(options.changeType)} }`
1209
+ ? `, eventType: { equalTo: ${this.mapChangeTypeToGraphQLEnum(options.changeType)} }`
1194
1210
  : '';
1195
1211
  const txFilter = options?.txId ? ', txHash: { equalTo: $txHash }' : '';
1196
1212
  // Build combined timestamp filter to avoid duplicate keys
@@ -1203,12 +1219,12 @@ export class StellarIndexerClient {
1203
1219
  }
1204
1220
  const timestampFilter =
1205
1221
  timestampConditions.length > 0 ? `, timestamp: { ${timestampConditions.join(', ')} }` : '';
1206
- const ledgerFilter = options?.ledger ? ', blockHeight: { equalTo: $blockHeight }' : '';
1222
+ const ledgerFilter = options?.ledger ? ', blockNumber: { equalTo: $blockNumber }' : '';
1207
1223
  const limitClause = options?.limit ? ', first: $limit' : '';
1208
1224
  const cursorClause = options?.cursor ? ', after: $cursor' : '';
1209
1225
 
1210
1226
  // Build variable declarations
1211
- // Note: SubQuery uses Datetime for timestamp filters and BigFloat for blockHeight filtering
1227
+ // Note: SubQuery uses Datetime for timestamp filters and BigFloat for blockNumber filtering
1212
1228
  const varDeclarations = [
1213
1229
  '$contract: String!',
1214
1230
  options?.roleId ? '$role: String' : '',
@@ -1216,7 +1232,7 @@ export class StellarIndexerClient {
1216
1232
  options?.txId ? '$txHash: String' : '',
1217
1233
  options?.timestampFrom ? '$timestampFrom: Datetime' : '',
1218
1234
  options?.timestampTo ? '$timestampTo: Datetime' : '',
1219
- options?.ledger ? '$blockHeight: BigFloat' : '',
1235
+ options?.ledger ? '$blockNumber: BigFloat' : '',
1220
1236
  options?.limit ? '$limit: Int' : '',
1221
1237
  options?.cursor ? '$cursor: Cursor' : '',
1222
1238
  ]
@@ -1235,10 +1251,14 @@ export class StellarIndexerClient {
1235
1251
  id
1236
1252
  role
1237
1253
  account
1238
- type
1254
+ eventType
1239
1255
  txHash
1240
1256
  timestamp
1241
- blockHeight
1257
+ blockNumber
1258
+ previousOwner
1259
+ newOwner
1260
+ previousAdmin
1261
+ newAdmin
1242
1262
  }
1243
1263
  pageInfo {
1244
1264
  hasNextPage
@@ -1276,8 +1296,8 @@ export class StellarIndexerClient {
1276
1296
  variables.timestampTo = options.timestampTo;
1277
1297
  }
1278
1298
  if (options?.ledger) {
1279
- // GraphQL expects blockHeight as string
1280
- variables.blockHeight = String(options.ledger);
1299
+ // GraphQL expects blockNumber as string
1300
+ variables.blockNumber = String(options.ledger);
1281
1301
  }
1282
1302
  if (options?.limit) {
1283
1303
  variables.limit = options.limit;
@@ -1300,7 +1320,7 @@ export class StellarIndexerClient {
1300
1320
  accessControlEvents(
1301
1321
  filter: {
1302
1322
  contract: { equalTo: $contract }
1303
- type: { in: [ROLE_GRANTED, ROLE_REVOKED] }
1323
+ eventType: { in: [ROLE_GRANTED, ROLE_REVOKED] }
1304
1324
  }
1305
1325
  ) {
1306
1326
  nodes {
@@ -1324,7 +1344,7 @@ export class StellarIndexerClient {
1324
1344
  contract: { equalTo: $contract }
1325
1345
  role: { equalTo: $role }
1326
1346
  account: { in: $accounts }
1327
- type: { equalTo: ROLE_GRANTED }
1347
+ eventType: { equalTo: ROLE_GRANTED }
1328
1348
  }
1329
1349
  orderBy: TIMESTAMP_DESC
1330
1350
  ) {
@@ -1332,13 +1352,53 @@ export class StellarIndexerClient {
1332
1352
  account
1333
1353
  txHash
1334
1354
  timestamp
1335
- blockHeight
1355
+ blockNumber
1336
1356
  }
1337
1357
  }
1338
1358
  }
1339
1359
  `;
1340
1360
  }
1341
1361
 
1362
+ /**
1363
+ * Normalize account from indexer entry
1364
+ *
1365
+ * Multi-chain schema uses different fields for different event types:
1366
+ * - Role events: `account` field
1367
+ * - Ownership events: `newOwner` field (pending/new owner)
1368
+ * - Admin events: `newAdmin` field (pending/new admin)
1369
+ */
1370
+ private normalizeAccount(entry: IndexerHistoryEntry): string {
1371
+ // For role events, use account directly
1372
+ if (
1373
+ entry.eventType === 'ROLE_GRANTED' ||
1374
+ entry.eventType === 'ROLE_REVOKED' ||
1375
+ entry.eventType === 'ROLE_ADMIN_CHANGED'
1376
+ ) {
1377
+ return entry.account || '';
1378
+ }
1379
+
1380
+ // For ownership events, use newOwner
1381
+ if (
1382
+ entry.eventType === 'OWNERSHIP_TRANSFER_STARTED' ||
1383
+ entry.eventType === 'OWNERSHIP_TRANSFER_COMPLETED' ||
1384
+ entry.eventType === 'OWNERSHIP_RENOUNCED'
1385
+ ) {
1386
+ return entry.newOwner || '';
1387
+ }
1388
+
1389
+ // For admin events, use newAdmin
1390
+ if (
1391
+ entry.eventType === 'ADMIN_TRANSFER_INITIATED' ||
1392
+ entry.eventType === 'ADMIN_TRANSFER_COMPLETED' ||
1393
+ entry.eventType === 'ADMIN_RENOUNCED'
1394
+ ) {
1395
+ return entry.newAdmin || '';
1396
+ }
1397
+
1398
+ // Fallback for unknown event types
1399
+ return entry.account || '';
1400
+ }
1401
+
1342
1402
  /**
1343
1403
  * Transform indexer entries to standard HistoryEntry format
1344
1404
  */
@@ -1350,41 +1410,50 @@ export class StellarIndexerClient {
1350
1410
 
1351
1411
  // Map SubQuery event types to internal types
1352
1412
  let changeType: HistoryChangeType;
1353
- switch (entry.type) {
1413
+ switch (entry.eventType) {
1354
1414
  case 'ROLE_GRANTED':
1355
1415
  changeType = 'GRANTED';
1356
1416
  break;
1357
1417
  case 'ROLE_REVOKED':
1358
1418
  changeType = 'REVOKED';
1359
1419
  break;
1420
+ case 'ROLE_ADMIN_CHANGED':
1421
+ changeType = 'ROLE_ADMIN_CHANGED';
1422
+ break;
1360
1423
  case 'OWNERSHIP_TRANSFER_STARTED':
1361
1424
  changeType = 'OWNERSHIP_TRANSFER_STARTED';
1362
1425
  break;
1363
1426
  case 'OWNERSHIP_TRANSFER_COMPLETED':
1364
1427
  changeType = 'OWNERSHIP_TRANSFER_COMPLETED';
1365
1428
  break;
1429
+ case 'OWNERSHIP_RENOUNCED':
1430
+ changeType = 'OWNERSHIP_RENOUNCED';
1431
+ break;
1366
1432
  case 'ADMIN_TRANSFER_INITIATED':
1367
1433
  changeType = 'ADMIN_TRANSFER_INITIATED';
1368
1434
  break;
1369
1435
  case 'ADMIN_TRANSFER_COMPLETED':
1370
1436
  changeType = 'ADMIN_TRANSFER_COMPLETED';
1371
1437
  break;
1438
+ case 'ADMIN_RENOUNCED':
1439
+ changeType = 'ADMIN_RENOUNCED';
1440
+ break;
1372
1441
  default:
1373
1442
  // Use UNKNOWN for unrecognized types to make indexer schema issues visible
1374
1443
  logger.warn(
1375
1444
  LOG_SYSTEM,
1376
- `Unknown event type: ${entry.type}, assigning changeType to UNKNOWN`
1445
+ `Unknown event type: ${entry.eventType}, assigning changeType to UNKNOWN`
1377
1446
  );
1378
1447
  changeType = 'UNKNOWN';
1379
1448
  }
1380
1449
 
1381
1450
  return {
1382
1451
  role,
1383
- account: entry.account,
1452
+ account: this.normalizeAccount(entry),
1384
1453
  changeType,
1385
1454
  txId: entry.txHash,
1386
1455
  timestamp: entry.timestamp,
1387
- ledger: parseInt(entry.blockHeight, 10),
1456
+ ledger: parseInt(entry.blockNumber, 10),
1388
1457
  };
1389
1458
  });
1390
1459
  }
package/src/adapter.ts CHANGED
@@ -33,6 +33,7 @@ import { logger } from '@openzeppelin/ui-utils';
33
33
  import { getCurrentLedger } from './access-control/onchain-reader';
34
34
  import { createStellarAccessControlService } from './access-control/service';
35
35
  import {
36
+ getStellarDefaultServiceConfig,
36
37
  getStellarNetworkServiceForms,
37
38
  testStellarNetworkServiceConnection,
38
39
  validateStellarNetworkServiceConfig,
@@ -157,6 +158,13 @@ export class StellarAdapter implements ContractAdapter {
157
158
  return testStellarNetworkServiceConnection(serviceId, values);
158
159
  }
159
160
 
161
+ /**
162
+ * @inheritdoc
163
+ */
164
+ public getDefaultServiceConfig(serviceId: string): Record<string, unknown> | null {
165
+ return getStellarDefaultServiceConfig(this.networkConfig, serviceId);
166
+ }
167
+
160
168
  /**
161
169
  * NOTE about artifact inputs (single input with auto-detection):
162
170
  *
package/src/config.ts CHANGED
@@ -26,8 +26,8 @@ export const stellarAdapterConfig: AdapterConfig = {
26
26
  // Wallet connection and integration
27
27
  '@creit.tech/stellar-wallets-kit': '^1.9.5',
28
28
 
29
- // OpenZeppelin Relayer integration (optional, for gasless transactions)
30
- '@openzeppelin/relayer-sdk': '1.1.0',
29
+ // OpenZeppelin Relayer integration for gasless transactions
30
+ '@openzeppelin/relayer-sdk': '1.9.0',
31
31
 
32
32
  // React integration for wallet components
33
33
  react: '^19.0.0',
@@ -1,8 +1,43 @@
1
- import type { NetworkServiceForm, UserRpcProviderConfig } from '@openzeppelin/ui-types';
1
+ import type {
2
+ NetworkServiceForm,
3
+ StellarNetworkConfig,
4
+ UserRpcProviderConfig,
5
+ } from '@openzeppelin/ui-types';
2
6
  import { isValidUrl } from '@openzeppelin/ui-utils';
3
7
 
4
8
  import { testStellarRpcConnection, validateStellarRpcEndpoint } from './rpc';
5
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', '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 'indexer':
29
+ // 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
+
6
41
  /**
7
42
  * Returns the network service forms for Stellar networks.
8
43
  * Defines the UI configuration for the RPC and Indexer services.