@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/dist/index.cjs +115 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +115 -55
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/getDefaultServiceConfig.test.ts +105 -0
- package/src/access-control/indexer-client.ts +134 -65
- package/src/adapter.ts +8 -0
- package/src/config.ts +2 -2
- package/src/configuration/network-services.ts +36 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openzeppelin/ui-builder-adapter-stellar",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
38
|
-
"@openzeppelin/ui-components": "^1.0
|
|
39
|
-
"@openzeppelin/ui-types": "^1.
|
|
40
|
-
"@openzeppelin/ui-utils": "^1.
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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.
|
|
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
|
-
|
|
718
|
-
|
|
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
|
|
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.
|
|
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.
|
|
747
|
-
pendingOwner: latestInitiation.
|
|
766
|
+
previousOwner: latestInitiation.previousOwner,
|
|
767
|
+
pendingOwner: latestInitiation.newOwner,
|
|
748
768
|
txHash: latestInitiation.txHash,
|
|
749
769
|
timestamp: latestInitiation.timestamp,
|
|
750
|
-
ledger: parseInt(latestInitiation.
|
|
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
|
-
|
|
895
|
-
if (!latestInitiation.admin) {
|
|
914
|
+
if (!latestInitiation.previousAdmin) {
|
|
896
915
|
logger.warn(
|
|
897
916
|
LOG_SYSTEM,
|
|
898
|
-
`Indexer returned ADMIN_TRANSFER_INITIATED event without
|
|
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.
|
|
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.
|
|
924
|
-
pendingAdmin: latestInitiation.
|
|
951
|
+
previousAdmin: latestInitiation.previousAdmin,
|
|
952
|
+
pendingAdmin: latestInitiation.newAdmin,
|
|
925
953
|
txHash: latestInitiation.txHash,
|
|
926
954
|
timestamp: latestInitiation.timestamp,
|
|
927
|
-
ledger: parseInt(latestInitiation.
|
|
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
|
-
|
|
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
|
-
|
|
972
|
-
|
|
994
|
+
previousOwner
|
|
995
|
+
newOwner
|
|
973
996
|
txHash
|
|
974
997
|
timestamp
|
|
975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1050
|
+
previousAdmin
|
|
1051
|
+
newAdmin
|
|
1036
1052
|
txHash
|
|
1037
1053
|
timestamp
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
-
? `,
|
|
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 ? ',
|
|
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
|
|
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 ? '$
|
|
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
|
-
|
|
1254
|
+
eventType
|
|
1239
1255
|
txHash
|
|
1240
1256
|
timestamp
|
|
1241
|
-
|
|
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
|
|
1280
|
-
variables.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1452
|
+
account: this.normalizeAccount(entry),
|
|
1384
1453
|
changeType,
|
|
1385
1454
|
txId: entry.txHash,
|
|
1386
1455
|
timestamp: entry.timestamp,
|
|
1387
|
-
ledger: parseInt(entry.
|
|
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
|
|
30
|
-
'@openzeppelin/relayer-sdk': '1.
|
|
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 {
|
|
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.
|