From aab4fcb50d88509299d8f3ec630cdb7c6cdc2081 Mon Sep 17 00:00:00 2001 From: Brent Date: Wed, 29 Sep 2021 10:22:19 -0400 Subject: [PATCH 1/7] Custom network - URL and IP port fix --- packages/extension/src/background/config.ts | 30 ++----- .../background/utils/networkUrlParser.test.ts | 85 +++++++++++++++++++ .../src/background/utils/networkUrlParser.ts | 80 +++++++++++++++++ 3 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 packages/extension/src/background/utils/networkUrlParser.test.ts create mode 100644 packages/extension/src/background/utils/networkUrlParser.ts diff --git a/packages/extension/src/background/config.ts b/packages/extension/src/background/config.ts index 563d7d13..17906626 100644 --- a/packages/extension/src/background/config.ts +++ b/packages/extension/src/background/config.ts @@ -1,6 +1,6 @@ -import logging from '@algosigner/common/logging'; import { LedgerTemplate } from '@algosigner/common/types/ledgers'; import { Ledger, Backend, API } from './messaging/types'; +import { parseUrlServerAndPort } from './utils/networkUrlParser'; export class Settings { static backend: Backend = Backend.PureStake; @@ -91,36 +91,20 @@ export class Settings { } // Setup port splits for algod and indexer - used in sandbox installs - let algodUrlPort = ''; - let indexerUrlPort = ''; - - try { - const algodUrlObj = new URL(ledger.algodUrl); - algodUrlPort = algodUrlObj.port; - } - catch { - logging.log(`Unable to parse the URL ${ledger.algodUrl}`) - } - - try { - const indexerUrlObj = new URL(ledger.indexerUrl); - indexerUrlPort = indexerUrlObj.port; - } - catch { - logging.log(`Unable to parse the URL ${ledger.indexerUrl}`) - } + const parsedAlgodUrlObj = parseUrlServerAndPort(ledger.algodUrl) + const parsedIndexerUrlObj = parseUrlServerAndPort(ledger.indexerUrl); this.backend_settings.InjectedNetworks[ledger.name][API.Algod] = { - url: ledger.algodUrl || `${defaultUrl}/algod`, - port: algodUrlPort, + url: parsedAlgodUrlObj.server || `${defaultUrl}/algod`, + port: parsedAlgodUrlObj.port, apiKey: headersAlgod || headers, headers: headersAlgod || headers, }; // Add the indexer links this.backend_settings.InjectedNetworks[ledger.name][API.Indexer] = { - url: ledger.indexerUrl || `${defaultUrl}/indexer`, - port: indexerUrlPort, + url: parsedIndexerUrlObj.server || `${defaultUrl}/indexer`, + port: parsedIndexerUrlObj.port, apiKey: headersIndexer || headers, headers: headersIndexer || headers, }; diff --git a/packages/extension/src/background/utils/networkUrlParser.test.ts b/packages/extension/src/background/utils/networkUrlParser.test.ts new file mode 100644 index 00000000..90131c9b --- /dev/null +++ b/packages/extension/src/background/utils/networkUrlParser.test.ts @@ -0,0 +1,85 @@ +import { parseUrlServerAndPort } from "./networkUrlParser"; + +test('Validate empty URL', () => { + const result = parseUrlServerAndPort(''); + expect(result["server"]).toBe(""); + expect(result["port"]).toBe(""); +}); + +const portURLs = [ + ['https://localhost:4001','https://localhost','4001'], + ['https://127.0.0.1:4001','https://127.0.0.1','4001'], + ['https://::/128:4001','https://::/128','4001'], + ['https://0:0:0:0:0:0:0:0:4001','https://0:0:0:0:0:0:0:0','4001'], + ['https://[::]:4001','https://[::]','4001'], + ['https://[::/128]:4001','https://[::/128]','4001'], + ['https://[0:0:0:0:0:0:0:0]:4001','https://[::]','4001'], + ['https://subdomain.domain.com:4001','https://subdomain.domain.com','4001'], + ['https://subdomain.domain.com:4001/foo/bar','https://subdomain.domain.com','4001'], + ['https://user:pass@subdomain.domain.com:4001/foo/bar?index=1&limit=10','https://user:pass@subdomain.domain.com','4001'] +]; +test.each(portURLs)( + 'Validate URLs with ports (%s)', + (inputUrl, expectedServer, expectedPort) => { + const result = parseUrlServerAndPort(inputUrl); + expect(result["server"]).toBe(expectedServer); + expect(result["port"]).toBe(expectedPort); + } +); + +const noPortURLs = [ + ['https://localhost','https://localhost',''], + ['https://127.0.0.1','https://127.0.0.1',''], + ['https://::/128','https://::/128',''], + ['https://0:0:0:0:0:0:0:0','https://0:0:0:0:0:0:0:0',''], + ['https://[::]','https://[::]',''], + ['https://[::/128]','https://[::/128]',''], + ['https://[0:0:0:0:0:0:0:0]','https://[::]',''], + ['https://subdomain.domain.com','https://subdomain.domain.com',''], + ['https://subdomain.domain.com/foo/bar','https://subdomain.domain.com',''], + ['https://user:pass@subdomain.domain.com/foo/bar?index=1&limit=10','https://user:pass@subdomain.domain.com',''] +]; +test.each(noPortURLs)( + 'Validate URLs without ports (%s)', + (inputUrl, expectedServer, expectedPort) => { + const result = parseUrlServerAndPort(inputUrl); + expect(result["server"]).toBe(expectedServer); + expect(result["port"]).toBe(expectedPort); + } +); + +const badPortURLs = [ + ['https://localhost:i4001','https://localhost:i4001',''], + ['https://127.0.0.1:i4001','https://127.0.0.1:i4001',''], + ['https://subdomain.domain.com:i4001','https://subdomain.domain.com:i4001',''], + ['https://user:pass@subdomain.domain.com:i4001/foo/bar?index=1&limit=10','https://user:pass@subdomain.domain.com:i4001/foo/bar?index=1&limit=10',''] +]; +test.each(badPortURLs)( + 'Validate Incorrect Ports (%s)', + (inputUrl, expectedServer, expectedPort) => { + const result = parseUrlServerAndPort(inputUrl); + expect(result["server"]).toBe(expectedServer); + expect(result["port"]).toBe(expectedPort); + } +); + +const noProtocolURLs = [ + ['localhost:4001','localhost','4001'], + ['127.0.0.1:4001','127.0.0.1','4001'], + ['::/128:4001','::/128','4001'], + ['0:0:0:0:0:0:0:0:4001','0:0:0:0:0:0:0:0','4001'], + ['[::]:4001','[::]','4001'], + ['[::/128]:4001','[::/128]','4001'], + ['[0:0:0:0:0:0:0:0]:4001','[0:0:0:0:0:0:0:0]','4001'], + ['subdomain.domain.com:4001','subdomain.domain.com','4001'], + ['subdomain.domain.com:4001/foo/bar','subdomain.domain.com','4001'], + ['user:pass@subdomain.domain.com:4001/foo/bar?index=1&limit=10','user:pass@subdomain.domain.com','4001'] +]; +test.each(noProtocolURLs)( + 'Validate URLs without protocols (%s)', + (inputUrl, expectedServer, expectedPort) => { + const result = parseUrlServerAndPort(inputUrl); + expect(result["server"]).toBe(expectedServer); + expect(result["port"]).toBe(expectedPort); + } +); \ No newline at end of file diff --git a/packages/extension/src/background/utils/networkUrlParser.ts b/packages/extension/src/background/utils/networkUrlParser.ts new file mode 100644 index 00000000..ebffbd45 --- /dev/null +++ b/packages/extension/src/background/utils/networkUrlParser.ts @@ -0,0 +1,80 @@ +import logging from "@algosigner/common/logging"; + +export function parseUrlServerAndPort(urlInput:string):any { + // In some cases the default URL builder will not work so we default to splitting on failure + let urlSplit = false; + + // Initialize the server and port as strings + const returnUrlObj = { "server": "", "port": "" } + + // If the input value is blank just return a blank urlObj + if (urlInput.length === 0) { + return returnUrlObj; + } + // Check for localhost, in which case we will just split by colon + if (urlInput.toLowerCase().includes("localhost")) { + urlSplit = true; + } + else { + try { + // Try to build a URL object + const urlObj = new URL(urlInput); + + // If the creation worked we will have a hostname and port + const hostname = urlObj.hostname; + returnUrlObj.port = urlObj.port; + + // If we are missing a hostname then the url didn't parse correctly use a split method + if (!urlObj.hostname) { + urlSplit = true; + } + else { + // Set the base server to the hostname + returnUrlObj.server = hostname; + + // Work backwards adding additional information but leave port separate for algod and indexer creation + if (urlObj.password) { + returnUrlObj.server = urlObj.password + '@' + returnUrlObj.server; + } + if (urlObj.username) { + returnUrlObj.server = urlObj.username + ':' + returnUrlObj.server; + } + if (urlObj.protocol) { + returnUrlObj.server = urlObj.protocol + '//' + returnUrlObj.server; + } + } + } + catch { + urlSplit = true; + } + } + + if (urlSplit) { + try { + const urlArr = urlInput.split(':'); + // A url with a 2 or 3 length may have either a user:pass or port + // If there is an 8 length split it is an IPv6 address without a port or protocol + if (urlArr.length > 1) { + const chkport = urlArr[urlArr.length-1]; + // Now check for the IPV6 localhost and mask components before popping the array + if (chkport[0] !== '/' && chkport !== '0' && (!chkport.includes(']'))) { + const potentialPort = urlArr.pop(); + // Quick cast to ensure port is numeric but a string + returnUrlObj.port = (parseInt(potentialPort) || undefined).toString(); + } + } + returnUrlObj.server = urlArr.join(':'); + } + catch { + returnUrlObj.server = urlInput; + } + } + + // If we dont have a value for server something went wrong + if(returnUrlObj.server.length === 0) { + logging.log(`Error parsing url:\n${urlInput}\nSetting value for server as a blank value.`); + } + + // Returns an object with server and port or blank values in failures + return returnUrlObj; +} \ No newline at end of file From c2ac96f27a38af322d939b6e2639431bf9801b7b Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Wed, 29 Sep 2021 20:24:38 -0300 Subject: [PATCH 2/7] Sequential Signing of nested arrays of transactions --- packages/common/src/messaging/types.ts | 1 + packages/common/src/types.ts | 1 + packages/dapp/src/fn/task.ts | 32 +- .../src/background/messaging/task.ts | 586 +++++++++++------- 4 files changed, 372 insertions(+), 248 deletions(-) diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index c4e8ea55..9f461b53 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -14,6 +14,7 @@ export enum JsonRpcMethod { SignDeny = 'sign-deny', SignTransaction = 'sign-transaction', SignMultisigTransaction = 'sign-multisig-transaction', + HandleWalletTransactions = 'handle-wallet-transactions', SignWalletTransaction = 'sign-wallet-transaction', SendTransaction = 'send-transaction', Algod = 'algod', diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index f2e2558e..d423fb1b 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -5,6 +5,7 @@ export enum RequestErrors { InvalidTransactionParams = '[RequestErrors.InvalidTransactionParams] Invalid transaction parameters.', UnsupportedAlgod = '[RequestErrors.UnsupportedAlgod] The provided method is not supported.', UnsupportedLedger = '[RequestErrors.UnsupportedLedger] The provided ledger is not supported.', + InvalidFormat = '[RequestErrors.InvalidFormat] Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', Undefined = '[RequestErrors.Undefined] An undefined error occurred.', } diff --git a/packages/dapp/src/fn/task.ts b/packages/dapp/src/fn/task.ts index 97b27730..ccd5a76c 100644 --- a/packages/dapp/src/fn/task.ts +++ b/packages/dapp/src/fn/task.ts @@ -50,30 +50,34 @@ export class Task extends Runtime implements ITask { } /** - * @param transactions array of valid wallet transaction objects - * @returns array of signed transactions + * @param transactionsOrGroups array or nested array of grouped transaction objects + * @returns array or nested array of signed transactions */ signTxn( - transactions: Array, + transactionsOrGroups: Array, error: RequestErrors = RequestErrors.None ): Promise { - const formatError = new Error( - 'There was a problem with the transaction(s) recieved. Please provide an array of valid transaction objects.' - ); - if (!Array.isArray(transactions) || !transactions.length) throw formatError; - transactions.forEach((walletTx) => { + const formatError = new Error(RequestErrors.InvalidFormat); + // We check for empty arrays + if (!Array.isArray(transactionsOrGroups) || !transactionsOrGroups.length) throw formatError; + transactionsOrGroups.forEach((txOrGroup) => { + // We check for no null values and no empty nested arrays if ( - walletTx === null || - typeof walletTx !== 'object' || - walletTx.txn === null || - !walletTx.txn.length + txOrGroup === null || + txOrGroup === undefined || + (!Array.isArray(txOrGroup) && typeof txOrGroup === 'object' && + (!txOrGroup.txn || (txOrGroup.txn && !txOrGroup.txn.length)) + ) || + (Array.isArray(txOrGroup) && + (!txOrGroup.length || (txOrGroup.length && !txOrGroup.every((tx) => tx !== null))) + ) ) throw formatError; }); const params = { - transactions: transactions, + transactions: transactionsOrGroups, }; - return MessageBuilder.promise(JsonRpcMethod.SignWalletTransaction, params, error); + return MessageBuilder.promise(JsonRpcMethod.HandleWalletTransactions, params, error); } } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index b558287b..b295de0f 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -38,7 +38,7 @@ const authPopupProperties = { type: 'popup', focused: true, width: 400, - height: 550 + titleBarHeight, + height: 550 + titleBarHeight, }; const signPopupProperties = { @@ -317,6 +317,7 @@ export class Task { }); } }, + // sign-multisig-transaction [JsonRpcMethod.SignMultisigTransaction]: (d: any, resolve: Function, reject: Function) => { // TODO: Possible support for blob transfer on previously signed transactions @@ -432,245 +433,82 @@ export class Task { }); } }, - // sign-wallet-transaction - [JsonRpcMethod.SignWalletTransaction]: async ( - d: any, - resolve: Function, - reject: Function - ) => { - const walletTransactions: Array = d.body.params.transactions; - const rawTxArray: Array = []; - const processedTxArray: Array = []; - const transactionWraps: Array = []; - const validationErrors: Array = []; - - walletTransactions.forEach((walletTx, index) => { - try { - // Runtime type checking - if ( - // prettier-ignore - (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || - (walletTx.message != null && typeof walletTx.message !== 'string') || - (!walletTx.txn || typeof walletTx.txn !== 'string') || - (walletTx.signers != null && - ( - !Array.isArray(walletTx.signers) || - (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) - ) - ) || - (walletTx.msig && typeof walletTx.msig !== 'object') - ) { - logging.log('Invalid Wallet Transaction Structure'); - throw new InvalidStructure(); - } else if ( - // prettier-ignore - walletTx.msig && ( - (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || - (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || - ( - !walletTx.msig.addrs || - !Array.isArray(walletTx.msig.addrs) || - (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) - ) - ) - ) { - logging.log('Invalid Wallet Transaction Multisig Structure'); - throw new InvalidMsigStructure(); - } - - /** - * In order to process the transaction and make it compatible with our validator, we: - * 0) Decode from base64 to Uint8Array msgpack - * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack - * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours - * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays - */ - const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); - rawTxArray[index] = rawTx; - const processedTx = rawTx._getDictForDisplay(); - processedTxArray[index] = processedTx; - const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); - transactionWraps[index] = wrap; - const genesisID = wrap.transaction.genesisID; - - const signers = walletTransactions[index].signers; - const msigData = walletTransactions[index].msig; - wrap.msigData = msigData; - wrap.signers = signers; - if (msigData) { - if (signers && signers.length) { - signers.forEach((address) => { - InternalMethods.checkValidAccount(genesisID, address); - }); - } - wrap.msigData = msigData; - } else { - if (!signers) { - InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); - } - } - - return wrap; - } catch (e) { - validationErrors[index] = e; - } - }); - - if ( - validationErrors.length || - !transactionWraps.length || - transactionWraps.some((w) => w === undefined) - ) { - // We don't have transaction wraps or we have an building error, reject the transaction. - let errorMessage = 'There was a problem validating the transaction(s): '; - let validationMessages = ''; + // handle-wallet-transactions + [JsonRpcMethod.HandleWalletTransactions]: (d: any, resolve: Function, reject: Function) => { + console.log(d); + console.log('===== START NESTED HANDLING ====='); + const transactionsOrGroups: Array | Array> = + d.body.params.transactions; + + console.log(transactionsOrGroups); + // We check to see if it's a simple or nested array of transactions + const singleGroup = (transactionsOrGroups as Array).every( + (walletTx) => + !Array.isArray(walletTx) && + typeof walletTx === 'object' && + walletTx.txn && + walletTx.txn.length + ); + console.log('after single'); + const multipleGroups = (transactionsOrGroups as Array>).every( + (walletTxArray) => + Array.isArray(walletTxArray) && + walletTxArray.length && + walletTxArray.every( + (walletTx) => + walletTx && + !Array.isArray(walletTx) && + typeof walletTx === 'object' && + walletTx.txn && + walletTx.txn.length + ) + ); + console.log('after multiple'); + console.log(`Single: ${singleGroup}, Multiple: ${multipleGroups}`); - validationErrors.forEach((err, index) => { - validationMessages = - validationMessages + - `\nValidation failed for transaction ${index} due to: ${err.message}`; - }); - errorMessage += - (validationMessages.length && validationMessages) || - 'Please verify the properties are valid.'; - logging.log(errorMessage); + // If none of the formats match up, we throw an error + if (!singleGroup && !multipleGroups) { + logging.log(RequestErrors.InvalidFormat); d.error = { - message: errorMessage, + message: RequestErrors.InvalidFormat, }; reject(d); return; - } else if ( - transactionWraps.some( - (tx) => - tx.validityObject && - Object.values(tx.validityObject).some( - (value) => value['status'] === ValidationStatus.Invalid - ) - ) - ) { - // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. - const invalidKeys = {}; - transactionWraps.forEach((tx, index) => { - invalidKeys[index] = []; - Object.entries(tx.validityObject).forEach(([key, value]) => { - if (value['status'] === ValidationStatus.Invalid) { - invalidKeys[index].push(`${key}: ${value['info']}`); - } - }); - if (!invalidKeys[index].length) delete invalidKeys[index]; - }); - - let errorMessage = ''; + } - Object.keys(invalidKeys).forEach((index) => { - errorMessage = - errorMessage + - `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ - index - ].join(', ')}]. `; - }); + let groupsToSign = [transactionsOrGroups]; + if (multipleGroups) { + groupsToSign = transactionsOrGroups as Array>; + } + console.log(groupsToSign); - d.error = { - message: errorMessage, - }; - reject(d); - return; - } else { - // Group validations - if (transactionWraps.length > 1) { - if ( - !transactionWraps.every( - (wrap) => transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID - ) - ) { - const e = new NoDifferentLedgers(); - logging.log(`Validation failed. ${e}`); - d.error = e; - reject(d); - return; - } + const newRequest = Object.assign({}, d); - const groupId = transactionWraps[0].transaction.group; - if (!groupId) { - const e = new MultipleTxsRequireGroup(); - logging.log(`Validation failed. ${e}`); - d.error = e; - reject(d); - return; - } + newRequest.body.method = JsonRpcMethod.SignWalletTransaction; + newRequest.body.params.groupsToSign = groupsToSign; + newRequest.body.params.currentGroup = 0; + newRequest.body.params.signedGroups = []; - if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { - const e = new NonMatchingGroup(); - logging.log(`Validation failed. ${e}`); - d.error = e; - reject(d); - return; - } + console.log('===== FINISH NESTED HANDLING ====='); - const recreatedGroupTxs = algosdk.assignGroupID( - rawTxArray.slice().map((tx) => { - delete tx.group; - return tx; - }) - ); - const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); - if (groupId !== recalculatedGroupID) { - const e = new IncompleteOrDisorderedGroup(); - logging.log(`Validation failed. ${e}`); - d.error = e; - reject(d); - return; - } + try { + this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); + } catch (e) { + let errorMessage = 'There was a problem validating the transaction(s). '; - // If the whole group is provided and verified, we mark the group field as valid instead of dangerous - transactionWraps.forEach((wrap) => { - wrap.validityObject['group'] = new ValidationResponse({ - status: ValidationStatus.Valid, - }); - }); + if (singleGroup) { + errorMessage += e.message; } else { - const wrap = transactionWraps[0]; - if ( - (!wrap.msigData && wrap.signers) || - (wrap.msigData && wrap.signers && !wrap.signers.length) - ) { - const e = new InvalidSigners(); - logging.log(`Validation failed. ${e}`); - d.error = e; - reject(d); - return; - } - } - - for (let i = 0; i < transactionWraps.length; i++) { - const wrap = transactionWraps[i]; - await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); + errorMessage += `\nOn group 0: [${e.message}].`; } - - d.body.params.transactionWraps = transactionWraps; - - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), - ...signPopupProperties, - }, - function (w) { - if (w) { - Task.requests[d.originTabID] = { - window_id: w.id, - message: d, - }; - // Send message with tx info - setTimeout(function () { - extensionBrowser.runtime.sendMessage(d); - }, 500); - } - } - ); + logging.log(errorMessage); + const error = new Error(errorMessage); + d.error = error; + reject(d); + return; } }, - // algod + // send-transaction [JsonRpcMethod.SendTransaction]: (d: any, resolve: Function, reject: Function) => { const { params } = d.body; const conn = Settings.getBackendParams(params.ledger, API.Algod); @@ -1118,18 +956,253 @@ export class Task { } return true; }, + // sign-wallet-transaction + [JsonRpcMethod.SignWalletTransaction]: async (d: any) => { + console.log('===== START PREPARING UI ====='); + console.log(d); + const groupsToSign: Array> = d.body.params.groupsToSign; + const currentGroup: number = d.body.params.currentGroup; + const walletTransactions: Array = groupsToSign[currentGroup]; + const rawTxArray: Array = []; + const processedTxArray: Array = []; + const transactionWraps: Array = []; + const validationErrors: Array = []; + + walletTransactions.forEach((walletTx, index) => { + try { + // Runtime type checking + if ( + // prettier-ignore + (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || + (walletTx.message != null && typeof walletTx.message !== 'string') || + (!walletTx.txn || typeof walletTx.txn !== 'string') || + (walletTx.signers != null && + ( + !Array.isArray(walletTx.signers) || + (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) + ) + ) || + (walletTx.msig && typeof walletTx.msig !== 'object') + ) { + logging.log('Invalid Wallet Transaction Structure'); + throw new InvalidStructure(); + } else if ( + // prettier-ignore + walletTx.msig && ( + (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || + (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || + ( + !walletTx.msig.addrs || + !Array.isArray(walletTx.msig.addrs) || + (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) + ) + ) + ) { + logging.log('Invalid Wallet Transaction Multisig Structure'); + throw new InvalidMsigStructure(); + } + + /** + * In order to process the transaction and make it compatible with our validator, we: + * 0) Decode from base64 to Uint8Array msgpack + * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack + * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours + * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays + */ + const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); + rawTxArray[index] = rawTx; + const processedTx = rawTx._getDictForDisplay(); + processedTxArray[index] = processedTx; + const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); + transactionWraps[index] = wrap; + const genesisID = wrap.transaction.genesisID; + + const signers = walletTransactions[index].signers; + const msigData = walletTransactions[index].msig; + wrap.msigData = msigData; + wrap.signers = signers; + if (msigData) { + if (signers && signers.length) { + signers.forEach((address) => { + InternalMethods.checkValidAccount(genesisID, address); + }); + } + wrap.msigData = msigData; + } else { + if (!signers) { + InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); + } + } + + return wrap; + } catch (e) { + validationErrors[index] = e; + } + }); + + if ( + validationErrors.length || + !transactionWraps.length || + transactionWraps.some((w) => w === undefined) + ) { + // We don't have transaction wraps or we have an building error, reject the transaction. + let errorMessage = 'There was a problem validating the transaction(s): '; + let validationMessages = ''; + + validationErrors.forEach((err, index) => { + validationMessages = + validationMessages + + `\nValidation failed for transaction ${index} due to: ${err.message}`; + }); + errorMessage += + (validationMessages.length && validationMessages) || + 'Please verify the properties are valid.'; + logging.log(errorMessage); + const error = new Error(errorMessage); + throw error; + } else if ( + transactionWraps.some( + (tx) => + tx.validityObject && + Object.values(tx.validityObject).some( + (value) => value['status'] === ValidationStatus.Invalid + ) + ) + ) { + // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. + // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. + const invalidKeys = {}; + transactionWraps.forEach((tx, index) => { + invalidKeys[index] = []; + Object.entries(tx.validityObject).forEach(([key, value]) => { + if (value['status'] === ValidationStatus.Invalid) { + invalidKeys[index].push(`${key}: ${value['info']}`); + } + }); + if (!invalidKeys[index].length) delete invalidKeys[index]; + }); + + let errorMessage = ''; + + Object.keys(invalidKeys).forEach((index) => { + errorMessage = + errorMessage + + `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ + index + ].join(', ')}]. `; + }); + + logging.log(errorMessage); + const error = new Error(errorMessage); + throw error; + } else { + // Group validations + if (transactionWraps.length > 1) { + if ( + !transactionWraps.every( + (wrap) => transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID + ) + ) { + const e = new NoDifferentLedgers(); + logging.log(`Validation failed. ${e}`); + throw e; + } + + const groupId = transactionWraps[0].transaction.group; + if (!groupId) { + const e = new MultipleTxsRequireGroup(); + logging.log(`Validation failed. ${e}`); + throw e; + } + + if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { + const e = new NonMatchingGroup(); + logging.log(`Validation failed. ${e}`); + throw e; + } + + const recreatedGroupTxs = algosdk.assignGroupID( + rawTxArray.slice().map((tx) => { + delete tx.group; + return tx; + }) + ); + const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); + if (groupId !== recalculatedGroupID) { + const e = new IncompleteOrDisorderedGroup(); + logging.log(`Validation failed. ${e}`); + throw e; + } + + // If the whole group is provided and verified, we mark the group field as valid instead of dangerous + transactionWraps.forEach((wrap) => { + wrap.validityObject['group'] = new ValidationResponse({ + status: ValidationStatus.Valid, + }); + }); + } else { + const wrap = transactionWraps[0]; + if ( + (!wrap.msigData && wrap.signers) || + (wrap.msigData && wrap.signers && !wrap.signers.length) + ) { + const e = new InvalidSigners(); + logging.log(`Validation failed. ${e}`); + throw e; + } + } + + for (let i = 0; i < transactionWraps.length; i++) { + const wrap = transactionWraps[i]; + await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); + } + + d.body.params.transactionWraps = transactionWraps; + + console.log('===== FINISH PREPARING UI ====='); + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), + ...signPopupProperties, + }, + function (w) { + if (w) { + Task.requests[d.originTabID] = { + window_id: w.id, + message: d, + }; + // Send message with tx info + setTimeout(function () { + extensionBrowser.runtime.sendMessage(d); + }, 500); + } + } + ); + } + }, // sign-allow-wallet-tx [JsonRpcMethod.SignAllowWalletTx]: (request: any, sendResponse: Function) => { const { passphrase, responseOriginTabID } = request.body.params; const auth = Task.requests[responseOriginTabID]; const message = auth.message; - const walletTransactions: Array = message.body.params.transactions; + const { groupsToSign, currentGroup, signedGroups } = message.body.params; + const singleGroup = groupsToSign.length === 1; + const walletTransactions: Array = groupsToSign[currentGroup]; const transactionsWraps: Array = message.body.params.transactionWraps; const transactionObjs = walletTransactions.map((walletTx) => algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)) ); + console.log('===== START BACKGROUND SIGNING ====='); + console.log(`Current group: ${currentGroup}, Single: ${singleGroup}`); + console.log(`UI ID: ${responseOriginTabID}`); + console.log(auth); + console.log(walletTransactions); + + // const groupsToSign: Array> = request.body.params.groupsToSign; + // const currentGroup: number = d.body.params.currentGroup; + const signedTxs = []; const signErrors = []; @@ -1169,8 +1242,9 @@ export class Task { sendResponse(unlockedValue); return false; } - extensionBrowser.windows.remove(auth.window_id); + // We close the current signing window and start retrieving accounts for signing + extensionBrowser.windows.remove(auth.window_id); const recoveredAccounts = []; if (unlockedValue[ledger] === undefined) { @@ -1239,21 +1313,65 @@ export class Task { } }); + // We check if there were errors signing this group if (signErrors.length) { - message.error = 'There was a problem signing the transaction(s): '; + let errorMessage = 'There was a problem signing the transaction(s): '; if (transactionObjs.length > 1) { signErrors.forEach((error, index) => { - message.error += `\nOn transaction ${index}, the error was: ${error}`; + errorMessage += `\nOn transaction ${index}, the error was: ${error}`; }); } else { - message.error += signErrors[0]; + errorMessage += signErrors[0]; } + if (!singleGroup) { + errorMessage = `\nOn group ${currentGroup}: [${errorMessage}].`; + } + message.error = errorMessage; + logging.log(errorMessage); } else { - message.response = signedTxs; + signedGroups[currentGroup] = signedTxs; + } + + console.log('After error processing'); + console.log(message); + // In case of signing error, we abort everything. + if (message.error) { + // Clean class saved request + delete Task.requests[responseOriginTabID]; + MessageApi.send(message); + } + + // We check if there are more groups to sign + const newRequest = Object.assign({}, message); + newRequest.body.method = JsonRpcMethod.SignWalletTransaction; + newRequest.body.params.currentGroup = currentGroup + 1; + newRequest.body.params.signedGroups = signedGroups; + console.log('Accumulated groups after sign:'); + console.log(signedGroups); + console.log('===== FINISH BACKGROUND SIGNING ====='); + + if (newRequest.body.params.currentGroup < groupsToSign.length) { + try { + this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); + } catch (e) { + let errorMessage = 'There was a problem validating the transaction(s). '; + + if (singleGroup) { + errorMessage += e.message; + } else { + errorMessage += `\nOn group ${currentGroup}: [${e.message}].`; + } + logging.log(errorMessage); + const error = new Error(errorMessage); + sendResponse(error); + return; + } + } else { + message.response = signedGroups; + // Clean class saved request + delete Task.requests[responseOriginTabID]; + MessageApi.send(message); } - // Clean class saved request - delete Task.requests[responseOriginTabID]; - MessageApi.send(message); }); } catch { // On error we should remove the task From 83cd6df41efa310069e4ffe903c70a63037c79a0 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Thu, 30 Sep 2021 16:00:19 -0300 Subject: [PATCH 3/7] Fix Error handling --- packages/dapp/src/fn/task.ts | 2 +- .../src/background/messaging/task.ts | 438 +++++++++--------- 2 files changed, 228 insertions(+), 212 deletions(-) diff --git a/packages/dapp/src/fn/task.ts b/packages/dapp/src/fn/task.ts index ccd5a76c..00ec0b52 100644 --- a/packages/dapp/src/fn/task.ts +++ b/packages/dapp/src/fn/task.ts @@ -76,7 +76,7 @@ export class Task extends Runtime implements ITask { }); const params = { - transactions: transactionsOrGroups, + transactionsOrGroups: transactionsOrGroups, }; return MessageBuilder.promise(JsonRpcMethod.HandleWalletTransactions, params, error); } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index b295de0f..cac5de8d 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -434,11 +434,11 @@ export class Task { } }, // handle-wallet-transactions - [JsonRpcMethod.HandleWalletTransactions]: (d: any, resolve: Function, reject: Function) => { + [JsonRpcMethod.HandleWalletTransactions]: async (d: any, resolve: Function, reject: Function) => { console.log(d); console.log('===== START NESTED HANDLING ====='); const transactionsOrGroups: Array | Array> = - d.body.params.transactions; + d.body.params.transactionsOrGroups; console.log(transactionsOrGroups); // We check to see if it's a simple or nested array of transactions @@ -491,22 +491,7 @@ export class Task { console.log('===== FINISH NESTED HANDLING ====='); - try { - this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); - } catch (e) { - let errorMessage = 'There was a problem validating the transaction(s). '; - - if (singleGroup) { - errorMessage += e.message; - } else { - errorMessage += `\nOn group 0: [${e.message}].`; - } - logging.log(errorMessage); - const error = new Error(errorMessage); - d.error = error; - reject(d); - return; - } + this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); }, // send-transaction [JsonRpcMethod.SendTransaction]: (d: any, resolve: Function, reject: Function) => { @@ -957,227 +942,244 @@ export class Task { return true; }, // sign-wallet-transaction - [JsonRpcMethod.SignWalletTransaction]: async (d: any) => { + [JsonRpcMethod.SignWalletTransaction]: async (request: any, sendResponse: Function) => { console.log('===== START PREPARING UI ====='); - console.log(d); - const groupsToSign: Array> = d.body.params.groupsToSign; - const currentGroup: number = d.body.params.currentGroup; + console.log(request); + const groupsToSign: Array> = request.body.params.groupsToSign; + const currentGroup: number = request.body.params.currentGroup; const walletTransactions: Array = groupsToSign[currentGroup]; const rawTxArray: Array = []; const processedTxArray: Array = []; const transactionWraps: Array = []; const validationErrors: Array = []; - - walletTransactions.forEach((walletTx, index) => { - try { - // Runtime type checking - if ( - // prettier-ignore - (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || - (walletTx.message != null && typeof walletTx.message !== 'string') || - (!walletTx.txn || typeof walletTx.txn !== 'string') || - (walletTx.signers != null && - ( - !Array.isArray(walletTx.signers) || - (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) - ) - ) || - (walletTx.msig && typeof walletTx.msig !== 'object') - ) { - logging.log('Invalid Wallet Transaction Structure'); - throw new InvalidStructure(); - } else if ( - // prettier-ignore - walletTx.msig && ( - (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || - (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || - ( - !walletTx.msig.addrs || - !Array.isArray(walletTx.msig.addrs) || - (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) + try { + walletTransactions.forEach((walletTx, index) => { + try { + // Runtime type checking + if ( + // prettier-ignore + (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || + (walletTx.message != null && typeof walletTx.message !== 'string') || + (!walletTx.txn || typeof walletTx.txn !== 'string') || + (walletTx.signers != null && + ( + !Array.isArray(walletTx.signers) || + (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) + ) + ) || + (walletTx.msig && typeof walletTx.msig !== 'object') + ) { + logging.log('Invalid Wallet Transaction Structure'); + throw new InvalidStructure(); + } else if ( + // prettier-ignore + walletTx.msig && ( + (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || + (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || + ( + !walletTx.msig.addrs || + !Array.isArray(walletTx.msig.addrs) || + (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) + ) ) - ) - ) { - logging.log('Invalid Wallet Transaction Multisig Structure'); - throw new InvalidMsigStructure(); - } - - /** - * In order to process the transaction and make it compatible with our validator, we: - * 0) Decode from base64 to Uint8Array msgpack - * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack - * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours - * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays - */ - const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); - rawTxArray[index] = rawTx; - const processedTx = rawTx._getDictForDisplay(); - processedTxArray[index] = processedTx; - const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); - transactionWraps[index] = wrap; - const genesisID = wrap.transaction.genesisID; - - const signers = walletTransactions[index].signers; - const msigData = walletTransactions[index].msig; - wrap.msigData = msigData; - wrap.signers = signers; - if (msigData) { - if (signers && signers.length) { - signers.forEach((address) => { - InternalMethods.checkValidAccount(genesisID, address); - }); + ) { + logging.log('Invalid Wallet Transaction Multisig Structure'); + throw new InvalidMsigStructure(); } + + /** + * In order to process the transaction and make it compatible with our validator, we: + * 0) Decode from base64 to Uint8Array msgpack + * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack + * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours + * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays + */ + const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); + rawTxArray[index] = rawTx; + const processedTx = rawTx._getDictForDisplay(); + processedTxArray[index] = processedTx; + const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); + transactionWraps[index] = wrap; + const genesisID = wrap.transaction.genesisID; + + const signers = walletTransactions[index].signers; + const msigData = walletTransactions[index].msig; wrap.msigData = msigData; - } else { - if (!signers) { - InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); + wrap.signers = signers; + if (msigData) { + if (signers && signers.length) { + signers.forEach((address) => { + InternalMethods.checkValidAccount(genesisID, address); + }); + } + wrap.msigData = msigData; + } else { + if (!signers) { + InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); + } } + + return wrap; + } catch (e) { + validationErrors[index] = e; } + }); - return wrap; - } catch (e) { - validationErrors[index] = e; - } - }); + if ( + validationErrors.length || + !transactionWraps.length || + transactionWraps.some((w) => w === undefined) + ) { + // We don't have transaction wraps or we have an building error, reject the transaction. + let errorMessage = ''; + let validationMessages = ''; + + validationErrors.forEach((err, index) => { + validationMessages = + validationMessages + + `\nValidation failed for transaction ${index} due to: ${err.message}`; + }); + errorMessage += + (validationMessages.length && validationMessages) || + 'Please verify the properties are valid.'; + const error = new Error(errorMessage); + throw error; + } else if ( + transactionWraps.some( + (tx) => + tx.validityObject && + Object.values(tx.validityObject).some( + (value) => value['status'] === ValidationStatus.Invalid + ) + ) + ) { + // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. + // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. + const invalidKeys = {}; + transactionWraps.forEach((tx, index) => { + invalidKeys[index] = []; + Object.entries(tx.validityObject).forEach(([key, value]) => { + if (value['status'] === ValidationStatus.Invalid) { + invalidKeys[index].push(`${key}: ${value['info']}`); + } + }); + if (!invalidKeys[index].length) delete invalidKeys[index]; + }); - if ( - validationErrors.length || - !transactionWraps.length || - transactionWraps.some((w) => w === undefined) - ) { - // We don't have transaction wraps or we have an building error, reject the transaction. - let errorMessage = 'There was a problem validating the transaction(s): '; - let validationMessages = ''; + let errorMessage = ''; - validationErrors.forEach((err, index) => { - validationMessages = - validationMessages + - `\nValidation failed for transaction ${index} due to: ${err.message}`; - }); - errorMessage += - (validationMessages.length && validationMessages) || - 'Please verify the properties are valid.'; - logging.log(errorMessage); - const error = new Error(errorMessage); - throw error; - } else if ( - transactionWraps.some( - (tx) => - tx.validityObject && - Object.values(tx.validityObject).some( - (value) => value['status'] === ValidationStatus.Invalid - ) - ) - ) { - // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. - const invalidKeys = {}; - transactionWraps.forEach((tx, index) => { - invalidKeys[index] = []; - Object.entries(tx.validityObject).forEach(([key, value]) => { - if (value['status'] === ValidationStatus.Invalid) { - invalidKeys[index].push(`${key}: ${value['info']}`); - } + Object.keys(invalidKeys).forEach((index) => { + errorMessage = + errorMessage + + `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ + index + ].join(', ')}]. `; }); - if (!invalidKeys[index].length) delete invalidKeys[index]; - }); - let errorMessage = ''; + const error = new Error(errorMessage); + throw error; + } else { + // Group validations + if (transactionWraps.length > 1) { + if ( + !transactionWraps.every( + (wrap) => + transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID + ) + ) { + const e = new NoDifferentLedgers(); + throw e; + } - Object.keys(invalidKeys).forEach((index) => { - errorMessage = - errorMessage + - `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ - index - ].join(', ')}]. `; - }); + const groupId = transactionWraps[0].transaction.group; + if (!groupId) { + const e = new MultipleTxsRequireGroup(); + throw e; + } - logging.log(errorMessage); - const error = new Error(errorMessage); - throw error; - } else { - // Group validations - if (transactionWraps.length > 1) { - if ( - !transactionWraps.every( - (wrap) => transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID - ) - ) { - const e = new NoDifferentLedgers(); - logging.log(`Validation failed. ${e}`); - throw e; - } + if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { + const e = new NonMatchingGroup(); + throw e; + } + + const recreatedGroupTxs = algosdk.assignGroupID( + rawTxArray.slice().map((tx) => { + delete tx.group; + return tx; + }) + ); + const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); + if (groupId !== recalculatedGroupID) { + const e = new IncompleteOrDisorderedGroup(); + throw e; + } - const groupId = transactionWraps[0].transaction.group; - if (!groupId) { - const e = new MultipleTxsRequireGroup(); - logging.log(`Validation failed. ${e}`); - throw e; + // If the whole group is provided and verified, we mark the group field as valid instead of dangerous + transactionWraps.forEach((wrap) => { + wrap.validityObject['group'] = new ValidationResponse({ + status: ValidationStatus.Valid, + }); + }); + } else { + const wrap = transactionWraps[0]; + if ( + (!wrap.msigData && wrap.signers) || + (wrap.msigData && wrap.signers && !wrap.signers.length) + ) { + const e = new InvalidSigners(); + throw e; + } } - if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { - const e = new NonMatchingGroup(); - logging.log(`Validation failed. ${e}`); - throw e; + for (let i = 0; i < transactionWraps.length; i++) { + const wrap = transactionWraps[i]; + await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); } - const recreatedGroupTxs = algosdk.assignGroupID( - rawTxArray.slice().map((tx) => { - delete tx.group; - return tx; - }) + request.body.params.transactionWraps = transactionWraps; + + console.log('===== FINISH PREPARING UI ====='); + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), + ...signPopupProperties, + }, + function (w) { + if (w) { + Task.requests[request.originTabID] = { + window_id: w.id, + message: request, + }; + // Send message with tx info + setTimeout(function () { + extensionBrowser.runtime.sendMessage(request); + }, 500); + } + } ); - const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); - if (groupId !== recalculatedGroupID) { - const e = new IncompleteOrDisorderedGroup(); - logging.log(`Validation failed. ${e}`); - throw e; - } + } + } catch (e) { + let errorMessage = 'There was a problem validating the transaction(s): '; - // If the whole group is provided and verified, we mark the group field as valid instead of dangerous - transactionWraps.forEach((wrap) => { - wrap.validityObject['group'] = new ValidationResponse({ - status: ValidationStatus.Valid, - }); - }); + if (groupsToSign.length === 1) { + errorMessage += e.message; } else { - const wrap = transactionWraps[0]; - if ( - (!wrap.msigData && wrap.signers) || - (wrap.msigData && wrap.signers && !wrap.signers.length) - ) { - const e = new InvalidSigners(); - logging.log(`Validation failed. ${e}`); - throw e; - } + errorMessage += `\nOn group ${currentGroup}: [${e.message}].`; } + logging.log(errorMessage); - for (let i = 0; i < transactionWraps.length; i++) { - const wrap = transactionWraps[i]; - await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); - } + const newRequest = Object.assign({}, request); + newRequest.error = { message: errorMessage }; + newRequest.body.method = JsonRpcMethod.SignWalletTransaction; - d.body.params.transactionWraps = transactionWraps; + console.log('======== BEFORE THROW ========'); + console.log(request); + console.log(newRequest); - console.log('===== FINISH PREPARING UI ====='); - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), - ...signPopupProperties, - }, - function (w) { - if (w) { - Task.requests[d.originTabID] = { - window_id: w.id, - message: d, - }; - // Send message with tx info - setTimeout(function () { - extensionBrowser.runtime.sendMessage(d); - }, 500); - } - } - ); + // Clean class saved request + delete Task.requests[request.originTabID]; + // sendResponse(request); + MessageApi.send(newRequest); } }, // sign-allow-wallet-tx @@ -1336,9 +1338,12 @@ export class Task { console.log(message); // In case of signing error, we abort everything. if (message.error) { + const newRequest = Object.assign({}, message); + newRequest.body.method = JsonRpcMethod.HandleWalletTransactions; // Clean class saved request delete Task.requests[responseOriginTabID]; - MessageApi.send(message); + MessageApi.send(newRequest); + return; } // We check if there are more groups to sign @@ -1367,7 +1372,13 @@ export class Task { return; } } else { - message.response = signedGroups; + let response; + if (signedGroups.length === 1) { + response = signedGroups[0]; + } else { + response = signedGroups; + } + message.response = response; // Clean class saved request delete Task.requests[responseOriginTabID]; MessageApi.send(message); @@ -1380,18 +1391,23 @@ export class Task { } return true; }, + // sign-deny /* eslint-disable-next-line no-unused-vars */ [JsonRpcMethod.SignDeny]: (request: any, sendResponse: Function) => { const { responseOriginTabID } = request.body.params; const auth = Task.requests[responseOriginTabID]; const message = auth.message; + console.log('========== BEGIN DENY ==========='); + console.log(request); + console.log(message); auth.message.error = { message: RequestErrors.NotAuthorized, }; extensionBrowser.windows.remove(auth.window_id); delete Task.requests[responseOriginTabID]; + console.log('========== FINISH DENY ==========='); setTimeout(() => { MessageApi.send(message); }, 100); From 503a0c5cc7ae0cc88fd1ddc9a7bff796aa651c7b Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Thu, 30 Sep 2021 16:28:50 -0300 Subject: [PATCH 4/7] Remove innecesary RPC Method --- packages/common/src/messaging/types.ts | 1 - packages/dapp/src/fn/task.ts | 2 +- .../src/background/messaging/task.ts | 506 +++++++++--------- 3 files changed, 247 insertions(+), 262 deletions(-) diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index 9f461b53..c4e8ea55 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -14,7 +14,6 @@ export enum JsonRpcMethod { SignDeny = 'sign-deny', SignTransaction = 'sign-transaction', SignMultisigTransaction = 'sign-multisig-transaction', - HandleWalletTransactions = 'handle-wallet-transactions', SignWalletTransaction = 'sign-wallet-transaction', SendTransaction = 'send-transaction', Algod = 'algod', diff --git a/packages/dapp/src/fn/task.ts b/packages/dapp/src/fn/task.ts index 00ec0b52..3d7db41c 100644 --- a/packages/dapp/src/fn/task.ts +++ b/packages/dapp/src/fn/task.ts @@ -78,6 +78,6 @@ export class Task extends Runtime implements ITask { const params = { transactionsOrGroups: transactionsOrGroups, }; - return MessageBuilder.promise(JsonRpcMethod.HandleWalletTransactions, params, error); + return MessageBuilder.promise(JsonRpcMethod.SignWalletTransaction, params, error); } } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index cac5de8d..2aead57d 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -180,6 +180,242 @@ export class Task { } } + // Intermediate function for Group of Groups handling + private static signIndividualGroup = async (request: any) => { + console.log('===== START PREPARING UI ====='); + console.log(request); + const groupsToSign: Array> = request.body.params.groupsToSign; + const currentGroup: number = request.body.params.currentGroup; + const walletTransactions: Array = groupsToSign[currentGroup]; + const rawTxArray: Array = []; + const processedTxArray: Array = []; + const transactionWraps: Array = []; + const validationErrors: Array = []; + try { + walletTransactions.forEach((walletTx, index) => { + try { + // Runtime type checking + if ( + // prettier-ignore + (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || + (walletTx.message != null && typeof walletTx.message !== 'string') || + (!walletTx.txn || typeof walletTx.txn !== 'string') || + (walletTx.signers != null && + ( + !Array.isArray(walletTx.signers) || + (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) + ) + ) || + (walletTx.msig && typeof walletTx.msig !== 'object') + ) { + logging.log('Invalid Wallet Transaction Structure'); + throw new InvalidStructure(); + } else if ( + // prettier-ignore + walletTx.msig && ( + (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || + (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || + ( + !walletTx.msig.addrs || + !Array.isArray(walletTx.msig.addrs) || + (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) + ) + ) + ) { + logging.log('Invalid Wallet Transaction Multisig Structure'); + throw new InvalidMsigStructure(); + } + + /** + * In order to process the transaction and make it compatible with our validator, we: + * 0) Decode from base64 to Uint8Array msgpack + * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack + * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours + * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays + */ + const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); + rawTxArray[index] = rawTx; + const processedTx = rawTx._getDictForDisplay(); + processedTxArray[index] = processedTx; + const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); + transactionWraps[index] = wrap; + const genesisID = wrap.transaction.genesisID; + + const signers = walletTransactions[index].signers; + const msigData = walletTransactions[index].msig; + wrap.msigData = msigData; + wrap.signers = signers; + if (msigData) { + if (signers && signers.length) { + signers.forEach((address) => { + InternalMethods.checkValidAccount(genesisID, address); + }); + } + wrap.msigData = msigData; + } else { + if (!signers) { + InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); + } + } + + return wrap; + } catch (e) { + validationErrors[index] = e; + } + }); + + if ( + validationErrors.length || + !transactionWraps.length || + transactionWraps.some((w) => w === undefined) + ) { + // We don't have transaction wraps or we have an building error, reject the transaction. + let errorMessage = ''; + let validationMessages = ''; + + validationErrors.forEach((err, index) => { + validationMessages = + validationMessages + + `\nValidation failed for transaction ${index} due to: ${err.message}`; + }); + errorMessage += + (validationMessages.length && validationMessages) || + 'Please verify the properties are valid.'; + const error = new Error(errorMessage); + throw error; + } else if ( + transactionWraps.some( + (tx) => + tx.validityObject && + Object.values(tx.validityObject).some( + (value) => value['status'] === ValidationStatus.Invalid + ) + ) + ) { + // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. + // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. + const invalidKeys = {}; + transactionWraps.forEach((tx, index) => { + invalidKeys[index] = []; + Object.entries(tx.validityObject).forEach(([key, value]) => { + if (value['status'] === ValidationStatus.Invalid) { + invalidKeys[index].push(`${key}: ${value['info']}`); + } + }); + if (!invalidKeys[index].length) delete invalidKeys[index]; + }); + + let errorMessage = ''; + + Object.keys(invalidKeys).forEach((index) => { + errorMessage = + errorMessage + + `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ + index + ].join(', ')}]. `; + }); + + const error = new Error(errorMessage); + throw error; + } else { + // Group validations + if (transactionWraps.length > 1) { + if ( + !transactionWraps.every( + (wrap) => + transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID + ) + ) { + const e = new NoDifferentLedgers(); + throw e; + } + + const groupId = transactionWraps[0].transaction.group; + if (!groupId) { + const e = new MultipleTxsRequireGroup(); + throw e; + } + + if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { + const e = new NonMatchingGroup(); + throw e; + } + + const recreatedGroupTxs = algosdk.assignGroupID( + rawTxArray.slice().map((tx) => { + delete tx.group; + return tx; + }) + ); + const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); + if (groupId !== recalculatedGroupID) { + const e = new IncompleteOrDisorderedGroup(); + throw e; + } + + // If the whole group is provided and verified, we mark the group field as valid instead of dangerous + transactionWraps.forEach((wrap) => { + wrap.validityObject['group'] = new ValidationResponse({ + status: ValidationStatus.Valid, + }); + }); + } else { + const wrap = transactionWraps[0]; + if ( + (!wrap.msigData && wrap.signers) || + (wrap.msigData && wrap.signers && !wrap.signers.length) + ) { + const e = new InvalidSigners(); + throw e; + } + } + + for (let i = 0; i < transactionWraps.length; i++) { + const wrap = transactionWraps[i]; + await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); + } + + request.body.params.transactionWraps = transactionWraps; + + console.log('===== FINISH PREPARING UI ====='); + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), + ...signPopupProperties, + }, + function (w) { + if (w) { + Task.requests[request.originTabID] = { + window_id: w.id, + message: request, + }; + // Send message with tx info + setTimeout(function () { + extensionBrowser.runtime.sendMessage(request); + }, 500); + } + } + ); + } + } catch (e) { + let errorMessage = 'There was a problem validating the transaction(s): '; + + if (groupsToSign.length === 1) { + errorMessage += e.message; + } else { + errorMessage += `\nOn group ${currentGroup}: [${e.message}].`; + } + logging.log(errorMessage); + + request.error = { message: errorMessage }; + + // Clean class saved request + delete Task.requests[request.originTabID]; + // sendResponse(request); + MessageApi.send(request); + } + } + public static methods(): { [key: string]: { [JsonRpcMethod: string]: Function; @@ -434,7 +670,7 @@ export class Task { } }, // handle-wallet-transactions - [JsonRpcMethod.HandleWalletTransactions]: async (d: any, resolve: Function, reject: Function) => { + [JsonRpcMethod.SignWalletTransaction]: async (d: any, resolve: Function, reject: Function) => { console.log(d); console.log('===== START NESTED HANDLING ====='); const transactionsOrGroups: Array | Array> = @@ -449,7 +685,6 @@ export class Task { walletTx.txn && walletTx.txn.length ); - console.log('after single'); const multipleGroups = (transactionsOrGroups as Array>).every( (walletTxArray) => Array.isArray(walletTxArray) && @@ -463,7 +698,6 @@ export class Task { walletTx.txn.length ) ); - console.log('after multiple'); console.log(`Single: ${singleGroup}, Multiple: ${multipleGroups}`); // If none of the formats match up, we throw an error @@ -482,16 +716,13 @@ export class Task { } console.log(groupsToSign); - const newRequest = Object.assign({}, d); - - newRequest.body.method = JsonRpcMethod.SignWalletTransaction; - newRequest.body.params.groupsToSign = groupsToSign; - newRequest.body.params.currentGroup = 0; - newRequest.body.params.signedGroups = []; + d.body.params.groupsToSign = groupsToSign; + d.body.params.currentGroup = 0; + d.body.params.signedGroups = []; console.log('===== FINISH NESTED HANDLING ====='); - this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); + Task.signIndividualGroup(d); }, // send-transaction [JsonRpcMethod.SendTransaction]: (d: any, resolve: Function, reject: Function) => { @@ -941,247 +1172,6 @@ export class Task { } return true; }, - // sign-wallet-transaction - [JsonRpcMethod.SignWalletTransaction]: async (request: any, sendResponse: Function) => { - console.log('===== START PREPARING UI ====='); - console.log(request); - const groupsToSign: Array> = request.body.params.groupsToSign; - const currentGroup: number = request.body.params.currentGroup; - const walletTransactions: Array = groupsToSign[currentGroup]; - const rawTxArray: Array = []; - const processedTxArray: Array = []; - const transactionWraps: Array = []; - const validationErrors: Array = []; - try { - walletTransactions.forEach((walletTx, index) => { - try { - // Runtime type checking - if ( - // prettier-ignore - (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || - (walletTx.message != null && typeof walletTx.message !== 'string') || - (!walletTx.txn || typeof walletTx.txn !== 'string') || - (walletTx.signers != null && - ( - !Array.isArray(walletTx.signers) || - (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) - ) - ) || - (walletTx.msig && typeof walletTx.msig !== 'object') - ) { - logging.log('Invalid Wallet Transaction Structure'); - throw new InvalidStructure(); - } else if ( - // prettier-ignore - walletTx.msig && ( - (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || - (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || - ( - !walletTx.msig.addrs || - !Array.isArray(walletTx.msig.addrs) || - (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) - ) - ) - ) { - logging.log('Invalid Wallet Transaction Multisig Structure'); - throw new InvalidMsigStructure(); - } - - /** - * In order to process the transaction and make it compatible with our validator, we: - * 0) Decode from base64 to Uint8Array msgpack - * 1) Use the 'decodeUnsignedTransaction' method of the SDK to parse the msgpack - * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours - * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays - */ - const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); - rawTxArray[index] = rawTx; - const processedTx = rawTx._getDictForDisplay(); - processedTxArray[index] = processedTx; - const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); - transactionWraps[index] = wrap; - const genesisID = wrap.transaction.genesisID; - - const signers = walletTransactions[index].signers; - const msigData = walletTransactions[index].msig; - wrap.msigData = msigData; - wrap.signers = signers; - if (msigData) { - if (signers && signers.length) { - signers.forEach((address) => { - InternalMethods.checkValidAccount(genesisID, address); - }); - } - wrap.msigData = msigData; - } else { - if (!signers) { - InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); - } - } - - return wrap; - } catch (e) { - validationErrors[index] = e; - } - }); - - if ( - validationErrors.length || - !transactionWraps.length || - transactionWraps.some((w) => w === undefined) - ) { - // We don't have transaction wraps or we have an building error, reject the transaction. - let errorMessage = ''; - let validationMessages = ''; - - validationErrors.forEach((err, index) => { - validationMessages = - validationMessages + - `\nValidation failed for transaction ${index} due to: ${err.message}`; - }); - errorMessage += - (validationMessages.length && validationMessages) || - 'Please verify the properties are valid.'; - const error = new Error(errorMessage); - throw error; - } else if ( - transactionWraps.some( - (tx) => - tx.validityObject && - Object.values(tx.validityObject).some( - (value) => value['status'] === ValidationStatus.Invalid - ) - ) - ) { - // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. - const invalidKeys = {}; - transactionWraps.forEach((tx, index) => { - invalidKeys[index] = []; - Object.entries(tx.validityObject).forEach(([key, value]) => { - if (value['status'] === ValidationStatus.Invalid) { - invalidKeys[index].push(`${key}: ${value['info']}`); - } - }); - if (!invalidKeys[index].length) delete invalidKeys[index]; - }); - - let errorMessage = ''; - - Object.keys(invalidKeys).forEach((index) => { - errorMessage = - errorMessage + - `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ - index - ].join(', ')}]. `; - }); - - const error = new Error(errorMessage); - throw error; - } else { - // Group validations - if (transactionWraps.length > 1) { - if ( - !transactionWraps.every( - (wrap) => - transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID - ) - ) { - const e = new NoDifferentLedgers(); - throw e; - } - - const groupId = transactionWraps[0].transaction.group; - if (!groupId) { - const e = new MultipleTxsRequireGroup(); - throw e; - } - - if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { - const e = new NonMatchingGroup(); - throw e; - } - - const recreatedGroupTxs = algosdk.assignGroupID( - rawTxArray.slice().map((tx) => { - delete tx.group; - return tx; - }) - ); - const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); - if (groupId !== recalculatedGroupID) { - const e = new IncompleteOrDisorderedGroup(); - throw e; - } - - // If the whole group is provided and verified, we mark the group field as valid instead of dangerous - transactionWraps.forEach((wrap) => { - wrap.validityObject['group'] = new ValidationResponse({ - status: ValidationStatus.Valid, - }); - }); - } else { - const wrap = transactionWraps[0]; - if ( - (!wrap.msigData && wrap.signers) || - (wrap.msigData && wrap.signers && !wrap.signers.length) - ) { - const e = new InvalidSigners(); - throw e; - } - } - - for (let i = 0; i < transactionWraps.length; i++) { - const wrap = transactionWraps[i]; - await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); - } - - request.body.params.transactionWraps = transactionWraps; - - console.log('===== FINISH PREPARING UI ====='); - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), - ...signPopupProperties, - }, - function (w) { - if (w) { - Task.requests[request.originTabID] = { - window_id: w.id, - message: request, - }; - // Send message with tx info - setTimeout(function () { - extensionBrowser.runtime.sendMessage(request); - }, 500); - } - } - ); - } - } catch (e) { - let errorMessage = 'There was a problem validating the transaction(s): '; - - if (groupsToSign.length === 1) { - errorMessage += e.message; - } else { - errorMessage += `\nOn group ${currentGroup}: [${e.message}].`; - } - logging.log(errorMessage); - - const newRequest = Object.assign({}, request); - newRequest.error = { message: errorMessage }; - newRequest.body.method = JsonRpcMethod.SignWalletTransaction; - - console.log('======== BEFORE THROW ========'); - console.log(request); - console.log(newRequest); - - // Clean class saved request - delete Task.requests[request.originTabID]; - // sendResponse(request); - MessageApi.send(newRequest); - } - }, // sign-allow-wallet-tx [JsonRpcMethod.SignAllowWalletTx]: (request: any, sendResponse: Function) => { const { passphrase, responseOriginTabID } = request.body.params; @@ -1338,26 +1328,22 @@ export class Task { console.log(message); // In case of signing error, we abort everything. if (message.error) { - const newRequest = Object.assign({}, message); - newRequest.body.method = JsonRpcMethod.HandleWalletTransactions; // Clean class saved request delete Task.requests[responseOriginTabID]; - MessageApi.send(newRequest); + MessageApi.send(message); return; } // We check if there are more groups to sign - const newRequest = Object.assign({}, message); - newRequest.body.method = JsonRpcMethod.SignWalletTransaction; - newRequest.body.params.currentGroup = currentGroup + 1; - newRequest.body.params.signedGroups = signedGroups; + message.body.params.currentGroup = currentGroup + 1; + message.body.params.signedGroups = signedGroups; console.log('Accumulated groups after sign:'); console.log(signedGroups); console.log('===== FINISH BACKGROUND SIGNING ====='); - if (newRequest.body.params.currentGroup < groupsToSign.length) { + if (message.body.params.currentGroup < groupsToSign.length) { try { - this.methods()['extension'][JsonRpcMethod.SignWalletTransaction](newRequest); + Task.signIndividualGroup(message); } catch (e) { let errorMessage = 'There was a problem validating the transaction(s). '; From 198627a67855f2fee18cb9ea46621a5cbba52337 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Thu, 30 Sep 2021 16:30:51 -0300 Subject: [PATCH 5/7] Clean up Console Logs --- .../src/background/messaging/task.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 2aead57d..aca50fb2 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -182,8 +182,6 @@ export class Task { // Intermediate function for Group of Groups handling private static signIndividualGroup = async (request: any) => { - console.log('===== START PREPARING UI ====='); - console.log(request); const groupsToSign: Array> = request.body.params.groupsToSign; const currentGroup: number = request.body.params.currentGroup; const walletTransactions: Array = groupsToSign[currentGroup]; @@ -377,7 +375,6 @@ export class Task { request.body.params.transactionWraps = transactionWraps; - console.log('===== FINISH PREPARING UI ====='); extensionBrowser.windows.create( { url: extensionBrowser.runtime.getURL('index.html#/sign-v2-transaction'), @@ -671,12 +668,9 @@ export class Task { }, // handle-wallet-transactions [JsonRpcMethod.SignWalletTransaction]: async (d: any, resolve: Function, reject: Function) => { - console.log(d); - console.log('===== START NESTED HANDLING ====='); const transactionsOrGroups: Array | Array> = d.body.params.transactionsOrGroups; - console.log(transactionsOrGroups); // We check to see if it's a simple or nested array of transactions const singleGroup = (transactionsOrGroups as Array).every( (walletTx) => @@ -698,7 +692,6 @@ export class Task { walletTx.txn.length ) ); - console.log(`Single: ${singleGroup}, Multiple: ${multipleGroups}`); // If none of the formats match up, we throw an error if (!singleGroup && !multipleGroups) { @@ -714,14 +707,10 @@ export class Task { if (multipleGroups) { groupsToSign = transactionsOrGroups as Array>; } - console.log(groupsToSign); - d.body.params.groupsToSign = groupsToSign; d.body.params.currentGroup = 0; d.body.params.signedGroups = []; - console.log('===== FINISH NESTED HANDLING ====='); - Task.signIndividualGroup(d); }, // send-transaction @@ -1186,15 +1175,6 @@ export class Task { algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)) ); - console.log('===== START BACKGROUND SIGNING ====='); - console.log(`Current group: ${currentGroup}, Single: ${singleGroup}`); - console.log(`UI ID: ${responseOriginTabID}`); - console.log(auth); - console.log(walletTransactions); - - // const groupsToSign: Array> = request.body.params.groupsToSign; - // const currentGroup: number = d.body.params.currentGroup; - const signedTxs = []; const signErrors = []; @@ -1324,8 +1304,6 @@ export class Task { signedGroups[currentGroup] = signedTxs; } - console.log('After error processing'); - console.log(message); // In case of signing error, we abort everything. if (message.error) { // Clean class saved request @@ -1337,10 +1315,6 @@ export class Task { // We check if there are more groups to sign message.body.params.currentGroup = currentGroup + 1; message.body.params.signedGroups = signedGroups; - console.log('Accumulated groups after sign:'); - console.log(signedGroups); - console.log('===== FINISH BACKGROUND SIGNING ====='); - if (message.body.params.currentGroup < groupsToSign.length) { try { Task.signIndividualGroup(message); @@ -1384,16 +1358,12 @@ export class Task { const auth = Task.requests[responseOriginTabID]; const message = auth.message; - console.log('========== BEGIN DENY ==========='); - console.log(request); - console.log(message); auth.message.error = { message: RequestErrors.NotAuthorized, }; extensionBrowser.windows.remove(auth.window_id); delete Task.requests[responseOriginTabID]; - console.log('========== FINISH DENY ==========='); setTimeout(() => { MessageApi.send(message); }, 100); From cc7c86a8387f110b6b94b119a8b676687ed112f4 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Fri, 1 Oct 2021 15:58:29 -0300 Subject: [PATCH 6/7] Update e2e tests --- .../background/messaging/internalMethods.ts | 3 + packages/test-project/package.json | 1 + packages/test-project/tests/common/tests.js | 17 ++- .../test-project/tests/dapp-groups.test.js | 124 ++++++++++++++++++ .../test-project/tests/dapp-signtxn.test.js | 17 +-- 5 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 packages/test-project/tests/dapp-groups.test.js diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 726d078e..50bf87ea 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -311,6 +311,7 @@ export class InternalMethods { ); return true; } + public static [JsonRpcMethod.LedgerGetSessionTxn](request: any, sendResponse: Function) { if (session.txnWrap && 'body' in session.txnWrap) { // The transaction may contain source and JSONRPC info, the body.params will be the transaction validation object @@ -455,6 +456,7 @@ export class InternalMethods { }); return true; } + public static [JsonRpcMethod.AccountDetails](request: any, sendResponse: Function) { const { ledger, address } = request.body.params; const algod = this.getAlgod(ledger); @@ -935,6 +937,7 @@ export class InternalMethods { sendResponse({ error: e.message }); } } + public static [JsonRpcMethod.GetLedgers](request: any, sendResponse: Function) { getAvailableLedgersExt((availableLedgers) => { sendResponse(availableLedgers); diff --git a/packages/test-project/package.json b/packages/test-project/package.json index 1459f8c6..448104a8 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -21,6 +21,7 @@ "dapp": "jest --group=dapp --group=-dapp-storage", "dapp/multisig": "jest --group=dapp/multisig", "dapp/signtxn": "jest --group=dapp/signtxn", + "dapp/groups": "jest --group=dapp/groups", "coveragetest": "jest --coverage=true --coverageDirectory ../test-project/coverage --projects ../crypto ../extension ../storage ../common ../dapp --runInBand && bash -c \"start chrome \"$(realpath ./coverage/lcov-report/index.html\"\")", "test": "jest -i --group=-github --group=-dapp-storage" } diff --git a/packages/test-project/tests/common/tests.js b/packages/test-project/tests/common/tests.js index efa66992..3e67c939 100644 --- a/packages/test-project/tests/common/tests.js +++ b/packages/test-project/tests/common/tests.js @@ -119,7 +119,7 @@ async function ConnectAlgoSigner() { await popup.waitForTimeout(250); } } catch (e) { - // Single transaction + // Maybe a Single transaction } await popup.waitForSelector('#approveTx'); @@ -130,6 +130,21 @@ async function ConnectAlgoSigner() { await popup.click('#authButton'); } await dappPage.exposeFunction('authorizeSignTxn', authorizeSignTxn); + + // Groups of Groups Approvals + async function authorizeSignTxnGroups(amount) { + const popup = await getPopup(); + for (let i = 0; i < amount; i++) { + try { + await authorizeSignTxn(); + await popup.waitForTimeout(2000); + } catch (e) { + console.log('Error:'); + console.log(e); + } + } + } + await dappPage.exposeFunction('authorizeSignTxnGroups', authorizeSignTxnGroups); }); test('Connect Dapp through content.js', async () => { diff --git a/packages/test-project/tests/dapp-groups.test.js b/packages/test-project/tests/dapp-groups.test.js new file mode 100644 index 00000000..3987296d --- /dev/null +++ b/packages/test-project/tests/dapp-groups.test.js @@ -0,0 +1,124 @@ +/** + * dapp e2e tests for the AlgoSigner V2 Signing functionality + * + * @group dapp/groups + */ + +const algosdk = require('algosdk'); +const { accounts } = require('./common/constants'); +const { + openExtension, + getLedgerSuggestedParams, + byteArrayToBase64, +} = require('./common/helpers'); +const { CreateWallet, ConnectAlgoSigner, ImportAccount } = require('./common/tests'); + +const account = accounts.ui; + +let ledgerParams; + +const buildSdkTx = (tx) => { + return new algosdk.Transaction(tx); +}; + +const prepareWalletTx = (tx) => { + return { + txn: byteArrayToBase64(tx.toByte()), + }; +}; + +async function signTxnGroups(transactionsToSign) { + await dappPage.waitForTimeout(2000); + const signedGroups = await dappPage.evaluate( + async (transactionsToSign) => { + const signPromise = AlgoSigner.signTxn(transactionsToSign) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + + const amountOfGroups = Array.isArray(transactionsToSign[0]) ? transactionsToSign.length : 1; + await window['authorizeSignTxnGroups'](amountOfGroups); + + return await Promise.resolve(signPromise); + }, + transactionsToSign, + ); + + signedGroups.forEach(async (group) => { + await expect(group.filter((i) => i).length).toBeGreaterThan(0); + group.forEach(async (signedTx) => { + if (signedTx) { + await expect(signedTx).toHaveProperty('txID'); + await expect(signedTx).toHaveProperty('blob'); + } + }); + }); + return signedGroups; +} + +describe('Wallet Setup', () => { + beforeAll(async () => { + await openExtension(); + }); + + CreateWallet(); + + ImportAccount(account); +}); + +describe('dApp Setup', () => { + ConnectAlgoSigner(); + + test('Get TestNet params', async () => { + ledgerParams = await getLedgerSuggestedParams(); + }); +}); + +describe('Group of Groups Use cases', () => { + let tx1, tx2, tx3, tx4; + + test('Group of Grouped Transactions', async () => { + tx1 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + tx2 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + }); + tx3 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + tx4 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + }); + + jest.setTimeout(30000); + const group1 = await algosdk.assignGroupID([tx1, tx2]).map((txn) => prepareWalletTx(txn)); + group1[1].signers = []; + const group2 = await algosdk.assignGroupID([tx3, tx4]).map((txn) => prepareWalletTx(txn)); + + const signedTransactions = await signTxnGroups([group1, group2]); + await expect(signedTransactions).toHaveLength(2); + await expect(signedTransactions[0][1]).toBeNull(); + }); +}); diff --git a/packages/test-project/tests/dapp-signtxn.test.js b/packages/test-project/tests/dapp-signtxn.test.js index be40b6e2..00bd63d0 100644 --- a/packages/test-project/tests/dapp-signtxn.test.js +++ b/packages/test-project/tests/dapp-signtxn.test.js @@ -32,7 +32,7 @@ const prepareWalletTx = (tx) => { }; }; -async function signTxn(transactions, testFunction) { +async function signTxn(transactionsToSign, testFunction) { const timestampedName = `popupTest-${new Date().getTime().toString()}`; if (testFunction) { await dappPage.exposeFunction(timestampedName, async () => { @@ -46,8 +46,8 @@ async function signTxn(transactions, testFunction) { await dappPage.waitForTimeout(2000); const signedTransactions = await dappPage.evaluate( - async (transactions, testFunction, testTimestamp) => { - const signPromise = AlgoSigner.signTxn(transactions) + async (transactionsToSign, testFunction, testTimestamp) => { + const signPromise = AlgoSigner.signTxn(transactionsToSign) .then((data) => { return data; }) @@ -58,10 +58,11 @@ async function signTxn(transactions, testFunction) { if (testFunction) { await window[testTimestamp](); } - await window.authorizeSignTxn(); + + await window['authorizeSignTxn'](); return await Promise.resolve(signPromise); }, - transactions, + transactionsToSign, !!testFunction, timestampedName ); @@ -210,7 +211,7 @@ describe('Single and Global Transaction Use cases', () => { expect(decodedTransaction).toHaveProperty('txn'); expect(decodedTransaction).toHaveProperty('msig'); expect(decodedTransaction.msig).toHaveProperty('subsig'); - expect(decodedTransaction.msig.subsig.length).toBe(3); + expect(decodedTransaction.msig.subsig).toHaveLength(3); expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); expect(decodedTransaction.msig.subsig[1]).toHaveProperty('s'); expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); @@ -225,7 +226,7 @@ describe('Single and Global Transaction Use cases', () => { expect(decodedTransaction).toHaveProperty('txn'); expect(decodedTransaction).toHaveProperty('msig'); expect(decodedTransaction.msig).toHaveProperty('subsig'); - expect(decodedTransaction.msig.subsig.length).toBe(3); + expect(decodedTransaction.msig.subsig).toHaveLength(3); expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); expect(decodedTransaction.msig.subsig[1]).not.toHaveProperty('s'); expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); @@ -263,7 +264,7 @@ describe('Group Transactions Use cases', () => { const signedTransactions = await signTxn(unsignedTransactions); await expect(signedTransactions[2]).toBeNull(); - await expect(signedTransactions.filter((i) => i).length).toBe(2); + await expect(signedTransactions.filter((i) => i)).toHaveLength(2); }); // @TODO: Add errors for mismatches, incomplete groups, etc From 5ce8778fcc432995d0bc0014fd39e4b4712927fc Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Fri, 1 Oct 2021 16:36:07 -0300 Subject: [PATCH 7/7] Patch 1.6.5 --- README.md | 6 +++++ docs/dApp-integration.md | 39 +++++++++++++++++++++++++++++- package.json | 2 +- packages/common/package.json | 2 +- packages/crypto/package.json | 2 +- packages/dapp/package.json | 2 +- packages/extension/manifest.json | 2 +- packages/extension/package.json | 2 +- packages/storage/package.json | 2 +- packages/test-project/package.json | 2 +- packages/ui/package.json | 2 +- 11 files changed, 53 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2e852bdb..d6e9e3ba 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ Added ability to use a Ledger device to add public addresses into AlgoSigner and - Allow for zero Algo pay transactions in UI - Fix stall for `Axfer` in txs list when decimals can't be obtained from the asset +### 1.6.5 Patch +- Fixed duplicate Port on Custom Network calls +- Added support for receiving an array of arrays when using `signTxn()` + - Each inner array should still contain transactions **belonging to a same group** + - Updated error message when providing an incorrect format of transactions to reflect this change + ## New Users - Watch [Getting Started with AlgoSigner](https://youtu.be/tG-xzG8r770) diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index 675e6d74..fa74fa29 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -181,7 +181,7 @@ AlgoSigner.signTxn([ ]); ``` -**NOTE:** Even though the method accepts an array of transactions, it requires atomic transactions with groupId and will error on non-atomic groups. +**NOTE:** Even though the method accepts an array of transactions, it requires atomic transactions that have a groupId and will error on non-atomic groups. **Response** @@ -221,6 +221,42 @@ let signedTxs = await AlgoSigner.signTxn([ }, ]); ``` +Alternatively, you can provide multiple arrays of transactions at once. Same rules regarding the contents of the groups apply. + +**Request** + +```js +AlgoSigner.signTxn([ + [ + { + txn: 'iqNhbXRko2ZlZc0D6KJmds4A259Go2dlbqx0ZXN0bmV0LXYxLjCiZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToio2dycMQgdsLAGqgrtwqqQS4UEN7O8CZHjfhPTwLHrB1A2pXwvKGibHbOANujLqNyY3bEIK0TEDcptY0uFvk2V5LDVzRfdz7O4freYHEuZbpI+6hMo3NuZMQglmyhKUPeU2KALzt/Jcs0GQ55k2vsqZ4pGeNlzpnYLbukdHlwZaNwYXk=', + }, + ], + [ + { + txn: 'iaRhZnJ6w6RmYWRkxCCWbKEpQ95TYoAvO38lyzQZDnmTa+ypnikZ42XOmdgtu6RmYWlkzgDITmiiZnbOAQNjIKNnZW6sdGVzdG5ldC12MS4womdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4BA2cIo3NuZMQglmyhKUPeU2KALzt/Jcs0GQ55k2vsqZ4pGeNlzpnYLbukdHlwZaRhZnJ6', + }, + ], +]); +``` +**Response** + +```json +[ + [ + { + "txID": "4F6GE5EBTBJ7DOTWKA3GK4JYARFDCVR5CYEXP6O27FUCE5SGFDYQ", + "blob": "gqNzaWfEQL6mW/7ss2HKAqsuHN/7ePx11wKSAvFocw5QEDvzSvrvJdzWYvT7ua8Lc0SS0zOmUDDaHQC/pGJ0PNqnu7W3qQKjdHhuiaNhbXQGo2ZlZc4AA7U4omZ2zgB+OrujZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAH4+o6NyY3bEIHhydylNDQQhpD9QdKWejLCMBgb5UYJTGCfDW3KgLsI+o3NuZMQgZM5ZNuFgR8pz2dHBgDlmHolfGgF96zX/X4x2bnAJ3aqkdHlwZaNwYXk=" + } + ], + [ + { + "txID": "QK4XYJYGN7CLER25SKT3DV4UWNI5DVYXRBJRZEUWYA523EU5ZB7A", + "blob": "gqNzaWfEQC8ZIPYimAypJD2TmEQjuWxEEk8/gJbBegEHdtyKr6TuA78otKIEB9PYQimgMLGn87YOEB6GgRe5vjWRTuWGsAqjdHhuiaRhZnJ6w6RmYWRkxCCWbKEpQ95TYoAvO38lyzQZDnmTa+ypnikZ42XOmdgtu6RmYWlkzgDITmiiZnbOAQNjO6NnZW6sdGVzdG5ldC12MS4womdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4BA2cjo3NuZMQglmyhKUPeU2KALzt/Jcs0GQ55k2vsqZ4pGeNlzpnYLbukdHlwZaRhZnJ6" + } + ] +] +``` The signed transactions can then be sent using the SDK (example below) or using the [AlgoSigner.send()](#algosignersend-ledger-mainnettestnet-txblob-) method. @@ -439,6 +475,7 @@ The dApp may return the following errors in case of users rejecting requests, or InvalidTransactionParams = '[RequestErrors.InvalidTransactionParams] Invalid transaction parameters.', UnsupportedAlgod = '[RequestErrors.UnsupportedAlgod] The provided method is not supported.', UnsupportedLedger = '[RequestErrors.UnsupportedLedger] The provided ledger is not supported.', + InvalidFormat = '[RequestErrors.InvalidFormat] Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', Undefined = '[RequestErrors.Undefined] An undefined error occurred.', ``` diff --git a/package.json b/package.json index d98d2852..86053789 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosigner", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/common/package.json b/packages/common/package.json index 2113e669..13a47411 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/common", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "description": "Common library functions for AlgoSigner.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/crypto/package.json b/packages/crypto/package.json index d6b8ae7c..e60f99fe 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-crypto", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "description": "Cryptographic wrapper for saving and retrieving extention information in Algosigner.", "repository": { diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 713f75ed..54b43e0c 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/dapp", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index f37aa1cf..0475e1d1 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "AlgoSigner", "author": "https://developer.purestake.io", - "version": "1.6.4", + "version": "1.6.5", "description": "Algorand Wallet Extension | Send & Receive ALGOs | Sign dApp Transactions", "icons": { "48": "icon.png" diff --git a/packages/extension/package.json b/packages/extension/package.json index c82a98eb..0e29883d 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-extension", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/storage/package.json b/packages/storage/package.json index 489ecb19..f50bd489 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-storage", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/test-project/package.json b/packages/test-project/package.json index 1459f8c6..461ca660 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -1,6 +1,6 @@ { "name": "algorand-test-project", - "version": "1.6.4", + "version": "1.6.5", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", "description": "Repository for tests", diff --git a/packages/ui/package.json b/packages/ui/package.json index d183bc0b..fe2f26d6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-ui", - "version": "1.6.4", + "version": "1.6.5", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT",