@lazy-sol/access-control 1.0.6 → 1.1.1
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/CHANGELOG.md +43 -0
- package/README.md +69 -48
- package/artifacts/contracts/OwnableToAccessControlAdapter.sol/OwnableToAccessControlAdapter.json +7 -64
- package/contracts/AccessControl.sol +15 -219
- package/contracts/AccessControlCore.sol +353 -0
- package/contracts/AdapterFactory.sol +1 -1
- package/contracts/OwnableToAccessControlAdapter.sol +7 -5
- package/contracts/mocks/AccessControlMock.sol +7 -1
- package/hardhat.config.js +6 -6
- package/index.js +4 -0
- package/package.json +12 -10
- package/test/include/deployment_routines.js +3 -12
- package/test/include/rbac.behaviour.js +22 -7
- package/test/ownable_to_rbac_adapter.js +1 -1
- package/test/ownable_to_rbac_adapter_rbac.js +3 -3
- package/test/rbac_modifier.js +1 -1
- package/ui.html +506 -0
package/ui.html
ADDED
@@ -0,0 +1,506 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Role-based Access Control (RBAC) Inspector</title>
|
6
|
+
<!-- Include Web3.js from a CDN -->
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
|
8
|
+
<style>
|
9
|
+
table#roles_list {
|
10
|
+
width: 100%;
|
11
|
+
border-collapse: collapse;
|
12
|
+
}
|
13
|
+
table#roles_list th, table#roles_list td {
|
14
|
+
border: 1px solid grey;
|
15
|
+
}
|
16
|
+
table#roles_list tr td {
|
17
|
+
font-family: monospace;
|
18
|
+
}
|
19
|
+
</style>
|
20
|
+
</head>
|
21
|
+
<body>
|
22
|
+
<fieldset id="root_container" style="display: none;"><legend id="connection_state" style="text-align: right;"></legend>
|
23
|
+
<div id="main_app" style="display: none;">
|
24
|
+
<fieldset><legend>Role-based Access Control (RBAC) Features and Roles Inspector</legend>
|
25
|
+
<form onsubmit="inspect_features_roles(event)">
|
26
|
+
<table>
|
27
|
+
<tr>
|
28
|
+
<td><label for="contract_address">Contract Address:</label></td>
|
29
|
+
<td><input id="contract_address" type="text" required size="42" style="font-family: monospace;" value="0xA9f50518f9119d5F7A324EC76481c5246970dEa6"/></td>
|
30
|
+
</tr>
|
31
|
+
<tr><td></td><td><input type="submit" value="Inspect"/></td></tr>
|
32
|
+
</table>
|
33
|
+
</form>
|
34
|
+
</fieldset>
|
35
|
+
<div id="features_roles_container" style="display: none;">
|
36
|
+
<fieldset>
|
37
|
+
<legend id="features_title">RBAC Features</legend>
|
38
|
+
<div id="features_hex">Loading...</div>
|
39
|
+
</fieldset>
|
40
|
+
<fieldset>
|
41
|
+
<legend id="roles_list_title">RBAC Operators and Roles</legend>
|
42
|
+
<table id="roles_list"><tr><td>Loading...</td></tr></table>
|
43
|
+
</fieldset>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</fieldset>
|
47
|
+
<div style="position: fixed; left: 0; bottom: 0; margin: 0.1em 0.2em;">© 2024 <a href="https://github.com/lazy-sol/">Lazy So[u]l</a></div>
|
48
|
+
<div style="position: fixed; right: 0; bottom: 0; margin: 0.1em 0.2em; font-family: monospace;">
|
49
|
+
[<a href="https://lazy-sol.github.io/advanced-erc20/ui.html">Advanced ERC20</a>]
|
50
|
+
[<a href="https://lazy-sol.github.io/tiny-erc721/ui.html">Tiny ERC721</a>]
|
51
|
+
[RBAC Inspector]
|
52
|
+
</div>
|
53
|
+
</body>
|
54
|
+
<script type="text/javascript">
|
55
|
+
// verify if MetaMask is installed
|
56
|
+
if(!window.ethereum) {
|
57
|
+
if(window.confirm("MetaMask is not installed. You will be redirected to the MetaMask installation page.")) {
|
58
|
+
window.setTimeout(function() {
|
59
|
+
window.location.href = "https://metamask.io/download/";
|
60
|
+
}, 0);
|
61
|
+
}
|
62
|
+
window.stop();
|
63
|
+
}
|
64
|
+
</script>
|
65
|
+
|
66
|
+
<script type="text/javascript">
|
67
|
+
// static HTML page anchors
|
68
|
+
const root_container = document.getElementById("root_container");
|
69
|
+
const connection_state = document.getElementById("connection_state");
|
70
|
+
const main_app = document.getElementById("main_app");
|
71
|
+
const contract_address = document.getElementById("contract_address");
|
72
|
+
const features_roles_container = document.getElementById("features_roles_container");
|
73
|
+
const features_title = document.getElementById("features_title");
|
74
|
+
const features_hex = document.getElementById("features_hex");
|
75
|
+
const roles_list_title = document.getElementById("roles_list_title");
|
76
|
+
const roles_list = document.getElementById("roles_list");
|
77
|
+
</script>
|
78
|
+
|
79
|
+
<script type="text/javascript">
|
80
|
+
// auxiliary pure functions
|
81
|
+
|
82
|
+
// auxiliary function to log and display non-fatal error, doesn't stop execution
|
83
|
+
function non_fatal_error(message, error) {
|
84
|
+
console.error("%s: %o", message, error);
|
85
|
+
window.alert(message + (error? ": " + json_stringify(error): ""));
|
86
|
+
}
|
87
|
+
|
88
|
+
// auxiliary function to log and display fatal error and stop execution
|
89
|
+
function fatal_error(message, error) {
|
90
|
+
non_fatal_error("FATAL: " + message, error);
|
91
|
+
window.stop();
|
92
|
+
throw error? error: "error";
|
93
|
+
}
|
94
|
+
|
95
|
+
// save JSON.stringify works properly with BigInt
|
96
|
+
function json_stringify(input) {
|
97
|
+
return JSON.stringify(input, (key, value) =>
|
98
|
+
typeof value === 'bigint' ? value.toString() : value
|
99
|
+
);
|
100
|
+
}
|
101
|
+
</script>
|
102
|
+
|
103
|
+
<script type="text/javascript">
|
104
|
+
// config and app state
|
105
|
+
|
106
|
+
// config stores settings which don't change
|
107
|
+
const CONF = {
|
108
|
+
// supported networks, where our contracts are known to be deployed
|
109
|
+
chains: {
|
110
|
+
1: {
|
111
|
+
name: "Mainnet",
|
112
|
+
block_explorer: {
|
113
|
+
tx: "https://etherscan.io/tx/",
|
114
|
+
address: "https://etherscan.io/address/",
|
115
|
+
token: "https://etherscan.io/token/",
|
116
|
+
},
|
117
|
+
},
|
118
|
+
11155111: {
|
119
|
+
name: "Sepolia",
|
120
|
+
block_explorer: {
|
121
|
+
tx: "https://sepolia.etherscan.io/tx/",
|
122
|
+
address: "https://sepolia.etherscan.io/address/",
|
123
|
+
token: "https://sepolia.etherscan.io/token/",
|
124
|
+
},
|
125
|
+
},
|
126
|
+
8453: {
|
127
|
+
name: "Base",
|
128
|
+
block_explorer: {
|
129
|
+
tx: "https://basescan.org/tx/",
|
130
|
+
address: "https://basescan.org/address/",
|
131
|
+
token: "https://basescan.org/token/",
|
132
|
+
},
|
133
|
+
},
|
134
|
+
84532: {
|
135
|
+
name: "Base Sepolia",
|
136
|
+
block_explorer: {
|
137
|
+
tx: "https://sepolia.basescan.org//tx/",
|
138
|
+
address: "https://sepolia.basescan.org/address/",
|
139
|
+
token: "https://sepolia.basescan.org/token/",
|
140
|
+
},
|
141
|
+
},
|
142
|
+
},
|
143
|
+
};
|
144
|
+
|
145
|
+
// state stores settings which may change due to user actions,
|
146
|
+
// due to the async nature of the app, many incoming events, it makes sense to follow
|
147
|
+
// the state of the app by listening to all the events and updating the state
|
148
|
+
const STATE = {
|
149
|
+
// currently connected network (recognized decimal Chain ID)
|
150
|
+
chain_id: undefined,
|
151
|
+
// currently connected account (accounts[0])
|
152
|
+
A0: undefined,
|
153
|
+
// currently connected chain config
|
154
|
+
chain: function() {
|
155
|
+
return this.chain_id? CONF.chains[this.chain_id]: undefined;
|
156
|
+
},
|
157
|
+
// function to display the chain we're connected to
|
158
|
+
update_chain_id: function(chain_id) {
|
159
|
+
this.chain_id = chain_id? parseInt(chain_id): undefined;
|
160
|
+
},
|
161
|
+
// function to update currently connected account
|
162
|
+
update_A0: function(accounts) {
|
163
|
+
// MetaMask is locked or not connected
|
164
|
+
if(!accounts || !accounts.length) {
|
165
|
+
this.A0 = undefined;
|
166
|
+
}
|
167
|
+
else {
|
168
|
+
[this.A0] = accounts;
|
169
|
+
}
|
170
|
+
},
|
171
|
+
// function to refresh the connection state UI
|
172
|
+
refresh_ui: function() {
|
173
|
+
if(CONF.chains[this.chain_id]) {
|
174
|
+
if(this.A0) {
|
175
|
+
connection_state.innerHTML = `Connected <span style="font-family: monospace;">${this.A0}</span>`;
|
176
|
+
}
|
177
|
+
else {
|
178
|
+
connection_state.innerHTML = `
|
179
|
+
MetaMask is not connected.
|
180
|
+
<input type="button" onclick="metamask_connect()" value="Connect"/>
|
181
|
+
`;
|
182
|
+
}
|
183
|
+
}
|
184
|
+
else if(this.chain_id) {
|
185
|
+
connection_state.innerHTML = `
|
186
|
+
Unsupported Chain ID: ${this.chain_id}
|
187
|
+
<input type="button" onclick="add_sepolia_network()" value="Switch to Sepolia"/>
|
188
|
+
`;
|
189
|
+
}
|
190
|
+
else {
|
191
|
+
connection_state.innerHTML = `
|
192
|
+
MetaMask disconnected.
|
193
|
+
<input type="button" onclick="metamask_connect()" value="Connect"/>
|
194
|
+
`;
|
195
|
+
}
|
196
|
+
root_container.style["display"] = "block";
|
197
|
+
},
|
198
|
+
// function to check if we're connected to supported network
|
199
|
+
connected: function() {
|
200
|
+
return this.chain() && this.A0;
|
201
|
+
},
|
202
|
+
}
|
203
|
+
</script>
|
204
|
+
|
205
|
+
<script type="text/javascript">
|
206
|
+
// main app section
|
207
|
+
|
208
|
+
// define window.ethereum shortcut
|
209
|
+
const ethereum = window.ethereum;
|
210
|
+
// MetaMask is installed, create a new Web3 instance
|
211
|
+
const web3 = new Web3(window.ethereum);
|
212
|
+
|
213
|
+
// function to connect to MetaMask
|
214
|
+
function metamask_connect() {
|
215
|
+
ethereum.request({ method: 'eth_requestAccounts' }).then(function(accounts) {
|
216
|
+
STATE.update_A0(accounts);
|
217
|
+
STATE.refresh_ui();
|
218
|
+
reload_main_app();
|
219
|
+
}).catch(function(e) {
|
220
|
+
non_fatal_error("Error connecting MetaMask", e);
|
221
|
+
});
|
222
|
+
}
|
223
|
+
|
224
|
+
// check if MetaMask is connected
|
225
|
+
ethereum.request({ method: 'eth_accounts' }).then(function(accounts) {
|
226
|
+
// check current connected network
|
227
|
+
ethereum.request({ method: 'eth_chainId' }).then(function(chain_id) {
|
228
|
+
STATE.update_chain_id(chain_id);
|
229
|
+
STATE.update_A0(accounts);
|
230
|
+
STATE.refresh_ui();
|
231
|
+
reload_main_app();
|
232
|
+
}).catch(function(e) {
|
233
|
+
fatal_error("Error getting Chain ID", e);
|
234
|
+
});
|
235
|
+
}).catch(function(e) {
|
236
|
+
fatal_error("Error connecting to MetaMask", e);
|
237
|
+
});
|
238
|
+
|
239
|
+
// Handle the case when MetaMask connects another account
|
240
|
+
ethereum.on('accountsChanged', function(accounts) {
|
241
|
+
console.log("account has been switched", accounts)
|
242
|
+
STATE.update_A0(accounts);
|
243
|
+
STATE.refresh_ui();
|
244
|
+
reload_main_app();
|
245
|
+
});
|
246
|
+
|
247
|
+
// Handle the case when MetaMask is connected
|
248
|
+
ethereum.on('connect', function(connect_info) {
|
249
|
+
console.log('MetaMask has been connected', connect_info);
|
250
|
+
STATE.update_chain_id(connect_info.chainId);
|
251
|
+
STATE.refresh_ui();
|
252
|
+
reload_main_app();
|
253
|
+
});
|
254
|
+
|
255
|
+
// MetaMask disconnect listener
|
256
|
+
ethereum.on('disconnect', function(error) {
|
257
|
+
console.warn('MetaMask has been disconnected', error);
|
258
|
+
STATE.update_A0();
|
259
|
+
STATE.update_chain_id();
|
260
|
+
STATE.refresh_ui();
|
261
|
+
reload_main_app();
|
262
|
+
});
|
263
|
+
|
264
|
+
// network switch listener
|
265
|
+
ethereum.on('chainChanged', function(chain_id) {
|
266
|
+
console.log('network has been changed', chain_id)
|
267
|
+
STATE.update_chain_id(chain_id);
|
268
|
+
STATE.refresh_ui();
|
269
|
+
reload_main_app();
|
270
|
+
});
|
271
|
+
|
272
|
+
// define an async routine to switch the network to Sepolia
|
273
|
+
async function add_sepolia_network() {
|
274
|
+
const sepolia_chain_id = '0xaa36a7'; // Hexadecimal chain ID for Sepolia
|
275
|
+
|
276
|
+
try {
|
277
|
+
// Check if Sepolia is already added
|
278
|
+
await ethereum.request({
|
279
|
+
method: 'wallet_switchEthereumChain',
|
280
|
+
params: [{ chainId: sepolia_chain_id }],
|
281
|
+
});
|
282
|
+
}
|
283
|
+
catch(e) {
|
284
|
+
// Error code 4902 indicates that the chain has not been added to MetaMask
|
285
|
+
if(e.code !== 4902) {
|
286
|
+
// Handle errors when switching the network
|
287
|
+
fatal_error("Error switching network to Sepolia", e)
|
288
|
+
}
|
289
|
+
|
290
|
+
try {
|
291
|
+
// Add the Sepolia network
|
292
|
+
await ethereum.request({
|
293
|
+
method: 'wallet_addEthereumChain',
|
294
|
+
params: [{
|
295
|
+
chainId: sepolia_chain_id,
|
296
|
+
rpcUrl: 'https://sepolia.infura.io/v3/',
|
297
|
+
chainName: 'Sepolia Test Network',
|
298
|
+
nativeCurrency: {
|
299
|
+
name: 'SepoliaETH',
|
300
|
+
symbol: 'SepoliaETH',
|
301
|
+
decimals: 18,
|
302
|
+
},
|
303
|
+
blockExplorerUrls: ['https://sepolia.etherscan.io/'],
|
304
|
+
}],
|
305
|
+
});
|
306
|
+
}
|
307
|
+
catch (e) {
|
308
|
+
// Handle errors when adding the network
|
309
|
+
fatal_error("Error adding Sepolia network", e);
|
310
|
+
}
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
// writes the entire roles_list table
|
315
|
+
function features_roles_update({features, roles, assignments}) {
|
316
|
+
if(!Array.isArray(roles)) {
|
317
|
+
roles = Object.entries(roles).map(([operator, role]) => Object.assign({}, {operator, role}));
|
318
|
+
}
|
319
|
+
|
320
|
+
if(assignments && assignments.length) {
|
321
|
+
features_hex.innerHTML = features?
|
322
|
+
`Features enabled: <span style="font-family: monospace">0x${features.toString(16).toUpperCase()}</span>`:
|
323
|
+
"No features enabled.";
|
324
|
+
|
325
|
+
if(roles && roles.length) {
|
326
|
+
const table_body = roles.map(render_role_row).join("\n");
|
327
|
+
roles_list.innerHTML = `<tr><th>Operator Address</th><th>Assigned Role</th></tr>\n${table_body}`;
|
328
|
+
}
|
329
|
+
else {
|
330
|
+
roles_list.innerHTML = "No roles assigned.";
|
331
|
+
}
|
332
|
+
}
|
333
|
+
else {
|
334
|
+
features_hex.innerHTML = "No features enabled or not an RBAC contract.";
|
335
|
+
roles_list.innerHTML = "No roles assigned or not an RBAC contract.";
|
336
|
+
}
|
337
|
+
features_roles_container.style["display"] = "block";
|
338
|
+
}
|
339
|
+
|
340
|
+
// renders an HTML of the role list table row
|
341
|
+
function render_role_row({operator, role}) {
|
342
|
+
return `<tr><td>${operator}</td><td>0x${role.toString(16).toUpperCase()}</td></tr>`;
|
343
|
+
}
|
344
|
+
|
345
|
+
// reloads the entire app, used when network or accounts have been changed
|
346
|
+
function reload_main_app() {
|
347
|
+
if(!STATE.connected()) {
|
348
|
+
main_app.style["display"] = "none";
|
349
|
+
return;
|
350
|
+
}
|
351
|
+
|
352
|
+
features_roles_container.style.display = "none";
|
353
|
+
features_title.innerHTML = "RBAC Features";
|
354
|
+
features_hex.innerHTML = "Loading features...";
|
355
|
+
roles_list_title.innerHTML = "RBAC Operators and Roles";
|
356
|
+
roles_list.innerHTML = "Loading operators and roles...";
|
357
|
+
|
358
|
+
main_app.style["display"] = "block";
|
359
|
+
}
|
360
|
+
|
361
|
+
// function to extract RBAC roles from the target contract
|
362
|
+
async function extract_rbac_features_roles(target_address) {
|
363
|
+
// the earliest versions of the AccessControl emitted both FeaturesUpdated and RoleUpdated;
|
364
|
+
// the features were assigned to a zero address
|
365
|
+
// TODO: the earliest versions didn't emit an event on deployment
|
366
|
+
// later versions of the AccessControl emitted only RoleUpdated event;
|
367
|
+
// the features were assigned to a zero address, and later to the contract address (self-role)
|
368
|
+
// the most recent versions of the AccessControl emit only RoleUpdated event;
|
369
|
+
// the features are assigned to the contract address, argument naming is improved and made clearer
|
370
|
+
const v0_abi = [{
|
371
|
+
// the earliest versions only
|
372
|
+
"name": "FeaturesUpdated",
|
373
|
+
"type": "event",
|
374
|
+
"anonymous": false,
|
375
|
+
"inputs": [
|
376
|
+
{"indexed": true, "internalType": "address", "name": "_by", "type": "address"},
|
377
|
+
{"indexed": false, "internalType": "uint256", "name": "_requested", "type": "uint256"},
|
378
|
+
{"indexed": false, "internalType": "uint256", "name": "_actual", "type": "uint256"}
|
379
|
+
],
|
380
|
+
}, {
|
381
|
+
// both the earliest and later versions
|
382
|
+
"name": "RoleUpdated",
|
383
|
+
"type": "event",
|
384
|
+
"anonymous": false,
|
385
|
+
"inputs": [
|
386
|
+
{"indexed": true, "internalType": "address", "name": "_by", "type": "address"},
|
387
|
+
{"indexed": true, "internalType": "address", "name": "_to", "type": "address"},
|
388
|
+
{"indexed": false, "internalType": "uint256", "name": "_requested", "type": "uint256"},
|
389
|
+
{"indexed": false, "internalType": "uint256", "name": "_actual", "type": "uint256"}
|
390
|
+
],
|
391
|
+
}, {
|
392
|
+
// both the earliest and later versions
|
393
|
+
"name": "userRoles",
|
394
|
+
"type": "function",
|
395
|
+
"inputs": [{"internalType": "address", "name": "", "type": "address"}],
|
396
|
+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
397
|
+
"stateMutability": "view",
|
398
|
+
}];
|
399
|
+
const v1_abi = [{
|
400
|
+
// the latest versions only
|
401
|
+
"name": "RoleUpdated",
|
402
|
+
"type": "event",
|
403
|
+
"anonymous": false,
|
404
|
+
"inputs": [
|
405
|
+
{"indexed": true, "internalType": "address", "name": "operator", "type": "address"},
|
406
|
+
{"indexed": false, "internalType": "uint256", "name": "requested", "type": "uint256"},
|
407
|
+
{"indexed": false, "internalType": "uint256", "name": "assigned", "type": "uint256"}
|
408
|
+
],
|
409
|
+
}, {
|
410
|
+
// the latest versions only
|
411
|
+
"name": "getRole",
|
412
|
+
"type": "function",
|
413
|
+
"inputs": [{"internalType": "address", "name": "operator", "type": "address"}],
|
414
|
+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
415
|
+
"stateMutability": "view",
|
416
|
+
}];
|
417
|
+
|
418
|
+
const v0_contract = new web3.eth.Contract(v0_abi, target_address);
|
419
|
+
const v1_contract = new web3.eth.Contract(v1_abi, target_address);
|
420
|
+
|
421
|
+
const features_v0 = await v0_contract.getPastEvents("FeaturesUpdated", {
|
422
|
+
fromBlock: "earliest",
|
423
|
+
});
|
424
|
+
console.log("v0.getPastEvents(FeaturesUpdated)", features_v0);
|
425
|
+
const role_v0 = await v0_contract.getPastEvents("RoleUpdated", {
|
426
|
+
fromBlock: "earliest",
|
427
|
+
});
|
428
|
+
console.log("v0.getPastEvents(RoleUpdated)", role_v0);
|
429
|
+
const role_v1 = await v1_contract.getPastEvents("RoleUpdated", {
|
430
|
+
fromBlock: "earliest",
|
431
|
+
});
|
432
|
+
console.log("v1.getPastEvents(RoleUpdated)", role_v1);
|
433
|
+
const assignments = features_v0.map(function(event) {
|
434
|
+
const {_actual: assigned} = event.returnValues;
|
435
|
+
// fix to the most recent format: feature is self-role
|
436
|
+
return {operator: target_address, assigned};
|
437
|
+
}).concat(role_v0.map(function(event) {
|
438
|
+
let {_to: operator, _actual: assigned} = event.returnValues;
|
439
|
+
if(operator === "0x0000000000000000000000000000000000000000") {
|
440
|
+
// fix to the most recent format: feature is self-role
|
441
|
+
operator = target_address;
|
442
|
+
}
|
443
|
+
return {operator, assigned};
|
444
|
+
})).concat(role_v1.map(function(event) {
|
445
|
+
const {operator, assigned} = event.returnValues;
|
446
|
+
return {operator, assigned};
|
447
|
+
}));
|
448
|
+
|
449
|
+
let features = 0;
|
450
|
+
v0_contract.methods["userRoles"]();
|
451
|
+
|
452
|
+
const roles = {};
|
453
|
+
assignments.forEach(function({operator, assigned}) {
|
454
|
+
if(operator === target_address) {
|
455
|
+
features = assigned;
|
456
|
+
}
|
457
|
+
else if(!BigInt(assigned)) {
|
458
|
+
delete roles[operator];
|
459
|
+
}
|
460
|
+
else {
|
461
|
+
roles[operator] = assigned;
|
462
|
+
}
|
463
|
+
});
|
464
|
+
console.log("extracted RBAC features and roles", features, roles);
|
465
|
+
|
466
|
+
return {features, roles, assignments};
|
467
|
+
}
|
468
|
+
|
469
|
+
// loads and parses features and roles on the RBAC contract,
|
470
|
+
// always returns false since is used as a form submit listener
|
471
|
+
function inspect_features_roles(e) {
|
472
|
+
if(e && e.preventDefault) {
|
473
|
+
e.preventDefault();
|
474
|
+
}
|
475
|
+
|
476
|
+
if(!contract_address.value) {
|
477
|
+
alert("Contract Address is required");
|
478
|
+
return false;
|
479
|
+
}
|
480
|
+
if(!STATE.connected()) {
|
481
|
+
alert("Not connected to the network. Please reload the page if the problem persists.")
|
482
|
+
return false;
|
483
|
+
}
|
484
|
+
|
485
|
+
web3.eth.getCode(contract_address.value).then(function(code) {
|
486
|
+
if(code.length < 4) {
|
487
|
+
alert("no code at " + contract_address.value);
|
488
|
+
}
|
489
|
+
}).catch(function(e) {
|
490
|
+
non_fatal_error("can't load contract at " + contract_address.value, e);
|
491
|
+
});
|
492
|
+
|
493
|
+
extract_rbac_features_roles(contract_address.value).then(features_roles_update).catch(function(e) {
|
494
|
+
console.warn("failed to load RBAC features and roles list", e);
|
495
|
+
features_hex.innerHTML = "Failed to load RBAC features: " + e;
|
496
|
+
roles_list.innerHTML = "Failed to load RBAC roles list: " + e;
|
497
|
+
features_roles_container.style["display"] = "block";
|
498
|
+
}).finally(function() {
|
499
|
+
features_title.innerHTML = "RBAC Features for " + contract_address.value;
|
500
|
+
roles_list_title.innerHTML = "RBAC Operators and Roles for " + contract_address.value;
|
501
|
+
});
|
502
|
+
|
503
|
+
return false;
|
504
|
+
}
|
505
|
+
</script>
|
506
|
+
</html>
|