From 133bacecd33b2414d3f4d88c3d349b4088bae0af Mon Sep 17 00:00:00 2001
From: Jobin Ayathil <0xjba@Macbook-0xJba.local>
Date: Tue, 24 Oct 2023 16:39:46 +0530
Subject: [PATCH] Add Obscuro Gateway Widget Package
---
tools/gatewaywidget/README.md | 37 ++
tools/gatewaywidget/package.json | 23 ++
tools/gatewaywidget/src/index.js | 662 +++++++++++++++++++++++++++++++
tools/gatewaywidget/src/menu.svg | 1 +
4 files changed, 723 insertions(+)
create mode 100644 tools/gatewaywidget/README.md
create mode 100644 tools/gatewaywidget/package.json
create mode 100644 tools/gatewaywidget/src/index.js
create mode 100644 tools/gatewaywidget/src/menu.svg
diff --git a/tools/gatewaywidget/README.md b/tools/gatewaywidget/README.md
new file mode 100644
index 0000000000..19e0008952
--- /dev/null
+++ b/tools/gatewaywidget/README.md
@@ -0,0 +1,37 @@
+
+# Obscuro Gateway Widget
+
+This package provides an easy-to-integrate widget for the Obscuro Gateway. Once included in a webpage, the widget allows users to interact with the Obscuro network.
+
+## Installation
+
+To install the Obscuro Gateway Widget, use npm:
+
+```bash
+npm install obscuro-widget
+```
+
+## Usage
+
+To use the Obscuro Gateway Widget in your webpage:
+
+1. Include the widget's JavaScript in your HTML file:
+
+```html
+
+```
+
+Once the script is included, the Obscuro Gateway Widget will automatically be embedded in your webpage.
+
+## Configuration
+
+Currently, the widget does not accept any external configuration. It's designed to work out-of-the-box once embedded.
+
+## License
+
+Include your license information here or refer to the LICENSE file in the package.
+
+---
+
+For more details or to contribute to this package, please visit the [GitHub repository](https://github.com/YourUsername/YourRepositoryName.git).
+
diff --git a/tools/gatewaywidget/package.json b/tools/gatewaywidget/package.json
new file mode 100644
index 0000000000..de395653e6
--- /dev/null
+++ b/tools/gatewaywidget/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "obscuro-widget",
+ "version": "1.0.0",
+ "description": "Obscuro widget that enables developers to embed a widget that makes it easy for users to connect with the Obscuro gateway.",
+ "main": "src/index.js",
+ "keywords": [
+ "obscuro",
+ "gateway",
+ "widget",
+ "ethereum",
+ "blockchain",
+ "web3",
+ "metamask"
+ ],
+ "author": "obscuro",
+ "license": "ISC",
+ "dependencies": {
+ "ethers": "latest"
+ },
+ "devDependencies": {
+ "parcel": "^2.10.0"
+ }
+}
diff --git a/tools/gatewaywidget/src/index.js b/tools/gatewaywidget/src/index.js
new file mode 100644
index 0000000000..ecfe95fab1
--- /dev/null
+++ b/tools/gatewaywidget/src/index.js
@@ -0,0 +1,662 @@
+
+// Embedding Obscuro Gateway Widget Styles
+(function() {
+ const styles = `/* Obscuro Gateway Widget CSS Code */
+#obscuro-button {
+ border: none;
+ cursor: pointer;
+ outline: none;
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background-color: #1D1D1D;
+ color: #fff;
+ font-size: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0px 0px 7.5px 1px rgba(0,0,0,0.75);
+ transition: transform 0.3s ease;
+ z-index: 9999
+}
+
+#obscuro-button:hover {
+ transform: scale(1.2);
+}
+
+/* Obscuro Gateway Panel */
+#obscuro-panel {
+ box-shadow: 0px 0px 10px 1px rgba(0,0,0,0.75);
+ border-radius: 15px;
+ border: 1px solid #ffffff9f;
+ background-color: #1D1D1D;
+ position: fixed;
+ bottom: 100px;
+ right: 20px;
+ width: 290px;
+ max-height: 600px;
+ overflow-y: auto;
+ z-index: 9998;
+ padding: 20px;
+ flex-direction: column;
+ font-family: 'Onest', sans-serif;
+}
+
+#options {
+ position: absolute;
+ cursor: pointer;
+ right: 20px;
+ border-radius: 25%;
+ font-size: 20px;
+ width: 25px;
+ height: 25px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #d01d38;
+ color: #fff;
+ border: 1px solid #d01d38;
+ transition: background-color 0.3s ease;
+}
+
+#options:hover {
+ background-color: #ff6262;
+ border: 1px solid #ff6262;
+}
+
+#options-menu {
+ position: absolute;
+ top: 80px; /* Adjust this as needed */
+ right: 15px;
+ background-color: #1D1D1D;
+ color: white;
+ box-shadow: 0px 0px 5px 0.5px rgba(0,0,0,0.75);
+ z-index: 10000;
+ width: 150px; /* Or whatever width you desire */
+ cursor: pointer;
+}
+
+#options-menu ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#options-menu li {
+ padding: 10px;
+ border-bottom: 1px solid #353535;
+}
+
+#options-menu li:last-child {
+ border-bottom: none;
+}
+
+h3 {
+ color: #fff;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid #353535; /* Dark line */
+ margin-bottom: 15px;
+}
+
+button {
+ margin: 5px;
+ padding: 10px 15px;
+ background-color: #00a8e6;
+ color: #fff;
+ border: 1px solid #00a8e6;
+ border-radius: 7.5px;
+ transition: background-color 0.3s ease;
+ width: 135px;
+ display: block; /* Makes the button behave like a block-level element */
+ margin-left: auto; /* Centers the button */
+ margin-right: auto;
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: #555;
+ border: 1px solid #00a8e6;
+}
+
+#accountsTable {
+ width: 100%;
+ margin-top: 20px;
+ border-collapse: collapse;
+ border-spacing: 0;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+#accountsTable th, #accountsTable td {
+ padding: 10px;
+ color: #fff;
+ background-color: #222222;
+ text-align: center;
+ border: 0.01em solid #353535;
+}
+
+#accountsTable th {
+ background-color: #000000;
+ color: white;
+}
+
+#status {
+ margin-top: 10px;
+ color: #ffae00;
+ font-size: 15px;
+ text-align: center;
+}
+
+.hidden {
+ display: none;
+}
+
+.close-logo {
+ font-size: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+`;
+
+ const styleElement = document.createElement('style');
+ styleElement.type = 'text/css';
+ styleElement.innerHTML = styles;
+
+ document.head.appendChild(styleElement);
+})();
+
+
+
+// Automatically embed the Obscuro Gateway Widget upon script execution
+(function() {
+ const widgetHTML = `
+
+
+
+
+
+
+
⫶
+
+
+
+
Obscuro Gateway
+
+
+
+
+
+
+
+
+
+
+
+
+ Account |
+ Status |
+
+
+
+
+
+
+
+
+ `;
+
+ // Create a new div for the widget and set its innerHTML
+ const widgetContainer = document.createElement('div');
+ widgetContainer.innerHTML = widgetHTML;
+
+ // Append the widget container to the document body
+ document.body.appendChild(widgetContainer);
+})();
+
+
+const eventClick = "click";
+const eventDomLoaded = "DOMContentLoaded";
+const idJoin = "join";
+const idOptionsButton = "options";
+const idOptionsMenu = "options-menu"
+const idRevokeUserID = "revokeUserID";
+const idStatus = "status";
+const idAccountsTable = "accountsTable";
+const idTableBody = "tableBody";
+const obscuroGatewayVersion = "v1";
+const obscuroGatewayAddress = "https://testnet.obscu.ro";
+const pathJoin = obscuroGatewayAddress + "/" + obscuroGatewayVersion + "/join/";
+const pathAuthenticate = obscuroGatewayAddress + "/" + obscuroGatewayVersion + "/authenticate/";
+const pathQuery = obscuroGatewayAddress + "/" + obscuroGatewayVersion + "/query/";
+const pathRevoke = obscuroGatewayAddress + "/" + obscuroGatewayVersion + "/revoke/";
+const pathVersion = obscuroGatewayAddress + "/version/";
+const obscuroChainIDDecimal = 443;
+const methodPost = "post";
+const methodGet = "get";
+const jsonHeaders = {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+};
+
+const metamaskPersonalSign = "personal_sign";
+const obscuroChainIDHex = "0x" + obscuroChainIDDecimal.toString(16);
+
+function isValidUserIDFormat(value) {
+ return typeof value === 'string' && value.length === 64;
+}
+
+let provider = null;
+
+async function fetchAndDisplayVersion() {
+ try {
+ const versionResp = await fetch(
+ pathVersion, {
+ method: methodGet,
+ headers: jsonHeaders,
+ }
+ );
+ if (!versionResp.ok) {
+ throw new Error("Failed to fetch the version");
+ }
+
+ let response = await versionResp.text();
+
+ const versionDiv = document.getElementById("versionDisplay");
+ versionDiv.textContent = "Version: " + response;
+ } catch (error) {
+ console.error("Error fetching the version:", error);
+ }
+}
+
+function getNetworkName(gatewayAddress) {
+ switch(gatewayAddress) {
+ case 'https://uat-testnet.obscu.ro':
+ return 'Obscuro UAT-Testnet';
+ case 'https://dev-testnet.obscu.ro':
+ return 'Obscuro Dev-Testnet';
+ default:
+ return 'Obscuro Testnet';
+ }
+}
+
+
+function getRPCFromUrl(gatewayAddress) {
+ // get the correct RPC endpoint for each network
+ switch(gatewayAddress) {
+ // case 'https://testnet.obscu.ro':
+ // return 'https://rpc.sepolia-testnet.obscu.ro'
+ case 'https://sepolia-testnet.obscu.ro':
+ return 'https://rpc.sepolia-testnet.obscu.ro'
+ case 'https://uat-testnet.obscu.ro':
+ return 'https://rpc.uat-testnet.obscu.ro';
+ case 'https://dev-testnet.obscu.ro':
+ return 'https://rpc.dev-testnet.obscu.ro';
+ default:
+ return gatewayAddress;
+ }
+}
+
+async function addNetworkToMetaMask(ethereum, userID, chainIDDecimal) {
+ // add network to MetaMask
+ try {
+ await ethereum.request({
+ method: 'wallet_addEthereumChain',
+ params: [
+ {
+ chainId: obscuroChainIDHex,
+ chainName: getNetworkName(obscuroGatewayAddress),
+ nativeCurrency: {
+ name: 'Sepolia Ether',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: [getRPCFromUrl(obscuroGatewayAddress)+"/"+obscuroGatewayVersion+'/?u='+userID],
+ blockExplorerUrls: ['https://testnet.obscuroscan.io'],
+ },
+ ],
+ });
+ } catch (error) {
+ console.error(error);
+ return false
+ }
+ return true
+}
+
+async function authenticateAccountWithObscuroGateway(ethereum, account, userID) {
+ const isAuthenticated = await accountIsAuthenticated(account, userID)
+ if (isAuthenticated) {
+ return "Account is already authenticated"
+ }
+
+ const textToSign = "Register " + userID + " for " + account.toLowerCase();
+ const signature = await ethereum.request({
+ method: metamaskPersonalSign,
+ params: [textToSign, account]
+ }).catch(_ => { return -1 })
+ if (signature === -1) {
+ return "Signing failed"
+ }
+
+ const authenticateUserURL = pathAuthenticate+"?u="+userID
+ const authenticateFields = {"signature": signature, "message": textToSign}
+ const authenticateResp = await fetch(
+ authenticateUserURL, {
+ method: methodPost,
+ headers: jsonHeaders,
+ body: JSON.stringify(authenticateFields)
+ }
+ );
+ return await authenticateResp.text()
+}
+
+async function accountIsAuthenticated(account, userID) {
+ const queryAccountUserID = pathQuery+"?u="+userID+"&a="+account
+ const isAuthenticatedResponse = await fetch(
+ queryAccountUserID, {
+ method: methodGet,
+ headers: jsonHeaders,
+ }
+ );
+ let response = await isAuthenticatedResponse.text();
+ let jsonResponseObject = JSON.parse(response);
+ return jsonResponseObject.status
+}
+
+async function revokeUserID(userID) {
+ const queryAccountUserID = pathRevoke+"?u="+userID
+ const revokeResponse = await fetch(
+ queryAccountUserID, {
+ method: methodGet,
+ headers: jsonHeaders,
+ }
+ );
+ return revokeResponse.ok
+}
+
+function getRandomIntAsString(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;
+ return randomInt.toString();
+}
+
+async function getUserID() {
+ try {
+ if (await isObscuroChain()) {
+ return await provider.send('eth_getStorageAt', ["getUserID", getRandomIntAsString(0, 1000), null])
+ } else {
+ return null
+ }
+ }catch (e) {
+ console.log(e)
+ return null;
+ }
+}
+
+async function connectAccounts() {
+ try {
+ return await window.ethereum.request({ method: 'eth_requestAccounts' });
+ } catch (error) {
+ // TODO: Display warning to user to allow it and refresh page...
+ console.error('User denied account access:', error);
+ return null;
+ }
+}
+
+async function isMetamaskConnected() {
+ let accounts;
+ try {
+ accounts = await provider.listAccounts()
+ return accounts.length > 0;
+
+ } catch (error) {
+ console.log("Unable to get accounts")
+ }
+ return false
+}
+
+// Check if Metamask is available on mobile or as a plugin in browser
+// (https://docs.metamask.io/wallet/how-to/integrate-with-mobile/)
+function checkIfMetamaskIsLoaded() {
+ if (window.ethereum) {
+ handleEthereum();
+ } else {
+ const statusArea = document.getElementById(idStatus);
+ const table = document.getElementById("accountsTable");
+ table.style.display = "none";
+ statusArea.innerText = 'Connecting to Metamask...';
+ window.addEventListener('ethereum#initialized', handleEthereum, {
+ once: true,
+ });
+
+ // If the event is not dispatched by the end of the timeout,
+ // the user probably doesn't have MetaMask installed.
+ setTimeout(handleEthereum, 3000); // 3 seconds
+ }
+}
+
+function handleEthereum() {
+ const { ethereum } = window;
+ if (ethereum && ethereum.isMetaMask) {
+ provider = new ethers.providers.Web3Provider(window.ethereum);
+ initialize()
+ } else {
+ const statusArea = document.getElementById(idStatus);
+ statusArea.innerText = 'Please install MetaMask to use Obscuro Gateway.';
+ }
+}
+
+async function populateAccountsTable(document, tableBody, userID) {
+ tableBody.innerHTML = '';
+ const accounts = await provider.listAccounts();
+ for (const account of accounts) {
+ const row = document.createElement('tr');
+
+ const accountCell = document.createElement('td');
+ accountCell.textContent = account.slice(0, 22);
+ row.appendChild(accountCell);
+
+ const statusCell = document.createElement('td');
+
+ statusCell.textContent = await accountIsAuthenticated(account, userID); // Status is empty for now
+ row.appendChild(statusCell);
+
+ tableBody.appendChild(row);
+ }
+}
+
+async function isObscuroChain() {
+ let currentChain = await ethereum.request({ method: 'eth_chainId' });
+ return currentChain === obscuroChainIDHex
+}
+
+async function switchToObscuroNetwork() {
+ try {
+ await ethereum.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: obscuroChainIDHex }],
+ });
+ return 0
+ } catch (switchError) {
+ return switchError.code
+ }
+ return -1
+}
+
+// Widget UI interactions
+function toggleObscuroPanel() {
+ const panel = document.getElementById('obscuro-panel');
+ if (panel.classList.contains('hidden')) {
+ panel.classList.remove('hidden');
+ } else {
+ panel.classList.add('hidden');
+ }
+}
+
+document.getElementById("options").addEventListener("click", function() {
+ const optionsMenu = document.getElementById("options-menu");
+ if (optionsMenu.style.display === "none" || optionsMenu.style.display === "") {
+ optionsMenu.style.display = "block";
+ } else {
+ optionsMenu.style.display = "none";
+ }
+});
+
+
+document.getElementById('obscuro-button').addEventListener('click', function() {
+ const panel = document.getElementById('obscuro-panel');
+ const openLogo = document.querySelector('.open-logo');
+ const closeLogo = document.querySelector('.close-logo');
+
+ // Toggle the panel's visibility
+ panel.classList.toggle('hidden');
+
+ // Switch between the ◠. and X logos
+ openLogo.classList.toggle('hidden');
+ closeLogo.classList.toggle('hidden');
+});
+
+
+const initialize = async () => {
+ const joinButton = document.getElementById(idJoin);
+ const revokeUserIDButton = document.getElementById(idRevokeUserID);
+ const statusArea = document.getElementById(idStatus);
+ const optionsButton = document.getElementById(idOptionsButton);
+ const optionsMenu = document.getElementById(idOptionsMenu);
+ const accountsTable = document.getElementById(idAccountsTable)
+ const tableBody = document.getElementById(idTableBody);
+ // getUserID from the gateway with getStorageAt method
+ let userID = await getUserID()
+
+ function displayOnlyJoin() {
+ joinButton.style.display = "block"
+ revokeUserIDButton.style.display = "none"
+ optionsButton.style.display = "none";
+ optionsMenu.style.display = "none";
+ accountsTable.style.display = "none"
+ }
+
+ async function displayConnectedAndJoinedSuccessfully() {
+ joinButton.style.display = "none"
+ revokeUserIDButton.style.display = "block"
+ optionsButton.style.display = "flex";
+ optionsMenu.style.display = "none";
+ accountsTable.style.display = "block"
+ await populateAccountsTable(document, tableBody, userID)
+ statusArea.innerText = "Successfully connected";
+ }
+
+ async function displayCorrectScreenBasedOnMetamaskAndUserID() {
+ // check if we are on Obscuro Chain
+ if(await isObscuroChain()){
+ // check if we have valid userID in rpcURL
+ if (isValidUserIDFormat(userID)) {
+ return await displayConnectedAndJoinedSuccessfully()
+ }
+ }
+ return displayOnlyJoin()
+ }
+
+ // load the current version
+ await fetchAndDisplayVersion();
+
+ await displayCorrectScreenBasedOnMetamaskAndUserID()
+
+ joinButton.addEventListener(eventClick, async () => {
+ // check if we are on an obscuro chain
+ if (await isObscuroChain()) {
+ userID = await getUserID()
+ if (!isValidUserIDFormat(userID)) {
+ statusArea.innerText = "Please remove existing Obscuro network from metamask and start again."
+ }
+ } else {
+ // we are not on an Obscuro network - try to switch
+ let switched = await switchToObscuroNetwork();
+ // error 4902 means that the chain does not exist
+ if (switched === 4902 || !isValidUserIDFormat(await getUserID())) {
+ // join the network
+ const joinResp = await fetch(
+ pathJoin, {
+ method: methodGet,
+ headers: jsonHeaders,
+ });
+ if (!joinResp.ok) {
+ console.log("Error joining Obscuro Gateway")
+ statusArea.innerText = "Error joining Obscuro Gateway. Please try again later."
+ return
+ }
+ userID = await joinResp.text();
+
+ // add Obscuro network
+ await addNetworkToMetaMask(window.ethereum, userID)
+ }
+
+ // we have to check if user has accounts connected with metamask - and promt to connect if not
+ if (!await isMetamaskConnected()) {
+ await connectAccounts();
+ }
+
+ // connect all accounts
+ // Get an accounts and prompt user to sign joining with a selected account
+ const accounts = await provider.listAccounts();
+ if (accounts.length === 0) {
+ statusArea.innerText = "No MetaMask accounts found."
+ return
+ }
+
+ userID = await getUserID();
+ for (const account of accounts) {
+ await authenticateAccountWithObscuroGateway(ethereum, account, userID)
+ accountsTable.style.display = "block"
+ await populateAccountsTable(document, tableBody, userID)
+ }
+
+ // if accounts change we want to give user chance to add them to Obscuro
+ window.ethereum.on('accountsChanged', async function (accounts) {
+ if (isValidUserIDFormat(await getUserID())) {
+ userID = await getUserID();
+ for (const account of accounts) {
+ await authenticateAccountWithObscuroGateway(ethereum, account, userID)
+ accountsTable.style.display = "block"
+ await populateAccountsTable(document, tableBody, userID)
+ }
+ }
+ });
+
+ await displayConnectedAndJoinedSuccessfully()
+ }
+ })
+
+ revokeUserIDButton.addEventListener(eventClick, async () => {
+ let result = await revokeUserID(userID);
+
+ await populateAccountsTable(document, tableBody, userID)
+
+ if (result) {
+ displayOnlyJoin()
+ statusArea.innerText = "Revoked UserID";
+ } else {
+ statusArea.innerText = "Revoking UserID failed";
+ }
+ })
+}
+
+window.addEventListener(eventDomLoaded, checkIfMetamaskIsLoaded);
diff --git a/tools/gatewaywidget/src/menu.svg b/tools/gatewaywidget/src/menu.svg
new file mode 100644
index 0000000000..39006d247f
--- /dev/null
+++ b/tools/gatewaywidget/src/menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file