diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 18355e7c..53f9117b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,7 +10,7 @@ on: branches: [develop, master, release] pull_request: # The branches below must be a subset of the branches above - branches: [develop] + branches: [develop, master, release] schedule: - cron: '0 16 * * 1' diff --git a/README.md b/README.md index 73ae246f..a4d60c2a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,15 @@ _This is the preferred solution for end-users, updates will be automatically ins Developers working with dApps may also install directly from the release package, or by downloading the project and building it. -## 1.4.0 Update +## 1.4.1 Update + +Minor release to address some bugs and usability issues: -The latest release brings: +- Custom network headers creation simplified to allow use of either the Algorand standard header key or custom headers +- Simultaneous AlgoSigner RPC calls through `algod` and `indexer` fix to prevent response mismatches +- Logout bug fix: prevents error message on each logout + +## 1.4.0 Update - Beta support for adding custom networks within AlgoSigner (development networks, BetaNet, etc.). - Navigation menu improvements diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index 62d48dfe..c1e6b2b4 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -13,22 +13,19 @@ Sent in transactions will be validated against the Algorand JS SDK transaction t Proxied requests are passed through to an API service - currently set to the PureStake API service. Endpoints available are limited to what the service exposes. The API backend may be configured by advanced users and is not guaranteed to respond as expected. -## Existing Methods - -- [!AlgoSigner](#) -- [Integrating AlgoSigner to add Transaction Capabilities for dApps on Algorand](#integrating-algosigner-to-add-transaction-capabilities-for-dapps-on-algorand) - - [Existing Methods](#existing-methods) - - [AlgoSigner.connect()](#algosignerconnect) - - [AlgoSigner.accounts({ ledger: ‘MainNet|TestNet’ })](#algosigneraccounts-ledger-mainnettestnet-) - - [AlgoSigner.algod({ ledger: ‘MainNet|TestNet’, path: ‘algod v2 path’, ... })](#algosigneralgod-ledger-mainnettestnet-path-algod-v2-path--) - - [AlgoSigner.indexer({ ledger: ‘MainNet|TestNet’, path: ‘indexer v2 path’ })](#algosignerindexer-ledger-mainnettestnet-path-indexer-v2-path-) - - [AlgoSigner.sign(txnObject)](#algosignersigntxnobject) - - [Transaction Requirements](#transaction-requirements) - - [Atomic Transactions](#atomic-transactions) - - [AlgoSigner.signMultisig(txn)](#algosignersignmultisigtxn) - - [Custom Networks](#custom-networks) - - [AlgoSigner.send({ ledger: ‘MainNet|TestNet’, txBlob })](#algosignersend-ledger-mainnettestnet-txblob-) - - [Rejection Messages](#rejection-messages) +## [Existing Methods](#existing-methods) + +- [AlgoSigner.connect()](#algosignerconnect) +- [AlgoSigner.accounts({ ledger: ‘MainNet|TestNet’ })](#algosigneraccounts-ledger-mainnettestnet-) +- [AlgoSigner.algod({ ledger: ‘MainNet|TestNet’, path: ‘algod v2 path’, ... })](#algosigneralgod-ledger-mainnettestnet-path-algod-v2-path--) +- [AlgoSigner.indexer({ ledger: ‘MainNet|TestNet’, path: ‘indexer v2 path’ })](#algosignerindexer-ledger-mainnettestnet-path-indexer-v2-path-) +- [AlgoSigner.sign(txnObject)](#algosignersigntxnobject) + - [Transaction Requirements](#transaction-requirements) + - [Atomic Transactions](#atomic-transactions) +- [AlgoSigner.signMultisig(txn)](#algosignersignmultisigtxn) +- [Custom Networks](#custom-networks) +- [AlgoSigner.send({ ledger: ‘MainNet|TestNet’, txBlob })](#algosignersend-ledger-mainnettestnet-txblob-) +- [Rejection Messages](#rejection-messages) ### AlgoSigner.connect() diff --git a/package.json b/package.json index 99aa2414..fc568449 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosigner", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "keywords": [ diff --git a/packages/common/package.json b/packages/common/package.json index 6a66e225..f0393aac 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/common", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "Common library functions for AlgoSigner.", "devDependencies": { diff --git a/packages/common/src/messaging/jsonrpc.ts b/packages/common/src/messaging/jsonrpc.ts index 09227ac2..636cf299 100644 --- a/packages/common/src/messaging/jsonrpc.ts +++ b/packages/common/src/messaging/jsonrpc.ts @@ -1,5 +1,3 @@ -import {IJsonRpc} from './interfaces' -import {RequestErrors} from '../types'; import { JSONRPC_VERSION, JsonRpcMethod, JsonPayload, JsonRpcBody } from './types'; @@ -16,7 +14,7 @@ export class JsonRpc /*implements IJsonRpc*/ { jsonrpc: JSONRPC_VERSION, method: method, params: params, - id: (+new Date).toString(16) + id: 'xxxxxxxxxxxxxxx'.replace(/[x]/g, () => {return (Math.random() * 16 | 0).toString(16)}) } } } \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 383df425..8e89c7b2 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-crypto", - "version": "1.4.0", + "version": "1.4.1", "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 64217a2a..d929a761 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/dapp", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "Sample DAPP for use with AlgoSigner.", "scripts": { diff --git a/packages/dapp/src/algosigner.ts b/packages/dapp/src/algosigner.ts index fa210c41..5f6133ac 100644 --- a/packages/dapp/src/algosigner.ts +++ b/packages/dapp/src/algosigner.ts @@ -1,26 +1,26 @@ -import {Task} from './fn/task'; -import {Router} from './fn/router'; +import { Task } from './fn/task'; +import { Router } from './fn/router'; class Wrapper { - private static instance: Wrapper; - private task: Task = new Task(); - private router: Router = new Router(); - - public connect: Function = this.task.connect; - public sign: Function = this.task.sign; - public signMultisig: Function = this.task.signMultisig; - public send: Function = this.task.send; - public accounts: Function = this.task.accounts; - public algod: Function = this.task.algod; - public indexer: Function = this.task.indexer; - public subscribe: Function = this.task.subscribe; + private static instance: Wrapper; + private task: Task = new Task(); + private router: Router = new Router(); - public static getInstance(): Wrapper { - if (!Wrapper.instance) { - Wrapper.instance = new Wrapper(); - } - return Wrapper.instance; + public connect: Function = this.task.connect; + public sign: Function = this.task.sign; + public signMultisig: Function = this.task.signMultisig; + public send: Function = this.task.send; + public accounts: Function = this.task.accounts; + public algod: Function = this.task.algod; + public indexer: Function = this.task.indexer; + public subscribe: Function = this.task.subscribe; + + public static getInstance(): Wrapper { + if (!Wrapper.instance) { + Wrapper.instance = new Wrapper(); } + return Wrapper.instance; + } } -export const AlgoSigner = Wrapper.getInstance(); \ No newline at end of file +export const AlgoSigner = Wrapper.getInstance(); diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index a056e4ec..7f3fe870 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.4.0", + "version": "1.4.1", "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 1c9795ff..6bbc35b2 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-extension", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "keywords": [ diff --git a/packages/extension/src/background/config.ts b/packages/extension/src/background/config.ts index 5e3b93df..a37f4a56 100644 --- a/packages/extension/src/background/config.ts +++ b/packages/extension/src/background/config.ts @@ -66,11 +66,11 @@ export class Settings { try { headers = JSON.parse(ledger['headers']); } catch (e) { - // Use headers default value, but use it as a token if if is a string + // Use headers default value, but use it as a token if it is a string if (typeof headers === 'string') { - headers = { 'X-API-Key': headers }; // Requests directly to a server would not require the X-API-Key - //headers = {"X-Algo-API-Token": headers}; + // This is the case for most users and is now the default for a string only method. + headers = { 'X-Algo-API-Token': headers }; } } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index c6a995ca..dbc838e1 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -85,6 +85,71 @@ export class Task { Task.authorized_pool = []; } + private static modifyTransactionWrapWithAssetCoreInfo(transactionWrap, callback) { + // Adjust decimal places if we are using an axfer transaction + if (transactionWrap.transaction['type'] === 'axfer') { + const assetIndex = transactionWrap.transaction['assetIndex']; + const ledger = getLedgerFromGenesisId(transactionWrap.transaction['genesisID']); + const conn = Settings.getBackendParams(ledger, API.Indexer); + const sendPath = `/v2/assets/${assetIndex}`; + const fetchAssets: any = { + headers: { + ...conn.headers, + }, + method: 'GET', + }; + + let url = conn.url; + if (conn.port.length > 0) url += ':' + conn.port; + Task.fetchAPI(`${url}${sendPath}`, fetchAssets) + .then((assets) => { + const params = assets['asset']['params']; + + // Get relevant data from asset params + const decimals = params['decimals']; + const unitName = params['unit-name']; + + // Update the unit-name for the asset + if (unitName) { + transactionWrap.unitName = unitName; + } + + // Get the display amount as a string to prevent screen deformation of large ints + let displayAmount = String(transactionWrap.transaction.amount); + + // If we have decimals, then we need to set the display amount with them in mind + if (decimals && decimals > 0) { + // Append missing zeros, if needed + if (displayAmount.length < decimals) { + displayAmount = displayAmount.padStart(decimals, '0'); + } + const offsetAmount = Math.abs(decimals - displayAmount.length); + + // Apply decimal transition + displayAmount = `${displayAmount.substr(0, offsetAmount)}.${displayAmount.substr( + offsetAmount + )}`; + + // If we start with a decimal now after padding and applying, add a 0 to the beginning for legibility + if (displayAmount.startsWith('.')) { + displayAmount = '0'.concat(displayAmount); + } + + // Set new amount + transactionWrap.displayAmount = displayAmount; + } + callback && callback(transactionWrap); + }) + .catch((ex) => { + // Could not get asset information for a transfer - attach error note + transactionWrap['error'] = ex['message']; + callback && callback(transactionWrap); + }); + } else { + callback && callback(transactionWrap); + } + } + public static methods(): { [key: string]: { [JsonRpcMethod: string]: Function; @@ -193,28 +258,35 @@ export class Task { Task.fetchAPI(`${url}${sendPath}`, fetchParams).then((params) => { calculateEstimatedFee(transactionWrap, params); - d.body.params = transactionWrap; - console.log('T-wrap'); - console.log(transactionWrap); - - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/sign-transaction'), - ...popupProperties, - }, - 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); - } + + Task.modifyTransactionWrapWithAssetCoreInfo(transactionWrap, (transactionWrap) => { + if (transactionWrap.error) { + // There was an error building the asset info. Outright reject / allow with warning. + //reject(d); + //return; } - ); + + d.body.params = transactionWrap; + + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/sign-transaction'), + ...popupProperties, + }, + 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); + } + } + ); + }); }); } }, @@ -291,44 +363,52 @@ export class Task { Task.fetchAPI(`${url}${sendPath}`, fetchParams).then((params) => { calculateEstimatedFee(transactionWrap, params); - d.body.params.validityObject = transactionWrap.validityObject; - d.body.params.txn = transactionWrap.transaction; - d.body.params.estimatedFee = transactionWrap.estimatedFee; + Task.modifyTransactionWrapWithAssetCoreInfo(transactionWrap, (transactionWrap) => { + if (transactionWrap.error) { + // There was an error building the asset info. Outright reject / allow with warning. + //reject(d); + //return; + } - const msig_txn = { msig: d.body.params.msig, txn: d.body.params.txn }; - const session = InternalMethods.getHelperSession(); - const ledger = getLedgerFromGenesisId(transactionWrap.transaction.genesisID); - const accounts = session.wallet[ledger]; - const multisigAccounts = getSigningAccounts(accounts, msig_txn); + d.body.params.validityObject = transactionWrap.validityObject; + d.body.params.txn = transactionWrap.transaction; + d.body.params.estimatedFee = transactionWrap.estimatedFee; - if (multisigAccounts.error) { - d.error = multisigAccounts.error.message; - reject(d); - } else { - if (multisigAccounts.accounts && multisigAccounts.accounts.length > 0) { - d.body.params.account = multisigAccounts.accounts[0]['address']; - d.body.params.name = multisigAccounts.accounts[0]['name']; - } + const msig_txn = { msig: d.body.params.msig, txn: d.body.params.txn }; + const session = InternalMethods.getHelperSession(); + const ledger = getLedgerFromGenesisId(transactionWrap.transaction.genesisID); + const accounts = session.wallet[ledger]; + const multisigAccounts = getSigningAccounts(accounts, msig_txn); - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/sign-multisig-transaction'), - ...popupProperties, - }, - 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); - } + if (multisigAccounts.error) { + d.error = multisigAccounts.error.message; + reject(d); + } else { + if (multisigAccounts.accounts && multisigAccounts.accounts.length > 0) { + d.body.params.account = multisigAccounts.accounts[0]['address']; + d.body.params.name = multisigAccounts.accounts[0]['name']; } - ); - } + + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/sign-multisig-transaction'), + ...popupProperties, + }, + 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); + } + } + ); + } + }); }); } }, @@ -510,180 +590,76 @@ export class Task { // note, } = message.body.params.transaction; - const ledger = getLedgerFromGenesisId(genesisID); - - const context = new encryptionWrap(passphrase); - context.unlock(async (unlockedValue: any) => { - if ('error' in unlockedValue) { - sendResponse(unlockedValue); - return false; - } - - extensionBrowser.windows.remove(auth.window_id); - - let account; + try { + const ledger = getLedgerFromGenesisId(genesisID); - if (unlockedValue[ledger] === undefined) { - message.error = RequestErrors.UnsupportedLedger; - MessageApi.send(message); - } - // Find address to send algos from - for (let i = unlockedValue[ledger].length - 1; i >= 0; i--) { - if (unlockedValue[ledger][i].address === from) { - account = unlockedValue[ledger][i]; - break; + const context = new encryptionWrap(passphrase); + context.unlock(async (unlockedValue: any) => { + if ('error' in unlockedValue) { + sendResponse(unlockedValue); + return false; } - } - const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); + extensionBrowser.windows.remove(auth.window_id); - const txn = { ...message.body.params.transaction }; - - Object.keys({ ...message.body.params.transaction }).forEach((key) => { - if (txn[key] === undefined || txn[key] === null) { - delete txn[key]; - } - }); + let account; - // Modify base64 encoded fields - if ('note' in txn && txn.note !== undefined) { - txn.note = new Uint8Array(Buffer.from(txn.note)); - } - // Application transactions only - if (txn && txn.type == 'appl') { - if ('appApprovalProgram' in txn) { - try { - txn.appApprovalProgram = Uint8Array.from( - Buffer.from(txn.appApprovalProgram, 'base64') - ); - } catch { - message.error = - 'Error trying to parse appApprovalProgram into a Uint8Array value.'; - } + if (unlockedValue[ledger] === undefined) { + message.error = RequestErrors.UnsupportedLedger; + MessageApi.send(message); } - if ('appClearProgram' in txn) { - try { - txn.appClearProgram = Uint8Array.from(Buffer.from(txn.appClearProgram, 'base64')); - } catch { - message.error = 'Error trying to parse appClearProgram into a Uint8Array value.'; + // Find address to send algos from + for (let i = unlockedValue[ledger].length - 1; i >= 0; i--) { + if (unlockedValue[ledger][i].address === from) { + account = unlockedValue[ledger][i]; + break; } } - if ('appArgs' in txn) { - try { - const tempArgs = []; - txn.appArgs.forEach((element) => { - logging.log(element); - tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); - }); - txn.appArgs = tempArgs; - } catch { - message.error = 'Error trying to parse appArgs into Uint8Array values.'; - } - } - } - try { - // This step transitions a raw object into a transaction style object - const builtTx = buildTransaction(txn); - // We are combining the tx id get and sign into one step/object because of legacy, - // this may not need to be the case any longer. - const signedTxn = { - txID: builtTx.txID().toString(), - blob: builtTx.signTxn(recoveredAccount.sk), - }; - const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); - - message.response = { - txID: signedTxn.txID, - blob: b64Obj, - }; - } catch (e) { - message.error = e.message; - } - - // Clean class saved request - delete Task.requests[responseOriginTabID]; - MessageApi.send(message); - }); - return true; - }, - // sign-allow-multisig - [JsonRpcMethod.SignAllowMultisig]: (request: any, sendResponse: Function) => { - const { passphrase, responseOriginTabID } = request.body.params; - const auth = Task.requests[responseOriginTabID]; - const message = auth.message; - - // Map the full multisig transaction here - const msig_txn = { msig: message.body.params.msig, txn: message.body.params.txn }; - - // Use MainNet if specified - default to TestNet - const ledger = getLedgerFromGenesisId(msig_txn.txn.genesisID); - - // Create an encryption wrap to get the needed signing account information - const context = new encryptionWrap(passphrase); - context.unlock(async (unlockedValue: any) => { - if ('error' in unlockedValue) { - sendResponse(unlockedValue); - return false; - } - - extensionBrowser.windows.remove(auth.window_id); - - // Verify this is a multisig sign occurs in the getSigningAccounts - // This get may receive a .error in return if an appropriate account is not found - let account; - const multisigAccounts = getSigningAccounts(unlockedValue[ledger], msig_txn); - if (multisigAccounts.error) { - message.error = multisigAccounts.error.message; - } else { - // TODO: Currently we are grabbing the first non-signed account. This may change. - account = multisigAccounts.accounts[0]; - } - - if (account) { - // We can now use the found account match to get the sign key const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - // Use the received txn component of the transaction, but remove undefined and null values - Object.keys({ ...msig_txn.txn }).forEach((key) => { - if (msig_txn.txn[key] === undefined || msig_txn.txn[key] === null) { - delete msig_txn.txn[key]; + const txn = { ...message.body.params.transaction }; + + Object.keys({ ...message.body.params.transaction }).forEach((key) => { + if (txn[key] === undefined || txn[key] === null) { + delete txn[key]; } }); // Modify base64 encoded fields - if ('note' in msig_txn.txn && msig_txn.txn.note !== undefined) { - msig_txn.txn.note = new Uint8Array(Buffer.from(msig_txn.txn.note)); + if ('note' in txn && txn.note !== undefined) { + txn.note = new Uint8Array(Buffer.from(txn.note)); } // Application transactions only - if (msig_txn.txn && msig_txn.txn.type == 'appl') { - if ('appApprovalProgram' in msig_txn.txn) { + if (txn && txn.type == 'appl') { + if ('appApprovalProgram' in txn) { try { - msig_txn.txn.appApprovalProgram = Uint8Array.from( - Buffer.from(msig_txn.txn.appApprovalProgram, 'base64') + txn.appApprovalProgram = Uint8Array.from( + Buffer.from(txn.appApprovalProgram, 'base64') ); } catch { message.error = 'Error trying to parse appApprovalProgram into a Uint8Array value.'; } } - if ('appClearProgram' in msig_txn.txn) { + if ('appClearProgram' in txn) { try { - msig_txn.txn.appClearProgram = Uint8Array.from( - Buffer.from(msig_txn.txn.appClearProgram, 'base64') + txn.appClearProgram = Uint8Array.from( + Buffer.from(txn.appClearProgram, 'base64') ); } catch { message.error = 'Error trying to parse appClearProgram into a Uint8Array value.'; } } - if ('appArgs' in msig_txn.txn) { + if ('appArgs' in txn) { try { const tempArgs = []; - msig_txn.txn.appArgs.forEach((element) => { + txn.appArgs.forEach((element) => { + logging.log(element); tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); }); - msig_txn.txn.appArgs = tempArgs; + txn.appArgs = tempArgs; } catch { message.error = 'Error trying to parse appArgs into Uint8Array values.'; } @@ -692,41 +668,13 @@ export class Task { try { // This step transitions a raw object into a transaction style object - const builtTx = buildTransaction(msig_txn.txn); - - // Building preimg - This allows the pks to be passed, but still use the default multisig sign with addrs - const version = msig_txn.msig.v || msig_txn.msig.version; - const threshold = msig_txn.msig.thr || msig_txn.msig.threshold; - const addrs = - msig_txn.msig.addrs || - msig_txn.msig.subsig.map((subsig) => { - return subsig.pk; - }); - const preimg = { - version: version, - threshold: threshold, - addrs: addrs, + const builtTx = buildTransaction(txn); + // We are combining the tx id get and sign into one step/object because of legacy, + // this may not need to be the case any longer. + const signedTxn = { + txID: builtTx.txID().toString(), + blob: builtTx.signTxn(recoveredAccount.sk), }; - - let signedTxn; - const appendEnabled = false; // TODO: This disables append functionality until blob objects are allowed and validated. - // Check for existing signatures. Append if there are any. - if (appendEnabled && msig_txn.msig.subsig.some((subsig) => subsig.s)) { - // TODO: This should use a sent multisig blob if provided. This is a future enhancement as validation doesn't allow it currently. - // It is subject to change and is built as scaffolding for future functionality. - const encodedBlob = message.body.params.txn; - const decodedBlob = Buffer.from(encodedBlob, 'base64'); - signedTxn = algosdk.appendSignMultisigTransaction( - decodedBlob, - preimg, - recoveredAccount.sk - ); - } else { - // If this is the first signature then do a normal sign - signedTxn = algosdk.signMultisigTransaction(builtTx, preimg, recoveredAccount.sk); - } - - // Converting the blob to an encoded string for transfer back to dApp const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); message.response = { @@ -736,11 +684,162 @@ export class Task { } catch (e) { message.error = e.message; } - } - // Clean class saved request + + // Clean class saved request + delete Task.requests[responseOriginTabID]; + MessageApi.send(message); + }); + } catch { + // On error we should remove the task delete Task.requests[responseOriginTabID]; - MessageApi.send(message); - }); + return false; + } + return true; + }, + // sign-allow-multisig + [JsonRpcMethod.SignAllowMultisig]: (request: any, sendResponse: Function) => { + const { passphrase, responseOriginTabID } = request.body.params; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; + + // Map the full multisig transaction here + const msig_txn = { msig: message.body.params.msig, txn: message.body.params.txn }; + + try { + // Use MainNet if specified - default to TestNet + const ledger = getLedgerFromGenesisId(msig_txn.txn.genesisID); + + // Create an encryption wrap to get the needed signing account information + const context = new encryptionWrap(passphrase); + context.unlock(async (unlockedValue: any) => { + if ('error' in unlockedValue) { + sendResponse(unlockedValue); + return false; + } + + extensionBrowser.windows.remove(auth.window_id); + + // Verify this is a multisig sign occurs in the getSigningAccounts + // This get may receive a .error in return if an appropriate account is not found + let account; + const multisigAccounts = getSigningAccounts(unlockedValue[ledger], msig_txn); + if (multisigAccounts.error) { + message.error = multisigAccounts.error.message; + } else { + // TODO: Currently we are grabbing the first non-signed account. This may change. + account = multisigAccounts.accounts[0]; + } + + if (account) { + // We can now use the found account match to get the sign key + const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); + + // Use the received txn component of the transaction, but remove undefined and null values + Object.keys({ ...msig_txn.txn }).forEach((key) => { + if (msig_txn.txn[key] === undefined || msig_txn.txn[key] === null) { + delete msig_txn.txn[key]; + } + }); + + // Modify base64 encoded fields + if ('note' in msig_txn.txn && msig_txn.txn.note !== undefined) { + msig_txn.txn.note = new Uint8Array(Buffer.from(msig_txn.txn.note)); + } + // Application transactions only + if (msig_txn.txn && msig_txn.txn.type == 'appl') { + if ('appApprovalProgram' in msig_txn.txn) { + try { + msig_txn.txn.appApprovalProgram = Uint8Array.from( + Buffer.from(msig_txn.txn.appApprovalProgram, 'base64') + ); + } catch { + message.error = + 'Error trying to parse appApprovalProgram into a Uint8Array value.'; + } + } + if ('appClearProgram' in msig_txn.txn) { + try { + msig_txn.txn.appClearProgram = Uint8Array.from( + Buffer.from(msig_txn.txn.appClearProgram, 'base64') + ); + } catch { + message.error = + 'Error trying to parse appClearProgram into a Uint8Array value.'; + } + } + if ('appArgs' in msig_txn.txn) { + try { + const tempArgs = []; + msig_txn.txn.appArgs.forEach((element) => { + tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); + }); + msig_txn.txn.appArgs = tempArgs; + } catch { + message.error = 'Error trying to parse appArgs into Uint8Array values.'; + } + } + } + + try { + // This step transitions a raw object into a transaction style object + const builtTx = buildTransaction(msig_txn.txn); + + // Building preimg - This allows the pks to be passed, but still use the default multisig sign with addrs + const version = msig_txn.msig.v || msig_txn.msig.version; + const threshold = msig_txn.msig.thr || msig_txn.msig.threshold; + const addrs = + msig_txn.msig.addrs || + msig_txn.msig.subsig.map((subsig) => { + return subsig.pk; + }); + const preimg = { + version: version, + threshold: threshold, + addrs: addrs, + }; + + let signedTxn; + const appendEnabled = false; // TODO: This disables append functionality until blob objects are allowed and validated. + // Check for existing signatures. Append if there are any. + if (appendEnabled && msig_txn.msig.subsig.some((subsig) => subsig.s)) { + // TODO: This should use a sent multisig blob if provided. This is a future enhancement as validation doesn't allow it currently. + // It is subject to change and is built as scaffolding for future functionality. + const encodedBlob = message.body.params.txn; + const decodedBlob = Buffer.from(encodedBlob, 'base64'); + signedTxn = algosdk.appendSignMultisigTransaction( + decodedBlob, + preimg, + recoveredAccount.sk + ); + } else { + // If this is the first signature then do a normal sign + signedTxn = algosdk.signMultisigTransaction( + builtTx, + preimg, + recoveredAccount.sk + ); + } + + // Converting the blob to an encoded string for transfer back to dApp + const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); + + message.response = { + txID: signedTxn.txID, + blob: b64Obj, + }; + } catch (e) { + message.error = e.message; + } + } + // Clean class saved request + delete Task.requests[responseOriginTabID]; + MessageApi.send(message); + }); + } catch { + // On error we should remove the task + delete Task.requests[responseOriginTabID]; + return false; + } return true; }, /* eslint-disable-next-line no-unused-vars */ @@ -775,6 +874,7 @@ export class Task { [JsonRpcMethod.Logout]: (request: any, sendResponse: Function) => { InternalMethods.clearSession(); Task.clearPool(); + sendResponse(true); }, [JsonRpcMethod.GetSession]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.GetSession](request, sendResponse); diff --git a/packages/extension/src/background/utils/validator.ts b/packages/extension/src/background/utils/validator.ts index efe4a849..95f290bd 100644 --- a/packages/extension/src/background/utils/validator.ts +++ b/packages/extension/src/background/utils/validator.ts @@ -51,7 +51,7 @@ export function Validate(field: any, value: any): ValidationResponse { if (!algosdk.isValidAddress(value)) { return new ValidationResponse({ status: ValidationStatus.Invalid, - info: 'Address does not adhear to a valid structure.', + info: 'Address does not adhere to a valid structure.', }); } else { return new ValidationResponse({ status: ValidationStatus.Valid }); @@ -60,7 +60,7 @@ export function Validate(field: any, value: any): ValidationResponse { if (!algosdk.isValidAddress(value)) { return new ValidationResponse({ status: ValidationStatus.Invalid, - info: 'Address does not adhear to a valid structure.', + info: 'Address does not adhere to a valid structure.', }); } else { return new ValidationResponse({ status: ValidationStatus.Valid }); diff --git a/packages/storage/package.json b/packages/storage/package.json index d0753ca7..670ce50b 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-storage", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "Storage wrapper for saving and retrieving extention information in Algosigner.", "devDependencies": { diff --git a/packages/test-project/package.json b/packages/test-project/package.json index 116b4b73..d925e04f 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -1,6 +1,6 @@ { "name": "algorand-test-project", - "version": "1.4.0", + "version": "1.4.1", "description": "Repository for tests", "devDependencies": { "algosdk": "1.8.1", diff --git a/packages/ui/package.json b/packages/ui/package.json index a2cfe0c7..2ebbca2c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-ui", - "version": "1.4.0", + "version": "1.4.1", "author": "https://developer.purestake.io", "description": "User interface built for AlgoSigner.", "private": true, diff --git a/packages/ui/src/components/LedgerNetworkModify.ts b/packages/ui/src/components/LedgerNetworkModify.ts index e258e2d4..b26baa26 100644 --- a/packages/ui/src/components/LedgerNetworkModify.ts +++ b/packages/ui/src/components/LedgerNetworkModify.ts @@ -126,11 +126,34 @@ const LedgerNetworkModify: FunctionalComponent = (props: any) => { value=${networkIndexerUrl} onInput=${(e) => setNetworkIndexerUrl(e.target.value)} /> - + setNetworkHeaders(e.target.value)} /> diff --git a/packages/ui/src/components/SignTransaction/TxAxfer.ts b/packages/ui/src/components/SignTransaction/TxAxfer.ts index d128639c..79b2cb28 100644 --- a/packages/ui/src/components/SignTransaction/TxAxfer.ts +++ b/packages/ui/src/components/SignTransaction/TxAxfer.ts @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks'; const TxAxfer: FunctionalComponent = (props: any) => { const [tab, setTab] = useState('overview'); - const { tx, account, ledger, vo, dt, fee } = props; + const { tx, account, ledger, vo, dt, fee, da, un } = props; const txText = JSON.stringify(tx, null, 2); @@ -35,16 +35,10 @@ const TxAxfer: FunctionalComponent = (props: any) => {
@@ -66,19 +60,13 @@ const TxAxfer: FunctionalComponent = (props: any) => { ${tx.assetIndex}
-
-

- ${!tx['flatFee'] ? 'Estimated fee:' : 'Fee:'} -

+
+

${!tx['flatFee'] ? 'Estimated fee:' : 'Fee:'}

${fee / 1e6} Algos

Amount:

-

${tx.amount}

+

${da || tx.amount} ${un}

`} diff --git a/packages/ui/src/pages/SetPassword.ts b/packages/ui/src/pages/SetPassword.ts index 77bbae6a..2d1056cc 100644 --- a/packages/ui/src/pages/SetPassword.ts +++ b/packages/ui/src/pages/SetPassword.ts @@ -21,6 +21,7 @@ const SetPassword: FunctionalComponent = (props) => { const [pwd, setPwd] = useState(''); const [confirmPwd, setConfirmPwd] = useState(''); const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); const store: any = useContext(StoreContext); const createWallet = () => { @@ -36,7 +37,10 @@ const SetPassword: FunctionalComponent = (props) => { passphrase: pwd, }; + setLoading(true); + sendMessage(JsonRpcMethod.CreateWallet, params, function (response) { + setLoading(false); if ('error' in response) { setError(response.error); } else { @@ -117,7 +121,7 @@ const SetPassword: FunctionalComponent = (props) => {