@lazy-sol/access-control 1.0.6 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|