diff --git a/tools/walletextension/api/routes.go b/tools/walletextension/api/routes.go index 197ae64850..2ef79d592f 100644 --- a/tools/walletextension/api/routes.go +++ b/tools/walletextension/api/routes.go @@ -156,9 +156,9 @@ func ethRequestHandler(walletExt *walletextension.WalletExtension, conn userconn return } - // Get userID + // Get userID and check if user exists (if not - use default user) hexUserID, err := getQueryParameter(conn.ReadRequestParams(), common.UserQueryParameter) - if err != nil { + if err != nil || !walletExt.UserExists(hexUserID) { walletExt.Logger().Error(fmt.Errorf("user not found in the query params: %w. Using the default user", err).Error()) hexUserID = hex.EncodeToString([]byte(common.DefaultUser)) // todo (@ziga) - this can be removed once old WE endpoints are removed } diff --git a/tools/walletextension/api/staticOG/Metamask Network Icon.png b/tools/walletextension/api/staticOG/Metamask Network Icon.png new file mode 100644 index 0000000000..1e966077f8 Binary files /dev/null and b/tools/walletextension/api/staticOG/Metamask Network Icon.png differ diff --git a/tools/walletextension/api/staticOG/index.html b/tools/walletextension/api/staticOG/index.html index 04bf159a86..716e07416a 100644 --- a/tools/walletextension/api/staticOG/index.html +++ b/tools/walletextension/api/staticOG/index.html @@ -4,7 +4,8 @@ Obscuro Gateway - + @@ -18,8 +19,19 @@

◠. Obscuro Gateway Demo! ◠.

-

+ + + + + + + + + + +
AccountsAdded to Obscuro Gateway
+ \ No newline at end of file diff --git a/tools/walletextension/api/staticOG/javascript.js b/tools/walletextension/api/staticOG/javascript.js index c261d1b3d3..f416ee5017 100644 --- a/tools/walletextension/api/staticOG/javascript.js +++ b/tools/walletextension/api/staticOG/javascript.js @@ -5,7 +5,6 @@ const idAddAccount = "addAccount"; const idAddAllAccounts = "addAllAccounts"; const idRevokeUserID = "revokeUserID"; const idStatus = "status"; -const idUserID = "userID"; const obscuroGatewayVersion = "v1" const pathJoin = obscuroGatewayVersion + "/join/"; const pathAuthenticate = obscuroGatewayVersion + "/authenticate/"; @@ -18,7 +17,7 @@ const jsonHeaders = { "Accept": "application/json", "Content-Type": "application/json" }; -const metamaskRequestAccounts = "eth_requestAccounts"; + const metamaskPersonalSign = "personal_sign"; function isValidUserIDFormat(value) { @@ -27,6 +26,8 @@ function isValidUserIDFormat(value) { let obscuroGatewayAddress = window.location.protocol + "//" + window.location.host; +let provider = null; + async function addNetworkToMetaMask(ethereum, userID, chainIDDecimal) { // add network to MetaMask @@ -45,7 +46,7 @@ async function addNetworkToMetaMask(ethereum, userID, chainIDDecimal) { decimals: 18 }, rpcUrls: [obscuroGatewayAddress+"/"+obscuroGatewayVersion+'/?u='+userID], - blockExplorerUrls: null, + blockExplorerUrls: null }, ], }); @@ -108,39 +109,114 @@ async function revokeUserID(userID) { return revokeResponse.ok } -const initialize = () => { +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 { + return await provider.send('eth_getStorageAt', ["getUserID", getRandomIntAsString(0, 1000), null]) + }catch (e) { + console.log(e) + return null; + } +} + +async function connectAccount() { + 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; + } +} + +// 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 { + // TODO: Refactor and change the way we hide and display items on our webpage + document.getElementById(idJoin).style.display = "none"; + document.getElementById(idAddAccount).style.display = "none"; + document.getElementById(idAddAllAccounts).style.display = "none"; + document.getElementById(idRevokeUserID).style.display = "none"; + const statusArea = document.getElementById(idStatus); + 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; + 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); + } +} + +const initialize = async () => { const joinButton = document.getElementById(idJoin); const addAccountButton = document.getElementById(idAddAccount); const addAllAccountsButton = document.getElementById(idAddAllAccounts); const revokeUserIDButton = document.getElementById(idRevokeUserID); const statusArea = document.getElementById(idStatus); - const userIDArea = document.getElementById(idUserID); - // get ObscuroGatewayUserID from local storage - let userID = localStorage.getItem("ObscuroGatewayUserID") + const accountsTable = document.getElementById('accountsTable') + const tableBody = document.getElementById('tableBody'); + // getUserID from the gateway with getStorageAt method + let userID = await getUserID() - // check if userID exists and has correct type and length (is valid) and display either - // option to join or to add new account to existing user + // check if userID exists and has a correct type and length (is valid) and display either + // option to join or to add a new account to existing user if (isValidUserIDFormat(userID)) { - userIDArea.innerText = "Your userID is: " + userID joinButton.style.display = "none" addAccountButton.style.display = "block" addAllAccountsButton.style.display = "block" revokeUserIDButton.style.display = "block" + accountsTable.style.display = "block" + await populateAccountsTable(document, tableBody, userID) } else { joinButton.style.display = "block" addAccountButton.style.display = "none" revokeUserIDButton.style.display = "none" + accountsTable.style.display = "none" } - let ethereum = window.ethereum; - if (!ethereum) { - joinButton.style.display = "none" - addAccountButton.style.display = "none" - statusArea.innerText = "Please install MetaMask to use Obscuro Gateway" - } - - joinButton.addEventListener(eventClick, async () => { // join Obscuro Gateway const joinResp = await fetch( @@ -156,7 +232,6 @@ const initialize = () => { // save userID to the localStorage and hide button that enables users to join userID = await joinResp.text(); - localStorage.setItem("ObscuroGatewayUserID", userID); joinButton.style.display = "none" // add Obscuro network to Metamask @@ -170,36 +245,44 @@ const initialize = () => { addAccountButton.style.display = "block" addAllAccountsButton.style.display = "block" revokeUserIDButton.style.display = "block" + accountsTable.style.display = "block" + await populateAccountsTable(document, tableBody, userID) }) addAccountButton.addEventListener(eventClick, async () => { - // check if we have userID and it is correct length - if (!isValidUserIDFormat(userID)){ + // check if we have userID and it is the correct length + if (!isValidUserIDFormat(userID)) { statusArea.innerText = "\n Please join Obscuro network first" joinButton.style.display = "block" addAccountButton.style.display = "none" } - // Get account and prompt user to sign joining with selected account - const accounts = await ethereum.request({method: metamaskRequestAccounts}); - if (accounts.length === 0) { + await connectAccount() + + // Get an account and prompt user to sign joining with a selected account + const account = await provider.getSigner().getAddress(); + if (account.length === 0) { statusArea.innerText = "No MetaMask accounts found." return } - let authenticateAccountStatus = await authenticateAccountWithObscuroGateway(ethereum, accounts[0], userID) - statusArea.innerText = "\n Authentication status: " + authenticateAccountStatus + let authenticateAccountStatus = await authenticateAccountWithObscuroGateway(ethereum, account, userID) + //statusArea.innerText = "\n Authentication status: " + authenticateAccountStatus + accountsTable.style.display = "block" + await populateAccountsTable(document, tableBody, userID) }) addAllAccountsButton.addEventListener(eventClick, async () => { - // check if we have userID and it is correct length - if (!isValidUserIDFormat(userID)){ + // check if we have userID and it is the correct length + if (!isValidUserIDFormat(userID)) { statusArea.innerText = "\n Please join Obscuro network first" joinButton.style.display = "block" addAccountButton.style.display = "none" } - // Get account and prompt user to sign joining with selected account - const accounts = await ethereum.request({method: metamaskRequestAccounts}); + await connectAccount() + + // Get an account and prompt user to sign joining with selected account + const accounts = await provider.listAccounts(); if (accounts.length === 0) { statusArea.innerText = "No MetaMask accounts found." return @@ -207,25 +290,28 @@ const initialize = () => { for (const account of accounts) { let authenticateAccountStatus = await authenticateAccountWithObscuroGateway(ethereum, account, userID) - statusArea.innerText += "\n Authentication status: " + authenticateAccountStatus + " for account: " + account; + accountsTable.style.display = "block" + await populateAccountsTable(document, tableBody, userID) } }) revokeUserIDButton.addEventListener(eventClick, async () => { let result = await revokeUserID(userID); + + await populateAccountsTable(document, tableBody, userID) + if (result) { - localStorage.removeItem("ObscuroGatewayUserID") joinButton.style.display = "block"; revokeUserIDButton.style.display = "none"; addAllAccountsButton.style.display = "none"; - userIDArea.innerText = ""; statusArea.innerText = "Revoking UserID successful. Please remove current network from Metamask." addAccountButton.style.display = "none"; - }else{ + accountsTable.style.display = "none" + } else { statusArea.innerText = "Revoking UserID failed"; } }) } -window.addEventListener(eventDomLoaded, initialize); \ No newline at end of file +window.addEventListener(eventDomLoaded, checkIfMetamaskIsLoaded); \ No newline at end of file diff --git a/tools/walletextension/api/staticOG/style.css b/tools/walletextension/api/staticOG/style.css index 2c9cadb9cd..e52d97ddb9 100644 --- a/tools/walletextension/api/staticOG/style.css +++ b/tools/walletextension/api/staticOG/style.css @@ -31,4 +31,21 @@ button { /*Formats the block-decoder form.*/ form { display: table; } label { display: table-cell; } -input { display: table-cell; } \ No newline at end of file +input { display: table-cell; } + +/* basic style for the accounts table*/ +table { + border-collapse: collapse; + width: fit-content; + margin: auto; /* Center the table */ + background-color: #333; /* Dark Background */ + color: white; /* Light Text */ +} +th, td { + border: 1px solid #666; /* Darker Border */ + padding: 15px; + text-align: center; +} +th { + background-color: #444; /* Slightly Darker Header */ +} diff --git a/tools/walletextension/test/wallet_extension_test.go b/tools/walletextension/test/wallet_extension_test.go index 9fcf3cb04e..7c1a92ae08 100644 --- a/tools/walletextension/test/wallet_extension_test.go +++ b/tools/walletextension/test/wallet_extension_test.go @@ -307,8 +307,8 @@ func TestGetStorageAtForReturningUserID(t *testing.T) { invalidUserID := "abc123" respBody2 := makeHTTPEthJSONReqWithUserID(walletHTTPPort, rpc.GetStorageAt, []interface{}{"getUserID", "0", nil}, invalidUserID) - if !strings.Contains(string(respBody2), "UserAccountManager doesn't exist for user: "+invalidUserID) { - t.Fatalf("expected response containing invalid userID '%s', got '%s'", invalidUserID, string(respBody2)) + if !strings.Contains(string(respBody2), "method eth_getStorageAt cannot be called with an unauthorised client - no signed viewing keys found") { + t.Fatalf("expected method eth_getStorageAt cannot be called with an unauthorised client - no signed viewing keys found, got '%s'", string(respBody2)) } // make a request to GetStorageAt with userID that is in the database, but wrong parameters diff --git a/tools/walletextension/wallet_extension.go b/tools/walletextension/wallet_extension.go index 77fd39456e..d1db9e1f84 100644 --- a/tools/walletextension/wallet_extension.go +++ b/tools/walletextension/wallet_extension.go @@ -274,7 +274,7 @@ func (w *WalletExtension) UserHasAccount(hexUserID string, address string) (bool return false, err } - // check if any of the accounts matches given account + // check if any of the account matches given account found := false for _, account := range accounts { if bytes.Equal(account.AccountAddress, addressBytes) { @@ -284,7 +284,7 @@ func (w *WalletExtension) UserHasAccount(hexUserID string, address string) (bool return found, nil } -// DeleteUser deletes user and accounts associated with user from database for given userID +// DeleteUser deletes user and accounts associated with user from the database for given userID func (w *WalletExtension) DeleteUser(hexUserID string) error { userIDBytes, err := common.GetUserIDbyte(hexUserID) if err != nil { @@ -307,7 +307,23 @@ func (w *WalletExtension) DeleteUser(hexUserID string) error { return nil } -// verifySignature checks if message was signed by the correct address and if signature is valid +func (w *WalletExtension) UserExists(hexUserID string) bool { + userIDBytes, err := common.GetUserIDbyte(hexUserID) + if err != nil { + w.Logger().Error(fmt.Errorf("error decoding string (%s), %w", hexUserID, err).Error()) + return false + } + + key, err := w.storage.GetUserPrivateKey(userIDBytes) + if err != nil { + w.Logger().Error(fmt.Errorf("error getting user's private key (%s), %w", hexUserID, err).Error()) + return false + } + + return len(key) > 0 +} + +// verifySignature checks if a message was signed by the correct address and if signature is valid func verifySignature(message string, signature []byte, address gethcommon.Address) (bool, error) { // prefix the message like in the personal_sign method prefixedMessage := fmt.Sprintf(common.PersonalSignMessagePrefix, len(message), message)