diff --git a/.eslintignore b/.eslintignore index beb4b2fc..b4b479f3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,5 +4,4 @@ .DS_Store **/coverage packages/crypto -packages/dapp **/*.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index b805f2ad..1adc3c39 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,8 @@ module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], globals: { browser: true, - page: true, + dappPage: true, + extensionPage: true, AlgoSigner: true, }, parser: '@typescript-eslint/parser', @@ -18,10 +19,23 @@ module.exports = { rules: { 'no-unused-vars': 'error', 'no-var': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', + 'no-prototype-builtins': 'warn', + '@typescript-eslint/no-this-alias': 'warn', + '@typescript-eslint/no-empty-function': 'warn', + '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + Function: false, + Object: false, + }, + extendDefaults: true, + }, + ], }, overrides: [ { diff --git a/README.md b/README.md index ffa73a1b..73ae246f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ![AlgoSigner](media/algosigner-wallet-banner-3.png) -An open-source Algorand wallet browser extension that permits dApp communication for signing Algorand transactions — available for Chrome initially. +An open-source, security audited, Algorand wallet browser extension that permits dApp communication for signing Algorand transactions — available for Chrome. ## Chrome Extension Store @@ -10,15 +10,30 @@ _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.3.0 Update +## 1.4.0 Update The latest release brings: -- Support for dApps to submit multisig transactions and retrieve the single associated address signature. +- Beta support for adding custom networks within AlgoSigner (development networks, BetaNet, etc.). +- Navigation menu improvements +- Logout! + +### Custom Networks + +- Network information can now be accessed by selecting "Network Configuration" in the options menu. + - This list shows the information needed by the dApp for connections. +- New networks can be added via the "New Network" button. Here is a brief overview of the fields: + - Display Name: Name that will be displayed which will also be used for dApps interacting with the network. + - Network ID: Genesis ID for the network. Transactions will be validated against the value here and must contain a matching value. Defaults to "mainnet-v1.0". + - Network Algod URL: The address which will be used for any Algod related calls. Defaults to the PureStake MainNet URL. + - Network Indexer URL: The address which will be used for any Indexer lookup calls. Defaults to the PureStake MainNet Indexer URL. + - Network Headers: Object stucture that will be used as replacement headers for calls. The object structure has an "Algod" and "Indexer" sub structure which will contain the headers the target api needs in order to function. ## Roadmap -The next feature release will be a feature release permitting the addition and configuration of networks, planned for early 2021. +Upcoming feature releases will focus on adding Ledger device support and a more streamlined approach to creating and interacting with transactions in AlgoSigner. + +## Previously delivered ### Multisig Transactions diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index 39cdadd2..62d48dfe 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -15,12 +15,20 @@ Proxied requests are passed through to an API service - currently set to the Pur ## Existing Methods -- [Connect to Extension](#algosignerconnect) -- [Request Wallet Accounts](#algosigneraccounts-ledger-mainnettestnet-) -- [Algod v2 API](#algosigneralgod-ledger-mainnettestnet-path-algod-v2-path--) -- [Indexer v2 API](#algosignerindexer-ledger-mainnettestnet-path-indexer-v2-path-) -- [Sign Transactions](#algosignersigntxnobject) -- [Send Transactions](#algosignersend-ledger-mainnettestnet-txblob-) +- [!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) ### AlgoSigner.connect() @@ -190,18 +198,68 @@ Due to limitations in Chrome internal messaging, AlgoSigner encodes the transact - [Python](https://github.com/PureStake/algosigner-dapp-example/blob/master/python/pythonTransaction.py) - [NodeJS](https://github.com/PureStake/algosigner-dapp-example/blob/master/nodeJs/nodeJsTransaction.js) -#### Multisig Transactions +#### Atomic Transactions + +- Grouped transactions intended for atomic transaction functionality need to be grouped outside of AlgoSigner, but can be signed individually. +- The grouped transactions need to have their binary components concatenated to be accepted in the AlgoSigner send method. +- An example of this can be seen in the [existing sample dApp group test](https://purestake.github.io/algosigner-dapp-example/tx-test/signTesting.html). + +### AlgoSigner.signMultisig(txn) - Multisig transactions can be signed individually through AlgoSigner. - Using the associated msig for the transaction an available matching unsigned address will be selected if possible to sign the txn component. - The resulting sign will return the a msig with only this signature in the blob and will need to be merged with other signatures before sending to the network. - An example of this can be seen in the [existing sample dApp multisig test](https://purestake.github.io/algosigner-dapp-example/tx-test/signTesting.html). -#### Atomic Transactions +### Custom Networks -- Grouped transactions intended for atomic transaction functionality need to be grouped outside of AlgoSigner, but can be signed individually. -- The grouped transactions need to have their binary components concatenated to be accepted in the AlgoSigner send method. -- An example of this can be seen in the [existing sample dApp group test](https://purestake.github.io/algosigner-dapp-example/tx-test/signTesting.html). +- Custom networks beta support is now in AlgoSigner. +- AlgoSigner.accounts(ledger) has changed such that calls now accept names that have been added to the user's custom network list as valid ledger names. + - A non-matching ledger name will result in a error: + - [RequestErrors.UnsupportedLedger] The provided ledger is not supported. + - An empty request will result with an error: + - Ledger not provided. Please use a base ledger: [TestNet,MainNet] or an available custom one [{"name":"Theta","genesisId":"testnet-v1.0"}]. +- Transaction requests will require a valid matching "genesisId", even for custom networks. + +**Request** + +```js +let msig = { + subsig: [ + { + pk: ms.account1.addr, + }, + { + pk: ms.account2.addr, + }, + { + pk: ms.account3.addr, + }, + ], + thr: 2, + v: 1, +}; + +let mstx = { + msig: msig, + txn: { + type: 'pay', + from: ms.multisigAddr, + to: '7GBK5IJCWFPRWENNUEZI3K4CSE5KDIRSR55KWTSDDOBH3E3JJCKGCSFDGQ', + amount: amount, + fee: txParams['fee'], + firstRound: txParams['last-round'], + lastRound: txParams['last-round'] + 1000, + genesisID: txParams['genesis-id'], + genesisHash: txParams['genesis-hash'], + }, +}; +``` + +The merge is complex, review: + +- [Multi-sig example](https://github.com/PureStake/algosigner-dapp-example/blob/master/tx-test/types/multisig.js) +- [Signing function](https://github.com/PureStake/algosigner-dapp-example/blob/master/tx-test/common/sign.js) ### AlgoSigner.send({ ledger: ‘MainNet|TestNet’, txBlob }) diff --git a/package.json b/package.json index f19c1c79..99aa2414 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosigner", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "keywords": [ @@ -36,11 +36,11 @@ "husky": { "hooks": { "pre-commit": "lint-staged", - "pre-push": "echo '\nChecking for uncommited files\nMake sure to stash uncommited changes before pushing\n'; git diff HEAD --quiet && npm run test:unit" + "pre-push": "npm run test:unit" } }, "lint-staged": { "*.{js,json,ts,scss,css,md,yaml}": "prettier --write", - "*.{js,json,ts}": "eslint --fix" + "*.{js,json,ts}": "eslint --quiet --fix" } } diff --git a/packages/common/package.json b/packages/common/package.json index 008cca98..6a66e225 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/common", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "Common library functions for AlgoSigner.", "devDependencies": { diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index 240c3b88..4627d24d 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -1,5 +1,6 @@ export const JSONRPC_VERSION: string = "2.0"; +/* eslint-disable no-unused-vars */ export enum JsonRpcMethod { Heartbeat = "heartbeat", Authorization = "authorization", @@ -23,6 +24,7 @@ export enum JsonRpcMethod { DeleteAccount = "delete-account", GetSession = "get-session", Login = "login", + Logout = "logout", AccountDetails = "account-details", Transactions = "transactions", AssetDetails = "asset-details", @@ -30,6 +32,9 @@ export enum JsonRpcMethod { AssetsVerifiedList = "assets-verified-list", SignSendTransaction = "sign-send-transaction", ChangeLedger = "change-ledger", + SaveNetwork = "save-network", + DeleteNetwork = "delete-network", + GetLedgers = "get-ledgers", } diff --git a/packages/common/src/types/ledgers.ts b/packages/common/src/types/ledgers.ts index 2eb83d0f..ccb1672a 100644 --- a/packages/common/src/types/ledgers.ts +++ b/packages/common/src/types/ledgers.ts @@ -1,5 +1,54 @@ -export function getSupportedLedgers(): Array{ +export class LedgerTemplate { + name: string; + readonly isEditable: boolean; + genesisId?: string; + genesisHash?: string; + symbol?: string; + algodUrl?: string; + indexerUrl?: string; + headers?: string; + + public get uniqueName() : string { + return this.name.toLowerCase(); + } + + constructor({ name, genesisId, genesisHash, symbol, algodUrl, indexerUrl, headers }: + { + name: string, + genesisId?: string, + genesisHash?: string, + symbol?: string, + algodUrl?: string, + indexerUrl?: string, + headers?: string + }) { + if(!name){ + throw Error('A name is required for ledgers.'); + } + + this.name = name; + this.genesisId = genesisId || 'mainnet-v1.0'; + this.genesisHash = genesisHash; + this.symbol = symbol; + this.algodUrl = algodUrl; + this.indexerUrl = indexerUrl; + this.headers = headers; + this.isEditable = (name !== 'MainNet' && name !== 'TestNet') + } +} + +export function getBaseSupportedLedgers(): Array{ // Need to add access to additional ledger types from import - return [{"name": "mainnet", "genesisId": 'mainnet-v1.0', "genesisHash": ""}, - {"name": "testnet", "genesisId": 'testnet-v1.0', "genesisHash": ""}]; + return [ + new LedgerTemplate({ + name: 'MainNet', + genesisId: 'mainnet-v1.0', + genesisHash: '' + }), + new LedgerTemplate({ + name: 'TestNet', + genesisId: 'testnet-v1.0', + genesisHash: '' + }) + ]; } \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 96449413..383df425 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-crypto", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "Cryptographic wrapper for saving and retrieving extention information in Algosigner.", "repository": { diff --git a/packages/dapp/jest.config.js b/packages/dapp/jest.config.js index 9ad4ae69..94700c42 100644 --- a/packages/dapp/jest.config.js +++ b/packages/dapp/jest.config.js @@ -1,16 +1,11 @@ module.exports = { - verbose: true, - moduleNameMapper: { - "^@algosigner/common(.*)$": "/../common/src$1" - }, - "roots": [ - "/src" - ], - "testMatch": [ - "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)" - ], - "transform": { - "^.+\\.(ts|tsx)$": "ts-jest" - } -} \ No newline at end of file + verbose: true, + moduleNameMapper: { + '^@algosigner/common(.*)$': '/../common/src$1', + }, + roots: ['/src'], + testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, +}; diff --git a/packages/dapp/package.json b/packages/dapp/package.json index fe81d1fe..64217a2a 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/dapp", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "Sample DAPP for use with AlgoSigner.", "scripts": { diff --git a/packages/dapp/src/fn/__clerk.ts b/packages/dapp/src/fn/__clerk.ts index 83c2c23e..2551b7b5 100644 --- a/packages/dapp/src/fn/__clerk.ts +++ b/packages/dapp/src/fn/__clerk.ts @@ -1,6 +1,6 @@ // import {IClerk} from './interfaces'; -// import {MessageBuilder} from '../messaging/builder'; +// import {MessageBuilder} from '../messaging/builder'; // import {Transaction,RequestErrors} from '@algosigner/common/types'; // import {JsonRpcMethod,JsonRpcResponse} from '@algosigner/common/messaging/types'; @@ -16,9 +16,9 @@ // error = RequestErrors.InvalidTransactionParams; // } // return MessageBuilder.promise( -// JsonRpcMethod.SignTransaction, +// JsonRpcMethod.SignTransaction, // params, // error // ); // } -// } \ No newline at end of file +// } diff --git a/packages/dapp/src/fn/interfaces.ts b/packages/dapp/src/fn/interfaces.ts index 3e57e87c..0147c62f 100644 --- a/packages/dapp/src/fn/interfaces.ts +++ b/packages/dapp/src/fn/interfaces.ts @@ -1,7 +1,8 @@ import { RequestErrors, Transaction, MultisigTransaction } from '@algosigner/common/types'; import { JsonPayload } from '@algosigner/common/messaging/types'; +/* eslint-disable no-unused-vars */ export interface ITask { - sign(p: Transaction, e: RequestErrors): Promise; - signMultisig(p: MultisigTransaction, e: RequestErrors): Promise; -} \ No newline at end of file + sign(p: Transaction, e: RequestErrors): Promise; + signMultisig(p: MultisigTransaction, e: RequestErrors): Promise; +} diff --git a/packages/dapp/src/fn/router.ts b/packages/dapp/src/fn/router.ts index 4dbb36f2..13f28429 100644 --- a/packages/dapp/src/fn/router.ts +++ b/packages/dapp/src/fn/router.ts @@ -1,57 +1,51 @@ - // The Router routes messages sent to the dApp back to the extension. // // By default we are routing every message from the Extension to a global handler. -// This handler bounces back a signal to the Extension as simple ACK mechanism, that will +// This handler bounces back a signal to the Extension as simple ACK mechanism, that will // ultimately resolve the Promise in the Extension side. // // In the future, we obviously want to offer more configuration. 2nd iteration probably adding // a dApp defined custom global handler, subsequent iterations probably be able of adding // custom handler for different message types, etc.. -import {MessageApi} from '../messaging/api'; -import {Task} from './task'; +import { MessageApi } from '../messaging/api'; +import { Task } from './task'; export class Router { - handler: Function; - constructor() { - this.handler = this.default; - window.addEventListener("message",(event) => { - var d = event.data; + handler: Function; + constructor() { + this.handler = this.default; + window.addEventListener('message', (event) => { + const d = event.data; - try { - if (typeof d === 'string') { - let result = JSON.parse(d); - let type = Object.prototype.toString.call(result); - if(type === '[object Object]' || type === '[object Array]') { - // We can display message output here, but as a string object it doesn't match our format and is likely from other sources - } - } - else { - if(Object.prototype.toString.call(d) === '[object Object]' && "source" in d){ - if(d.source == "extension") { - d.source = 'router'; - d.origin = window.location.origin; - this.handler(d); - } - } - } + try { + if (typeof d === 'string') { + const result = JSON.parse(d); + const type = Object.prototype.toString.call(result); + if (type === '[object Object]' || type === '[object Array]') { + // We can display message output here, but as a string object it doesn't match our format and is likely from other sources + } + } else { + if (Object.prototype.toString.call(d) === '[object Object]' && 'source' in d) { + if (d.source == 'extension') { + d.source = 'router'; + d.origin = window.location.origin; + this.handler(d); } - catch { - //console.log(`Unable to determine source from message. \nEvent:${JSON.stringify(event)}`); - } - - - - }); - } - default(d:any){ - if(d.body.method in Task.subscriptions) { - Task.subscriptions[d.body.method](); + } } - this.bounce(d); - } - bounce(d:any){ - let api = new MessageApi(); - window.postMessage(d, window.location.origin, [api.mc.port2]); + } catch { + //console.log(`Unable to determine source from message. \nEvent:${JSON.stringify(event)}`); + } + }); + } + default(d: any) { + if (d.body.method in Task.subscriptions) { + Task.subscriptions[d.body.method](); } -} \ No newline at end of file + this.bounce(d); + } + bounce(d: any) { + const api = new MessageApi(); + window.postMessage(d, window.location.origin, [api.mc.port2]); + } +} diff --git a/packages/dapp/src/fn/task.test.ts b/packages/dapp/src/fn/task.test.ts index 5b2dbf3c..3cc50f36 100644 --- a/packages/dapp/src/fn/task.test.ts +++ b/packages/dapp/src/fn/task.test.ts @@ -1,11 +1,10 @@ -import {Task} from './task'; -import {MessageBuilder} from '../messaging/builder'; -import {RequestErrors} from '@algosigner/common/types'; -import {JsonRpcMethod} from '@algosigner/common/messaging/types'; +import { Task } from './task'; +import { MessageBuilder } from '../messaging/builder'; +import { RequestErrors } from '@algosigner/common/types'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; jest.mock('../messaging/builder'); - describe('task tests', () => { beforeEach(() => { // Clear all instances and calls to constructor and all methods: @@ -13,19 +12,25 @@ describe('task tests', () => { }); test('connect must call MessageBuilder once', () => { - const task = new Task().connect(); + const task = new Task(); + task.connect(); expect(MessageBuilder.promise).toHaveBeenCalledTimes(1); }); test('sign must call MessageBuilder with expected params', () => { const transaction = { amount: 10, - from: "FROMACC", - note: "NOTE", - to: "TOACC", - } + from: 'FROMACC', + note: 'NOTE', + to: 'TOACC', + }; const error = RequestErrors.None; - const task = new Task().sign(transaction, error); - expect(MessageBuilder.promise).toHaveBeenLastCalledWith(JsonRpcMethod.SignTransaction, transaction, error); + const task = new Task(); + task.sign(transaction, error); + expect(MessageBuilder.promise).toHaveBeenLastCalledWith( + JsonRpcMethod.SignTransaction, + transaction, + error + ); }); -}); \ No newline at end of file +}); diff --git a/packages/dapp/src/fn/task.ts b/packages/dapp/src/fn/task.ts index da76ee03..d9169c83 100644 --- a/packages/dapp/src/fn/task.ts +++ b/packages/dapp/src/fn/task.ts @@ -1,93 +1,46 @@ -import {ITask} from './interfaces'; +import { ITask } from './interfaces'; -import {MessageBuilder} from '../messaging/builder'; +import { MessageBuilder } from '../messaging/builder'; import { Transaction, RequestErrors, MultisigTransaction } from '@algosigner/common/types'; import { JsonRpcMethod, JsonPayload } from '@algosigner/common/messaging/types'; import { Runtime } from '@algosigner/common/runtime/runtime'; export class Task extends Runtime implements ITask { - - static subscriptions: {[key: string]: Function} = {}; - - connect(): Promise { - return MessageBuilder.promise( - JsonRpcMethod.Authorization, - {} - ); - } - - accounts( - params: JsonPayload, - error: RequestErrors = RequestErrors.None - ): Promise{ - return MessageBuilder.promise( - JsonRpcMethod.Accounts, - params as JsonPayload, - error - ); - } - - sign( - params: Transaction, - error: RequestErrors = RequestErrors.None - ): Promise { - return MessageBuilder.promise( - JsonRpcMethod.SignTransaction, - params, - error - ); - } - - signMultisig( - params: MultisigTransaction, - error: RequestErrors = RequestErrors.None - ): Promise { - return MessageBuilder.promise( - JsonRpcMethod.SignMultisigTransaction, - params, - error - ); - } - - send( - params: Transaction, - error: RequestErrors = RequestErrors.None - ): Promise { - return MessageBuilder.promise( - JsonRpcMethod.SendTransaction, - params, - error - ); - } - - algod( - params: JsonPayload, - error: RequestErrors = RequestErrors.None - ): Promise{ - return MessageBuilder.promise( - JsonRpcMethod.Algod, - params, - error - ); - } - - indexer( - params: JsonPayload, - error: RequestErrors = RequestErrors.None - ): Promise{ - return MessageBuilder.promise( - JsonRpcMethod.Indexer, - params, - error - ); - } - - - subscribe( - eventName: string, - callback: Function - ) { - Task.subscriptions[eventName] = callback; - } -} \ No newline at end of file + static subscriptions: { [key: string]: Function } = {}; + + connect(): Promise { + return MessageBuilder.promise(JsonRpcMethod.Authorization, {}); + } + + accounts(params: JsonPayload, error: RequestErrors = RequestErrors.None): Promise { + return MessageBuilder.promise(JsonRpcMethod.Accounts, params as JsonPayload, error); + } + + sign(params: Transaction, error: RequestErrors = RequestErrors.None): Promise { + return MessageBuilder.promise(JsonRpcMethod.SignTransaction, params, error); + } + + signMultisig( + params: MultisigTransaction, + error: RequestErrors = RequestErrors.None + ): Promise { + return MessageBuilder.promise(JsonRpcMethod.SignMultisigTransaction, params, error); + } + + send(params: Transaction, error: RequestErrors = RequestErrors.None): Promise { + return MessageBuilder.promise(JsonRpcMethod.SendTransaction, params, error); + } + + algod(params: JsonPayload, error: RequestErrors = RequestErrors.None): Promise { + return MessageBuilder.promise(JsonRpcMethod.Algod, params, error); + } + + indexer(params: JsonPayload, error: RequestErrors = RequestErrors.None): Promise { + return MessageBuilder.promise(JsonRpcMethod.Indexer, params, error); + } + + subscribe(eventName: string, callback: Function) { + Task.subscriptions[eventName] = callback; + } +} diff --git a/packages/dapp/src/index.ts b/packages/dapp/src/index.ts index 83433cb9..b4bc0d7a 100644 --- a/packages/dapp/src/index.ts +++ b/packages/dapp/src/index.ts @@ -1 +1 @@ -export {AlgoSigner} from './algosigner'; \ No newline at end of file +export { AlgoSigner } from './algosigner'; diff --git a/packages/dapp/src/messaging/api.ts b/packages/dapp/src/messaging/api.ts index f1077fd9..ed05bdac 100644 --- a/packages/dapp/src/messaging/api.ts +++ b/packages/dapp/src/messaging/api.ts @@ -1,21 +1,21 @@ -import {OnMessageListener} from './types'; -import {JsonRpcBody,MessageBody,MessageSource} from '@algosigner/common/messaging/types'; +import { OnMessageListener } from './types'; +import { JsonRpcBody, MessageBody, MessageSource } from '@algosigner/common/messaging/types'; export class MessageApi { - mc: MessageChannel; - constructor() { - this.mc = new MessageChannel(); - } + mc: MessageChannel; + constructor() { + this.mc = new MessageChannel(); + } - listen(handler: OnMessageListener) { - this.mc.port1.onmessage = handler; - } + listen(handler: OnMessageListener) { + this.mc.port1.onmessage = handler; + } - send(body: JsonRpcBody, source: MessageSource = MessageSource.DApp) { - let msg: MessageBody = { - source: source, - body: body - } - window.postMessage(msg, window.location.origin, [this.mc.port2]); - } -} \ No newline at end of file + send(body: JsonRpcBody, source: MessageSource = MessageSource.DApp) { + const msg: MessageBody = { + source: source, + body: body, + }; + window.postMessage(msg, window.location.origin, [this.mc.port2]); + } +} diff --git a/packages/dapp/src/messaging/builder.ts b/packages/dapp/src/messaging/builder.ts index 6bd87df4..0e5819da 100644 --- a/packages/dapp/src/messaging/builder.ts +++ b/packages/dapp/src/messaging/builder.ts @@ -1,31 +1,25 @@ - -import {RequestErrors} from '@algosigner/common/types'; -import {JsonRpcMethod,JsonPayload} from '@algosigner/common/messaging/types'; +import { RequestErrors } from '@algosigner/common/types'; +import { JsonRpcMethod, JsonPayload } from '@algosigner/common/messaging/types'; -import {JsonRpc} from '@algosigner/common/messaging/jsonrpc'; +import { JsonRpc } from '@algosigner/common/messaging/jsonrpc'; -import {MessageApi} from './api'; -import {OnMessageHandler} from './handler'; +import { MessageApi } from './api'; +import { OnMessageHandler } from './handler'; export class MessageBuilder { - static promise( - method: JsonRpcMethod, - params: JsonPayload, - error: RequestErrors = RequestErrors.None - ): Promise { - - return new Promise((resolve,reject) => { - if(error == RequestErrors.None) { - let api = new MessageApi(); - api.listen(OnMessageHandler.promise(resolve,reject)); - api.send(JsonRpc.getBody( - method, - params - )); - } else { - reject(error); - } - }); - - } -} \ No newline at end of file + static promise( + method: JsonRpcMethod, + params: JsonPayload, + error: RequestErrors = RequestErrors.None + ): Promise { + return new Promise((resolve, reject) => { + if (error == RequestErrors.None) { + const api = new MessageApi(); + api.listen(OnMessageHandler.promise(resolve, reject)); + api.send(JsonRpc.getBody(method, params)); + } else { + reject(error); + } + }); + } +} diff --git a/packages/dapp/src/messaging/handler.ts b/packages/dapp/src/messaging/handler.ts index 39090e48..df5d30c6 100644 --- a/packages/dapp/src/messaging/handler.ts +++ b/packages/dapp/src/messaging/handler.ts @@ -1,16 +1,16 @@ -import {OnMessageListener} from './types'; -import {RequestErrors} from '@algosigner/common/types'; +import { OnMessageListener } from './types'; +import { RequestErrors } from '@algosigner/common/types'; export class OnMessageHandler { - static promise(resolve: Function,reject: Function): OnMessageListener { - return (event) => { - if (event.data.error) { - reject(event.data.error); - } else if (event.data.response) { - resolve(event.data.response); - } else { - reject(RequestErrors.Undefined); - } - } - } -} \ No newline at end of file + static promise(resolve: Function, reject: Function): OnMessageListener { + return (event) => { + if (event.data.error) { + reject(event.data.error); + } else if (event.data.response) { + resolve(event.data.response); + } else { + reject(RequestErrors.Undefined); + } + }; + } +} diff --git a/packages/dapp/src/messaging/types.ts b/packages/dapp/src/messaging/types.ts index 2b2e17c2..bbc9d6fe 100644 --- a/packages/dapp/src/messaging/types.ts +++ b/packages/dapp/src/messaging/types.ts @@ -1 +1,2 @@ -export type OnMessageListener = (this: MessagePort, event: MessageEvent) => void; \ No newline at end of file +/* eslint-disable no-unused-vars */ +export type OnMessageListener = (this: MessagePort, event: MessageEvent) => void; diff --git a/packages/dapp/webpack.config.js b/packages/dapp/webpack.config.js index 30e8c9e0..5f218953 100644 --- a/packages/dapp/webpack.config.js +++ b/packages/dapp/webpack.config.js @@ -1,40 +1,40 @@ var path = require('path'); function srcPath(subdir) { - return path.join(__dirname, "./", subdir); + return path.join(__dirname, './', subdir); } module.exports = { - // Change to your "entry-point". - mode: 'production', - entry: './src/index', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'AlgoSigner.min.js', - libraryTarget: "umd" + // Change to your "entry-point". + mode: 'production', + entry: './src/index', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'AlgoSigner.min.js', + libraryTarget: 'umd', + }, + resolve: { + alias: { + '@algosigner/common': srcPath('../common/src'), }, - resolve: { - alias: { - "@algosigner/common": srcPath('../common/src'), - }, - extensions: ['.ts', '.tsx', '.js', '.json'] - }, - optimization: { - minimize: false, - namedModules: true - }, - module: { - rules: [ - { - test: /\.(ts|js)x?$/, - exclude: /node_modules/, - use: [ - { - loader: "ts-loader", - options: {} - } - ] - } - ] - } -}; \ No newline at end of file + extensions: ['.ts', '.tsx', '.js', '.json'], + }, + optimization: { + minimize: false, + namedModules: true, + }, + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader', + options: {}, + }, + ], + }, + ], + }, +}; diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index d42a857f..a056e4ec 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.3.0", + "version": "1.4.0", "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 cad8a46a..1c9795ff 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-extension", - "version": "1.3.0", + "version": "1.4.0", "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 f263b619..5e3b93df 100644 --- a/packages/extension/src/background/config.ts +++ b/packages/extension/src/background/config.ts @@ -1,38 +1,149 @@ +import { LedgerTemplate } from '@algosigner/common/types/ledgers'; import { Ledger, Backend, API } from './messaging/types'; export class Settings { - static backend: Backend = Backend.PureStake; - static backend_settings: {[key: string]: any} = { - [Backend.PureStake]: { - [Ledger.TestNet]: { - [API.Algod] : { - url: "https://algosigner.api.purestake.io/testnet/algod", - port: "" - }, - [API.Indexer] : { - url: "https://algosigner.api.purestake.io/testnet/indexer", - port: "" - } - }, - [Ledger.MainNet]: { - [API.Algod] : { - url: "https://algosigner.api.purestake.io/mainnet/algod", - port: "" - }, - [API.Indexer] : { - url: "https://algosigner.api.purestake.io/mainnet/indexer", - port: "" - }, - }, - apiKey: {} - } + static backend: Backend = Backend.PureStake; + static backend_settings: { [key: string]: any } = { + [Backend.PureStake]: { + [Ledger.TestNet]: { + [API.Algod]: { + url: 'https://algosigner.api.purestake.io/testnet/algod', + port: '', + }, + [API.Indexer]: { + url: 'https://algosigner.api.purestake.io/testnet/indexer', + port: '', + }, + }, + [Ledger.MainNet]: { + [API.Algod]: { + url: 'https://algosigner.api.purestake.io/mainnet/algod', + port: '', + }, + [API.Indexer]: { + url: 'https://algosigner.api.purestake.io/mainnet/indexer', + port: '', + }, + }, + apiKey: {}, + }, + InjectedNetworks: {}, + }; + + public static deleteInjectedNetwork(ledgerUniqueName: string) { + delete this.backend_settings.InjectedNetworks[ledgerUniqueName]; + } + + // Returns a copy of Injected networks with just basic information for dApp or display. + public static getCleansedInjectedNetworks() { + const injectedNetworks = []; + const injectedNetworkKeys = Object.keys(this.backend_settings.InjectedNetworks); + for (var i = 0; i < injectedNetworkKeys.length; i++) { + injectedNetworks.push({ + name: this.backend_settings.InjectedNetworks[injectedNetworkKeys[i]].name, + genesisId: this.backend_settings.InjectedNetworks[injectedNetworkKeys[i]].genesisId, + }); } - public static getBackendParams(ledger: Ledger, api: API) { - return { - url: this.backend_settings[this.backend][ledger][api].url, - port: this.backend_settings[this.backend][ledger][api].port, - apiKey: this.backend_settings[this.backend].apiKey + return injectedNetworks; + } + + private static setInjectedHeaders(ledger: LedgerTemplate) { + if (!this.backend_settings.InjectedNetworks[ledger.name]) { + console.log('Error: Ledger headers can not be updated. Ledger not available.'); + return; + } + + // Initialize headers for apiKey and individuals if there + let headers = {}; + let headersAlgod = undefined; + let headersIndexer = undefined; + if (ledger['headers']) { + // Set the headers to the base level first, this allows a string key to be used + headers = ledger['headers']; + + // Then try to parse the headers, in the case it is a string object. + try { + headers = JSON.parse(ledger['headers']); + } catch (e) { + // Use headers default value, but use it as a token if if 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}; } + } + + // Get individual sub headers if they are available + if (headers['Algod']) { + headersAlgod = headers['Algod']; + } + if (headers['Indexer']) { + headersIndexer = headers['Indexer']; + } + } + + // Add the algod links defaulting the url to one based on the genesisId + let defaultUrl = 'https://algosigner.api.purestake.io/mainnet'; + if (ledger.genesisId && ledger.genesisId.indexOf('testnet') > -1) { + defaultUrl = 'https://algosigner.api.purestake.io/testnet'; } -}; \ No newline at end of file + this.backend_settings.InjectedNetworks[ledger.name][API.Algod] = { + url: ledger.algodUrl || `${defaultUrl}/algod`, + 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: '', + apiKey: headersIndexer || headers, + headers: headersIndexer || headers, + }; + + this.backend_settings.InjectedNetworks[ledger.name].headers = headers; + } + + public static addInjectedNetwork(ledger: LedgerTemplate) { + // Initialize the injected network with the genesisId and a name that mimics the ledger for reference + this.backend_settings.InjectedNetworks[ledger.name] = { + name: ledger.name, + genesisId: ledger.genesisId || '', + }; + + this.setInjectedHeaders(ledger); + } + + public static updateInjectedNetwork(updatedLedger: LedgerTemplate) { + this.backend_settings.InjectedNetworks[updatedLedger.name].genesisId = updatedLedger.genesisId; + this.backend_settings.InjectedNetworks[updatedLedger.name].symbol = updatedLedger.symbol; + this.backend_settings.InjectedNetworks[updatedLedger.name].genesisHash = + updatedLedger.genesisHash; + this.backend_settings.InjectedNetworks[updatedLedger.name].algodUrl = updatedLedger.algodUrl; + this.backend_settings.InjectedNetworks[updatedLedger.name].indexerUrl = + updatedLedger.indexerUrl; + this.setInjectedHeaders(updatedLedger); + } + + public static getBackendParams(ledger: string, api: API) { + // If we are using the PureStake backend we can return the url, port, and apiKey + if (this.backend_settings[this.backend][ledger]) { + return { + url: this.backend_settings[this.backend][ledger][api].url, + port: this.backend_settings[this.backend][ledger][api].port, + apiKey: this.backend_settings[this.backend].apiKey, + headers: {}, + }; + } + + // Here we have to grab data from injected networks instead of the backend + return { + url: this.backend_settings.InjectedNetworks[ledger][api].url, + port: '', + apiKey: this.backend_settings.InjectedNetworks[ledger][api].apiKey, + headers: this.backend_settings.InjectedNetworks[ledger][api].headers, + }; + } +} diff --git a/packages/extension/src/background/messaging/internalMethods.test.ts b/packages/extension/src/background/messaging/internalMethods.test.ts index 65f2f617..89af6628 100644 --- a/packages/extension/src/background/messaging/internalMethods.test.ts +++ b/packages/extension/src/background/messaging/internalMethods.test.ts @@ -95,6 +95,7 @@ describe('wallet flow', () => { InternalMethods[JsonRpcMethod.CreateWallet](request, sendResponse); expect(sendResponse).toHaveBeenCalledWith({ + availableLedgers: [], ledger: Ledger.MainNet, wallet: { TestNet: [], @@ -117,6 +118,7 @@ describe('wallet flow', () => { const sendResponse = jest.fn(); const session = { + availableLedgers: [], ledger: Ledger.MainNet, wallet: { TestNet: [], diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 8320b89f..c215c5c9 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -1,3 +1,6 @@ +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const algosdk = require('algosdk'); + import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { logging } from '@algosigner/common/logging'; import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; @@ -7,47 +10,42 @@ import { Settings } from '../config'; import encryptionWrap from '../encryptionWrap'; import Session from '../utils/session'; import AssetsDetailsHelper from '../utils/assetsDetailsHelper'; -import { initializeCache } from '../utils/helper'; +import { initializeCache, getAvailableLedgersExt } from '../utils/helper'; import { ValidationStatus } from '../utils/validator'; import { getValidatedTxnWrap } from '../transaction/actions'; import { buildTransaction } from '../utils/transactionBuilder'; -/* eslint-disable-next-line @typescript-eslint/no-var-requires */ -const algosdk = require('algosdk'); +import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; const session = new Session(); export class InternalMethods { private static _encryptionWrap: encryptionWrap | undefined; - public static getAlgod(ledger: Ledger) { + public static getAlgod(ledger: string) { const params = Settings.getBackendParams(ledger, API.Algod); return new algosdk.Algodv2(params.apiKey, params.url, params.port); } - public static getIndexer(ledger: Ledger) { + public static getIndexer(ledger: string) { const params = Settings.getBackendParams(ledger, API.Indexer); return new algosdk.Indexer(params.apiKey, params.url, params.port); } private static safeWallet(wallet: any) { - let safeWallet: { TestNet: any[]; MainNet: any[] } = { - TestNet: [], - MainNet: [], - }; + // Intialize the safe wallet then add the wallet ledgers in as empty arrays + const safeWallet = {}; + Object.keys(wallet).forEach((key) => { + safeWallet[key] = []; + + // Afterwards we can add in all the non-private keys and names into the safewallet + for (var j = 0; j < wallet[key].length; j++) { + const { address, name } = wallet[key][j]; + safeWallet[key].push({ + address: address, + name: name, + }); + } + }); - for (var i = 0; i < wallet.TestNet.length; i++) { - const { address, name } = wallet['TestNet'][i]; - safeWallet.TestNet.push({ - address: address, - name: name, - }); - } - for (var i = 0; i < wallet.MainNet.length; i++) { - const { address, name } = wallet['MainNet'][i]; - safeWallet.MainNet.push({ - address: address, - name: name, - }); - } return safeWallet; } @@ -77,10 +75,14 @@ export class InternalMethods { }); } - public static getHelperSession() { + public static getHelperSession(): Session { return session.session; } + public static clearSession(): void { + session.clearSession(); + } + public static [JsonRpcMethod.GetSession](request: any, sendResponse: Function) { this._encryptionWrap = new encryptionWrap(''); @@ -140,26 +142,30 @@ export class InternalMethods { if ('error' in response) { sendResponse(response); } else { - let wallet = this.safeWallet(response); - // Load Accounts details from Cache - new ExtensionStorage().getStorage('cache', (storedCache: any) => { - let cache: Cache = initializeCache(storedCache); - let cachedLedgers = Object.keys(cache.accounts); - - console.log('cached', cachedLedgers); - for (var j = cachedLedgers.length - 1; j >= 0; j--) { - const ledger = cachedLedgers[j]; - console.log('cached', cachedLedgers); - for (var i = wallet[ledger].length - 1; i >= 0; i--) { - if (wallet[ledger][i].address in cache.accounts[ledger]) { - wallet[ledger][i].details = cache.accounts[ledger][wallet[ledger][i].address]; + const wallet = this.safeWallet(response); + getAvailableLedgersExt((availableLedgers) => { + const extensionStorage = new ExtensionStorage(); + // Load Accounts details from Cache + extensionStorage.getStorage('cache', (storedCache: any) => { + const cache: Cache = initializeCache(storedCache); + const cachedLedgerAccounts = Object.keys(cache.accounts); + + for (var j = cachedLedgerAccounts.length - 1; j >= 0; j--) { + const ledger = cachedLedgerAccounts[j]; + if (wallet[ledger]) { + for (var i = wallet[ledger].length - 1; i >= 0; i--) { + if (wallet[ledger][i].address in cache.accounts[ledger]) { + wallet[ledger][i].details = cache.accounts[ledger][wallet[ledger][i].address]; + } + } } } - } - (session.wallet = wallet), - (session.ledger = Ledger.MainNet), - sendResponse(session.session); + (session.wallet = wallet), + (session.ledger = Ledger.MainNet), + (session.availableLedgers = availableLedgers), + sendResponse(session.session); + }); }); } }); @@ -180,11 +186,16 @@ export class InternalMethods { if ('error' in unlockedValue) { sendResponse(unlockedValue); } else { - let newAccount = { + const newAccount = { address: address, mnemonic: mnemonic, name: name, }; + + if (!unlockedValue[ledger]) { + unlockedValue[ledger] = []; + } + unlockedValue[ledger].push(newAccount); this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { if (isSuccessful) { @@ -234,11 +245,15 @@ export class InternalMethods { try { var recoveredAccountAddress = algosdk.mnemonicToSecretKey(mnemonic).addr; var existingAccounts = session.wallet[ledger]; - for (let i = 0; i < existingAccounts.length; i++) { - if (existingAccounts[i].address === recoveredAccountAddress) { - throw new Error(`Account already exists in ${ledger} wallet.`); + + if (existingAccounts) { + for (let i = 0; i < existingAccounts.length; i++) { + if (existingAccounts[i].address === recoveredAccountAddress) { + throw new Error(`Account already exists in ${ledger} wallet.`); + } } } + var newAccount = { address: recoveredAccountAddress, mnemonic: mnemonic, @@ -253,6 +268,10 @@ export class InternalMethods { if ('error' in unlockedValue) { sendResponse(unlockedValue); } else { + if (!unlockedValue[ledger]) { + unlockedValue[ledger] = []; + } + unlockedValue[ledger].push(newAccount); this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { if (isSuccessful) { @@ -275,13 +294,13 @@ export class InternalMethods { .accountInformation(address) .do() .then((res: any) => { - let extensionStorage = new ExtensionStorage(); + const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (storedCache: any) => { - let cache: Cache = initializeCache(storedCache, ledger); + const cache: Cache = initializeCache(storedCache, ledger); // Check for asset details saved in storage, if needed if ('assets' in res && res.assets.length > 0) { - let missingAssets = []; + const missingAssets = []; for (var i = res.assets.length - 1; i >= 0; i--) { const assetId = res.assets[i]['asset-id']; if (assetId in cache.assets[ledger]) { @@ -306,10 +325,13 @@ export class InternalMethods { extensionStorage.setStorage('cache', cache, null); // Add details to session - let wallet = session.wallet; - for (var i = wallet[ledger].length - 1; i >= 0; i--) { - if (wallet[ledger][i].address === address) { - wallet[ledger][i].details = res; + const wallet = session.wallet; + // Validate the ledger still exists in the wallet + if (ledger && wallet[ledger]) { + for (var i = wallet[ledger].length - 1; i >= 0; i--) { + if (wallet[ledger][i].address === address) { + wallet[ledger][i].details = res; + } } } }); @@ -377,14 +399,14 @@ export class InternalMethods { public static [JsonRpcMethod.AssetDetails](request: any, sendResponse: Function) { const assetId = request.body.params['asset-id']; const { ledger } = request.body.params; - let indexer = this.getIndexer(ledger); + const indexer = this.getIndexer(ledger); indexer .lookupAssetByID(assetId) .do() .then((res: any) => { sendResponse(res); // Save asset details in storage if needed - let extensionStorage = new ExtensionStorage(); + const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (cache: any) => { if (cache === undefined) cache = new Cache(); if (!(ledger in cache.assets)) cache.assets[ledger] = {}; @@ -408,7 +430,7 @@ export class InternalMethods { req .do() .then((res: any) => { - let newAssets = assets.concat(res.assets); + const newAssets = assets.concat(res.assets); for (var i = newAssets.length - 1; i >= 0; i--) { newAssets[i] = { asset_id: newAssets[i].index, @@ -426,7 +448,7 @@ export class InternalMethods { } const { ledger, filter, nextToken } = request.body.params; - let indexer = this.getIndexer(ledger); + const indexer = this.getIndexer(ledger); // Do the search for asset id (if filter value is integer) // and asset name and concat them. if (filter.length > 0 && !isNaN(filter) && (!nextToken || nextToken.length === 0)) { @@ -492,8 +514,8 @@ export class InternalMethods { } var recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - let params = await algod.getTransactionParams().do(); - let txn = { + const params = await algod.getTransactionParams().do(); + const txn = { ...txnParams, fee: params.fee, firstRound: params.firstRound, @@ -533,14 +555,19 @@ export class InternalMethods { ) ) { // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - sendResponse({ error: 'One or more fields are not valid. Please check and try again.' }); + const e = + 'One or more fields are not valid. Please check and try again.\n' + + Object.values(transactionWrap.validityObject) + .filter((value) => value['status'] === ValidationStatus.Invalid) + .map((vo) => vo['info']); + sendResponse({ error: e }); return; } else { // We have a transaction which does not contain invalid fields, but may contain fields that are dangerous // or ones we've flagged as needing to be reviewed. We can use a modified popup to allow the normal flow, but require extra scrutiny. let signedTxn; try { - let builtTx = buildTransaction(txn); + const builtTx = buildTransaction(txn); signedTxn = { txID: builtTx.txID().toString(), blob: builtTx.signTxn(recoveredAccount.sk), @@ -571,4 +598,154 @@ export class InternalMethods { session.ledger = request.body.params['ledger']; sendResponse({ ledger: session.ledger }); } + + public static [JsonRpcMethod.DeleteNetwork](request: any, sendResponse: Function) { + const ledger = request.body.params['name']; + const ledgerUniqueName = ledger.toLowerCase(); + getAvailableLedgersExt((availiableLedgers) => { + const matchingLedger = availiableLedgers.find((avls) => avls.uniqueName === ledgerUniqueName); + + if (!matchingLedger || !matchingLedger.isEditable) { + sendResponse({ error: 'This ledger can not be deleted.' }); + } else { + // Delete ledger from availableLedgers and assign to new array // + const remainingLedgers = availiableLedgers.filter( + (avls) => avls.uniqueName !== ledgerUniqueName + ); + + // Delete Accounts from wallet // + this._encryptionWrap = new encryptionWrap(request.body.params.passphrase); + + // Remove existing accoutns in session.wallet + var existingAccounts = session.wallet[request.body.params['ledger']]; + if (existingAccounts) { + delete session.wallet[request.body.params['ledger']]; + } + + // Using the passphrase unlock the current saved data + this._encryptionWrap.unlock((unlockedValue: any) => { + if ('error' in unlockedValue) { + sendResponse(unlockedValue); + } else { + if (unlockedValue[ledger]) { + // The unlocked value contains ledger information - delete it + delete unlockedValue[ledger]; + } + + // Resave the updated wallet value + this._encryptionWrap + ?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { + if (isSuccessful) { + // Fully rebuild the session wallet + session.wallet = this.safeWallet(unlockedValue); + } + }) + .then(() => { + // Update cache with remaining ledgers// + const extensionStorage = new ExtensionStorage(); + extensionStorage.getStorage('cache', (cache: any) => { + if (cache === undefined) { + cache = initializeCache(cache); + } + if (cache) { + cache.availableLedgers = remainingLedgers; + extensionStorage.setStorage('cache', cache, () => {}); + } + + // Update the session // + session.availableLedgers = remainingLedgers; + + // Delete from the injected ledger settings // + Settings.deleteInjectedNetwork(ledgerUniqueName); + + // Send back remaining ledgers // + sendResponse({ availableLedgers: remainingLedgers }); + }); + }); + } + }); + } + }); + return true; + } + + public static [JsonRpcMethod.SaveNetwork](request: any, sendResponse: Function) { + try { + // If we have a passphrase then we are modifying. + // There may be accounts attatched, if we match on a unique name, we should update. + if (request.body.params['passphrase'] !== undefined) { + this._encryptionWrap = new encryptionWrap(request.body.params['passphrase']); + this._encryptionWrap.unlock((unlockedValue: any) => { + if ('error' in unlockedValue) { + sendResponse(unlockedValue); + } + // We have evaluated the passphrase and it was valid. + }); + } + const addedLedger = new LedgerTemplate({ + name: request.body.params['name'], + genesisId: request.body.params['genesisId'], + genesisHash: request.body.params['genesisHash'], + symbol: request.body.params['symbol'], + algodUrl: request.body.params['algodUrl'], + indexerUrl: request.body.params['indexerUrl'], + headers: request.body.params['headers'], + }); + + // Specifically get the base ledgers to check and prevent them from being overriden. + const defaultLedgers = getBaseSupportedLedgers(); + + getAvailableLedgersExt((availiableLedgers) => { + const comboLedgers = [...availiableLedgers]; + + // Add the new ledger if it isn't there. + if (!comboLedgers.some((cledg) => cledg.uniqueName === addedLedger.uniqueName)) { + comboLedgers.push(addedLedger); + + // Also add the ledger to the injected ledgers in settings + Settings.addInjectedNetwork(addedLedger); + } + // If the new ledger name does exist, we sould update the values as long as it is not a default ledger. + else { + const matchingLedger = comboLedgers.find( + (cledg) => cledg.uniqueName === addedLedger.uniqueName + ); + if (!defaultLedgers.some((dledg) => dledg.uniqueName === matchingLedger.uniqueName)) { + Settings.updateInjectedNetwork(addedLedger); + matchingLedger.genesisId = addedLedger.genesisId; + matchingLedger.symbol = addedLedger.symbol; + matchingLedger.genesisHash = addedLedger.genesisHash; + matchingLedger.algodUrl = addedLedger.algodUrl; + matchingLedger.indexerUrl = addedLedger.indexerUrl; + matchingLedger.headers = addedLedger.headers; + } + } + // Update the session and send response before setting cache. + session.availableLedgers = comboLedgers; + sendResponse({ availableLedgers: comboLedgers }); + + // Updated the cached ledgers. + const extensionStorage = new ExtensionStorage(); + extensionStorage.getStorage('cache', (cache: any) => { + if (cache === undefined) { + cache = initializeCache(cache); + } + if (cache) { + cache.availableLedgers = comboLedgers; + extensionStorage.setStorage('cache', cache, () => {}); + } + }); + }); + return true; + } catch (e) { + sendResponse({ error: e.message }); + } + } + public static [JsonRpcMethod.GetLedgers](request: any, sendResponse: Function) { + getAvailableLedgersExt((availableLedgers) => { + sendResponse(availableLedgers); + }); + + return true; + } } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 107b8f10..c6a995ca 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -1,17 +1,12 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable prefer-const */ /* eslint-disable-next-line @typescript-eslint/no-var-requires */ const algosdk = require('algosdk'); import { RequestErrors } from '@algosigner/common/types'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { API } from './types'; +import { API, Ledger } from './types'; import { getValidatedTxnWrap, - getLedgerFromGenesisID, + getLedgerFromGenesisId, calculateEstimatedFee, } from '../transaction/actions'; import { ValidationStatus } from '../utils/validator'; @@ -56,7 +51,7 @@ export class Task { resolve(json); }) .catch((error) => { - let res: Object = { + const res: Object = { message: error.message, data: error.data, }; @@ -66,8 +61,8 @@ export class Task { } public static build(request: any) { - let body = request.body; - let method = body.method; + const body = request.body; + const method = body.method; // Check if there's a previous request from the same origin if (request.originTabID in Task.requests) @@ -167,7 +162,7 @@ export class Task { ) { // 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. - let invalidKeys = []; + const invalidKeys = []; Object.entries(transactionWrap.validityObject).forEach(([key, value]) => { if (value['status'] === ValidationStatus.Invalid) { invalidKeys.push(`${key}`); @@ -182,13 +177,13 @@ export class Task { } else { // Get Ledger params const conn = Settings.getBackendParams( - getLedgerFromGenesisID(transactionWrap.transaction.genesisID), + getLedgerFromGenesisId(transactionWrap.transaction.genesisID), API.Algod ); const sendPath = '/v2/transactions/params'; const fetchParams: any = { headers: { - ...conn.apiKey, + ...conn.headers, }, method: 'GET', }; @@ -264,7 +259,7 @@ export class Task { ) { // 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. - let invalidKeys = []; + const invalidKeys = []; Object.entries(transactionWrap.validityObject).forEach(([key, value]) => { if (value['status'] === ValidationStatus.Invalid) { invalidKeys.push(`${key}`); @@ -279,13 +274,13 @@ export class Task { } else { // Get Ledger params const conn = Settings.getBackendParams( - getLedgerFromGenesisID(transactionWrap.transaction.genesisID), + getLedgerFromGenesisId(transactionWrap.transaction.genesisID), API.Algod ); const sendPath = '/v2/transactions/params'; const fetchParams: any = { headers: { - ...conn.apiKey, + ...conn.headers, }, method: 'GET', }; @@ -300,11 +295,11 @@ export class Task { d.body.params.txn = transactionWrap.transaction; d.body.params.estimatedFee = transactionWrap.estimatedFee; - let msig_txn = { msig: d.body.params.msig, txn: d.body.params.txn }; + const msig_txn = { msig: d.body.params.msig, txn: d.body.params.txn }; const session = InternalMethods.getHelperSession(); - const ledger = getLedgerFromGenesisID(transactionWrap.transaction.genesisID); + const ledger = getLedgerFromGenesisId(transactionWrap.transaction.genesisID); const accounts = session.wallet[ledger]; - let multisigAccounts = getSigningAccounts(accounts, msig_txn); + const multisigAccounts = getSigningAccounts(accounts, msig_txn); if (multisigAccounts.error) { d.error = multisigAccounts.error.message; @@ -342,9 +337,9 @@ export class Task { const { params } = d.body; const conn = Settings.getBackendParams(params.ledger, API.Algod); const sendPath = '/v2/transactions'; - let fetchParams: any = { + const fetchParams: any = { headers: { - ...conn.apiKey, + ...conn.headers, 'Content-Type': 'application/x-binary', }, method: 'POST', @@ -374,9 +369,9 @@ export class Task { const contentType = params.contentType ? params.contentType : ''; - let fetchParams: any = { + const fetchParams: any = { headers: { - ...conn.apiKey, + ...conn.headers, 'Content-Type': contentType, }, method: params.method || 'GET', @@ -403,9 +398,9 @@ export class Task { const contentType = params.contentType ? params.contentType : ''; - let fetchParams: any = { + const fetchParams: any = { headers: { - ...conn.apiKey, + ...conn.headers, 'Content-Type': contentType, }, method: params.method || 'GET', @@ -426,10 +421,33 @@ export class Task { }); }, // Accounts + /* eslint-disable-next-line no-unused-vars */ [JsonRpcMethod.Accounts]: (d: any, resolve: Function, reject: Function) => { const session = InternalMethods.getHelperSession(); + // If we don't have a ledger requested, respond with an error giving available ledgers + if (!d.body.params.ledger) { + const baseNetworks = Object.keys(Ledger); + const injectedNetworks = Settings.getCleansedInjectedNetworks(); + d.error = { + message: `Ledger not provided. Please use a base ledger: [${baseNetworks}] or an available custom one ${JSON.stringify( + injectedNetworks + )}.`, + }; + reject(d); + return; + } + const accounts = session.wallet[d.body.params.ledger]; - let res = []; + // If we have requested a ledger but don't have it, respond with an error + if (accounts === undefined) { + d.error = { + message: RequestErrors.UnsupportedLedger, + }; + reject(d); + return; + } + + const res = []; for (let i = 0; i < accounts.length; i++) { res.push({ address: accounts[i].address, @@ -443,8 +461,8 @@ export class Task { // authorization-allow [JsonRpcMethod.AuthorizationAllow]: (d) => { const { responseOriginTabID } = d.body.params; - let auth = Task.requests[responseOriginTabID]; - let message = auth.message; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; extensionBrowser.windows.remove(auth.window_id); Task.authorized_pool.push(message.origin); @@ -459,8 +477,8 @@ export class Task { // authorization-deny [JsonRpcMethod.AuthorizationDeny]: (d) => { const { responseOriginTabID } = d.body.params; - let auth = Task.requests[responseOriginTabID]; - let message = auth.message; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; auth.message.error = { message: RequestErrors.NotAuthorized, @@ -477,8 +495,8 @@ export class Task { // sign-allow [JsonRpcMethod.SignAllow]: (request: any, sendResponse: Function) => { const { passphrase, responseOriginTabID } = request.body.params; - let auth = Task.requests[responseOriginTabID]; - let message = auth.message; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; const { from, @@ -492,9 +510,9 @@ export class Task { // note, } = message.body.params.transaction; - const ledger = getLedgerFromGenesisID(genesisID); + const ledger = getLedgerFromGenesisId(genesisID); - let context = new encryptionWrap(passphrase); + const context = new encryptionWrap(passphrase); context.unlock(async (unlockedValue: any) => { if ('error' in unlockedValue) { sendResponse(unlockedValue); @@ -505,6 +523,10 @@ export class Task { let account; + 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) { @@ -513,9 +535,9 @@ export class Task { } } - let recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); + const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - let txn = { ...message.body.params.transaction }; + const txn = { ...message.body.params.transaction }; Object.keys({ ...message.body.params.transaction }).forEach((key) => { if (txn[key] === undefined || txn[key] === null) { @@ -548,7 +570,7 @@ export class Task { } if ('appArgs' in txn) { try { - let tempArgs = []; + const tempArgs = []; txn.appArgs.forEach((element) => { logging.log(element); tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); @@ -562,14 +584,14 @@ export class Task { try { // This step transitions a raw object into a transaction style object - let builtTx = buildTransaction(txn); + 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. - let signedTxn = { + const signedTxn = { txID: builtTx.txID().toString(), blob: builtTx.signTxn(recoveredAccount.sk), }; - let b64Obj = Buffer.from(signedTxn.blob).toString('base64'); + const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); message.response = { txID: signedTxn.txID, @@ -588,21 +610,17 @@ export class Task { // sign-allow-multisig [JsonRpcMethod.SignAllowMultisig]: (request: any, sendResponse: Function) => { const { passphrase, responseOriginTabID } = request.body.params; - let auth = Task.requests[responseOriginTabID]; - let message = auth.message; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; // Map the full multisig transaction here - let msig_txn = { msig: message.body.params.msig, txn: message.body.params.txn }; + const msig_txn = { msig: message.body.params.msig, txn: message.body.params.txn }; // Use MainNet if specified - default to TestNet - let ledger = getLedgerFromGenesisID(msig_txn.txn.genesisID); - - // Get parameters and connect the SDK - const params = Settings.getBackendParams(ledger, API.Algod); - const algod = new algosdk.Algodv2(params.apiKey, params.url, params.port); + const ledger = getLedgerFromGenesisId(msig_txn.txn.genesisID); // Create an encryption wrap to get the needed signing account information - let context = new encryptionWrap(passphrase); + const context = new encryptionWrap(passphrase); context.unlock(async (unlockedValue: any) => { if ('error' in unlockedValue) { sendResponse(unlockedValue); @@ -614,7 +632,7 @@ export class Task { // 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; - let multisigAccounts = getSigningAccounts(unlockedValue[ledger], msig_txn); + const multisigAccounts = getSigningAccounts(unlockedValue[ledger], msig_txn); if (multisigAccounts.error) { message.error = multisigAccounts.error.message; } else { @@ -624,7 +642,7 @@ export class Task { if (account) { // We can now use the found account match to get the sign key - let recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); + 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) => { @@ -661,7 +679,7 @@ export class Task { } if ('appArgs' in msig_txn.txn) { try { - let tempArgs = []; + const tempArgs = []; msig_txn.txn.appArgs.forEach((element) => { tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); }); @@ -674,30 +692,30 @@ export class Task { try { // This step transitions a raw object into a transaction style object - let builtTx = buildTransaction(msig_txn.txn); + const builtTx = buildTransaction(msig_txn.txn); // Building preimg - This allows the pks to be passed, but still use the default multisig sign with addrs - let version = msig_txn.msig.v || msig_txn.msig.version; - let threshold = msig_txn.msig.thr || msig_txn.msig.threshold; - let 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; }); - let preimg = { + const preimg = { version: version, threshold: threshold, addrs: addrs, }; let signedTxn; - let appendEnabled = false; // TODO: This disables append functionality until blob objects are allowed and validated. + 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. - let encodedBlob = message.body.params.txn; - let decodedBlob = Buffer.from(encodedBlob, 'base64'); + const encodedBlob = message.body.params.txn; + const decodedBlob = Buffer.from(encodedBlob, 'base64'); signedTxn = algosdk.appendSignMultisigTransaction( decodedBlob, preimg, @@ -709,7 +727,7 @@ export class Task { } // Converting the blob to an encoded string for transfer back to dApp - let b64Obj = Buffer.from(signedTxn.blob).toString('base64'); + const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); message.response = { txID: signedTxn.txID, @@ -725,10 +743,11 @@ export class Task { }); return true; }, + /* eslint-disable-next-line no-unused-vars */ [JsonRpcMethod.SignDeny]: (request: any, sendResponse: Function) => { const { responseOriginTabID } = request.body.params; - let auth = Task.requests[responseOriginTabID]; - let message = auth.message; + const auth = Task.requests[responseOriginTabID]; + const message = auth.message; auth.message.error = { message: RequestErrors.NotAuthorized, @@ -752,6 +771,11 @@ export class Task { [JsonRpcMethod.Login]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.Login](request, sendResponse); }, + /* eslint-disable-next-line no-unused-vars */ + [JsonRpcMethod.Logout]: (request: any, sendResponse: Function) => { + InternalMethods.clearSession(); + Task.clearPool(); + }, [JsonRpcMethod.GetSession]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.GetSession](request, sendResponse); }, @@ -785,6 +809,15 @@ export class Task { [JsonRpcMethod.ChangeLedger]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.ChangeLedger](request, sendResponse); }, + [JsonRpcMethod.SaveNetwork]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.SaveNetwork](request, sendResponse); + }, + [JsonRpcMethod.DeleteNetwork]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.DeleteNetwork](request, sendResponse); + }, + [JsonRpcMethod.GetLedgers]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.GetLedgers](request, sendResponse); + }, }, }; } diff --git a/packages/extension/src/background/messaging/types.ts b/packages/extension/src/background/messaging/types.ts index 05c2b279..da0eafa5 100644 --- a/packages/extension/src/background/messaging/types.ts +++ b/packages/extension/src/background/messaging/types.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import { LedgerTemplate } from '@algosigner/common/types/ledgers'; + // Key and value must match in this enum so we // can compare its existance with i.e. "Testnet" in SupportedLedger /* eslint-disable no-unused-vars */ @@ -35,7 +39,13 @@ export interface Cache { ], ... }, + availableLedgers: [ + { + ... + } + ], */ assets: object; accounts: object; + availableLedgers: Array; } diff --git a/packages/extension/src/background/transaction/actions.ts b/packages/extension/src/background/transaction/actions.ts index 94135358..517ab8ed 100644 --- a/packages/extension/src/background/transaction/actions.ts +++ b/packages/extension/src/background/transaction/actions.ts @@ -1,34 +1,36 @@ -import { IPaymentTx } from "@algosigner/common/interfaces/pay"; -import { IAssetConfigTx } from "@algosigner/common/interfaces/acfg"; -import { IAssetCreateTx } from "@algosigner/common/interfaces/acfg_create"; -import { IAssetDestroyTx } from "@algosigner/common/interfaces/acfg_destroy"; -import { IAssetFreezeTx } from "@algosigner/common/interfaces/afrz"; -import { IAssetTransferTx } from "@algosigner/common/interfaces/axfer"; -import { IAssetAcceptTx } from "@algosigner/common/interfaces/axfer_accept"; -import { IAssetClawbackTx } from "@algosigner/common/interfaces/axfer_clawback"; -import { IKeyRegistrationTx } from "@algosigner/common/interfaces/keyreg"; -import { IApplTx } from "@algosigner/common/interfaces/appl"; -import { PayTransaction } from "./payTransaction"; -import { AssetConfigTransaction } from "./acfgTransaction"; -import { AssetCreateTransaction } from "./acfgCreateTransaction"; -import { AssetDestroyTransaction } from "./acfgDestroyTransaction"; -import { AssetFreezeTransaction } from "./afrzTransaction"; -import { AssetTransferTransaction } from "./axferTransaction"; -import { AssetAcceptTransaction } from "./axferAcceptTransaction"; -import { AssetClawbackTransaction } from "./axferClawbackTransaction"; -import { KeyregTransaction } from "./keyregTransaction"; -import { ApplTransaction } from "./applTransaction"; -import { TransactionType } from "@algosigner/common/types/transaction"; -import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; -import { Ledger } from '../messaging/types'; +/* eslint-disable @typescript-eslint/ban-types */ +import { IPaymentTx } from '@algosigner/common/interfaces/pay'; +import { IAssetConfigTx } from '@algosigner/common/interfaces/acfg'; +import { IAssetCreateTx } from '@algosigner/common/interfaces/acfg_create'; +import { IAssetDestroyTx } from '@algosigner/common/interfaces/acfg_destroy'; +import { IAssetFreezeTx } from '@algosigner/common/interfaces/afrz'; +import { IAssetTransferTx } from '@algosigner/common/interfaces/axfer'; +import { IAssetAcceptTx } from '@algosigner/common/interfaces/axfer_accept'; +import { IAssetClawbackTx } from '@algosigner/common/interfaces/axfer_clawback'; +import { IKeyRegistrationTx } from '@algosigner/common/interfaces/keyreg'; +import { IApplTx } from '@algosigner/common/interfaces/appl'; +import { PayTransaction } from './payTransaction'; +import { AssetConfigTransaction } from './acfgTransaction'; +import { AssetCreateTransaction } from './acfgCreateTransaction'; +import { AssetDestroyTransaction } from './acfgDestroyTransaction'; +import { AssetFreezeTransaction } from './afrzTransaction'; +import { AssetTransferTransaction } from './axferTransaction'; +import { AssetAcceptTransaction } from './axferAcceptTransaction'; +import { AssetClawbackTransaction } from './axferClawbackTransaction'; +import { KeyregTransaction } from './keyregTransaction'; +import { ApplTransaction } from './applTransaction'; +import { TransactionType } from '@algosigner/common/types/transaction'; +import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; +import { Settings } from '../config'; +import { getBaseSupportedLedgers } from '@algosigner/common/types/ledgers'; /* eslint-disable-next-line @typescript-eslint/no-var-requires */ const algosdk = require('algosdk'); /// -// Sign transaction and return. +// Sign transaction and return. /// -export function getValidatedTxnWrap(txn: object, type: string):BaseValidatedTxnWrap { +export function getValidatedTxnWrap(txn: object, type: string): BaseValidatedTxnWrap { let validatedTxnWrap: BaseValidatedTxnWrap = undefined; let error: Error = undefined; @@ -54,9 +56,7 @@ export function getValidatedTxnWrap(txn: object, type: string):BaseValidatedTxnW } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetDestroyTransaction( - txn as IAssetDestroyTx - ); + validatedTxnWrap = new AssetDestroyTransaction(txn as IAssetDestroyTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -79,9 +79,7 @@ export function getValidatedTxnWrap(txn: object, type: string):BaseValidatedTxnW } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetTransferTransaction( - txn as IAssetTransferTx - ); + validatedTxnWrap = new AssetTransferTransaction(txn as IAssetTransferTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -89,9 +87,7 @@ export function getValidatedTxnWrap(txn: object, type: string):BaseValidatedTxnW } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetClawbackTransaction( - txn as IAssetClawbackTx - ); + validatedTxnWrap = new AssetClawbackTransaction(txn as IAssetClawbackTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -114,17 +110,26 @@ export function getValidatedTxnWrap(txn: object, type: string):BaseValidatedTxnW return validatedTxnWrap; } -export function getLedgerFromGenesisID(genesisID: string):Ledger { - let ledger; - if (genesisID === 'mainnet-v1.0') ledger = Ledger.MainNet; - else if (genesisID === 'testnet-v1.0') ledger = Ledger.TestNet; - return ledger; +export function getLedgerFromGenesisId(genesisId: string) { + // Default the ledger to mainnet + const defaultLedger = 'MainNet'; + + // Check Genesis ID for base supported ledgers first + const defaultLedgers = getBaseSupportedLedgers(); + let ledger = defaultLedgers.find((l) => genesisId === l['genesisId']); + if (ledger !== undefined) { + return ledger.name; + } + // Injected networks may have additional information, multiples, or additional checks + // so we will check them separately + ledger = Settings.getCleansedInjectedNetworks().find((l) => genesisId === l['genesisId']); + if (ledger !== undefined) { + return ledger.name; + } + return defaultLedger; } -export function calculateEstimatedFee( - transactionWrap: BaseValidatedTxnWrap, - params: any -): void { +export function calculateEstimatedFee(transactionWrap: BaseValidatedTxnWrap, params: any): void { const transaction = transactionWrap.transaction; const minFee = +params['min-fee']; let estimatedFee = +transaction['fee']; @@ -150,18 +155,16 @@ export function calculateEstimatedFee( const dummyTransaction = {}; Object.keys(transaction).map((key, index) => { if (transaction[key]) { - dummyTransaction[index.toString().padStart(4, '0')] = - transaction[key]; + dummyTransaction[index.toString().padStart(4, '0')] = transaction[key]; } }); // We use algosdk to encode our dummy transaction into MessagePack // and use the resulting MessagePack to determine an estimate byte size - const transactionSize: number = algosdk.encodeObj(dummyTransaction) - .byteLength; + const transactionSize: number = algosdk.encodeObj(dummyTransaction).byteLength; // Finally we estimate the final fee with the dApp fee // and our estimated transaction byte-size estimatedFee = dappFee * transactionSize; } } transactionWrap.estimatedFee = estimatedFee; -} \ No newline at end of file +} diff --git a/packages/extension/src/background/utils/assetsDetailsHelper.ts b/packages/extension/src/background/utils/assetsDetailsHelper.ts index 623cc310..58f2566e 100644 --- a/packages/extension/src/background/utils/assetsDetailsHelper.ts +++ b/packages/extension/src/background/utils/assetsDetailsHelper.ts @@ -1,6 +1,6 @@ -import { ExtensionStorage } from "@algosigner/storage/src/extensionStorage"; +import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; import { InternalMethods } from '../messaging/internalMethods'; -import { Cache, Ledger } from "../messaging/types" +import { Cache, Ledger } from '../messaging/types'; import { initializeCache } from './helper'; const TIMEOUT = 500; @@ -9,49 +9,58 @@ const TIMEOUT = 500; // Helper class for getting and saving the details of assets in an ordered fashion /// export default class AssetsDetailsHelper { - private static assetsToAdd : {[key: string]: Array} = { - [Ledger.TestNet]: [], - [Ledger.MainNet]: [] - } + private static assetsToAdd: { [key: string]: Array } = { + [Ledger.TestNet]: [], + [Ledger.MainNet]: [], + }; + + private static timeouts = { + [Ledger.TestNet]: null, + [Ledger.MainNet]: null, + }; - private static timeouts = { - [Ledger.TestNet]: null, - [Ledger.MainNet]: null + public static add(assets: Array, ledger: Ledger) { + // If this ledger doesn't have assets yet, then default them to an array + if (this.assetsToAdd[ledger] === undefined) { + this.assetsToAdd[ledger] = []; } - public static add(assets: Array, ledger: Ledger) { - this.assetsToAdd[ledger] = this.assetsToAdd[ledger].concat(assets); - if (this.timeouts[ledger] === null && this.assetsToAdd[ledger].length > 0) - this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); + this.assetsToAdd[ledger] = this.assetsToAdd[ledger].concat(assets); + if (this.timeouts[ledger] === null && this.assetsToAdd[ledger].length > 0) + this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); + } + + private static run(ledger: Ledger) { + if (this.assetsToAdd[ledger].length === 0) { + this.timeouts[ledger] = null; + return; } - private static run(ledger: Ledger) { - if (this.assetsToAdd[ledger].length === 0){ - this.timeouts[ledger] = null; - return; + const extensionStorage = new ExtensionStorage(); + extensionStorage.getStorage('cache', (storedCache: any) => { + const cache: Cache = initializeCache(storedCache, ledger); + + let assetId = this.assetsToAdd[ledger][0]; + while (assetId in cache.assets[ledger]) { + this.assetsToAdd[ledger].shift(); + if (this.assetsToAdd[ledger].length === 0) { + this.timeouts[ledger] = null; + return; } + assetId = this.assetsToAdd[ledger][0]; + } - let extensionStorage = new ExtensionStorage(); - extensionStorage.getStorage('cache', (storedCache: any) => { - let cache: Cache = initializeCache(storedCache, ledger); - - let assetId = this.assetsToAdd[ledger][0]; - while (assetId in cache.assets[ledger]) { - this.assetsToAdd[ledger].shift(); - if (this.assetsToAdd[ledger].length === 0) { - this.timeouts[ledger] = null; - return; - } - assetId = this.assetsToAdd[ledger][0]; - } - - let indexer = InternalMethods.getIndexer(ledger); - indexer.lookupAssetByID(assetId).do().then((res: any) => { - cache.assets[ledger][assetId] = res.asset.params; - extensionStorage.setStorage('cache', cache, null); - }).finally(() => { - this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); - }); + const indexer = InternalMethods.getIndexer(ledger); + indexer + .lookupAssetByID(assetId) + .do() + .then((res: any) => { + cache.assets[ledger][assetId] = res.asset.params; + extensionStorage.setStorage('cache', cache, null); + }) + .finally(() => { + this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); }); - } -} \ No newline at end of file + }); + } +} diff --git a/packages/extension/src/background/utils/helper.ts b/packages/extension/src/background/utils/helper.ts index a88204b1..56fd181a 100644 --- a/packages/extension/src/background/utils/helper.ts +++ b/packages/extension/src/background/utils/helper.ts @@ -1,35 +1,67 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const algosdk = require('algosdk'); +import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; +import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; import { Settings } from '../config'; import { API, Cache, Ledger } from '../messaging/types'; -const algosdk = require("algosdk"); -export function getAlgod(ledger: Ledger) { - const params = Settings.getBackendParams(ledger, API.Algod); - return new algosdk.Algodv2(params.apiKey, params.url, params.port); +export function getAlgod(ledger: string) { + const params = Settings.getBackendParams(ledger, API.Algod); + return new algosdk.Algodv2(params.apiKey, params.url, params.port, params.headers); } -export function getIndexer(ledger: Ledger) { - const params = Settings.getBackendParams(ledger, API.Indexer); - return new algosdk.Indexer(params.apiKey, params.url, params.port); +export function getIndexer(ledger: string) { + const params = Settings.getBackendParams(ledger, API.Indexer); + return new algosdk.Indexer(params.apiKey, params.url, params.port, params.headers); } // Helper function to initialize Cache -export function initializeCache(c: Cache | undefined, ledger: Ledger | undefined=undefined) : Cache { - let cache : Cache; - if (c === undefined) { - cache = { - assets: {}, - accounts: {} - } - } else { - cache = c; - } +export function initializeCache( + c: Cache | undefined, + ledger: Ledger | undefined = undefined +): Cache { + let cache: Cache; + if (c === undefined) { + cache = { + assets: {}, + accounts: {}, + availableLedgers: [], + }; + } else { + cache = c; + } - if (ledger !== undefined) { - if (!(ledger in cache.assets)) - cache.assets[ledger] = {}; - if (!(ledger in cache.accounts)) - cache.accounts[ledger] = {}; - } + if (ledger !== undefined) { + if (!(ledger in cache.assets)) cache.assets[ledger] = {}; + if (!(ledger in cache.accounts)) cache.accounts[ledger] = {}; + } + + return cache; +} - return cache; -} \ No newline at end of file +export function getAvailableLedgersExt(callback) { + // Load Accounts details from Cache + const availableLedgers = getBaseSupportedLedgers(); + const extensionStorage = new ExtensionStorage(); + extensionStorage.getStorage('cache', (storedCache: any) => { + const cache: Cache = initializeCache(storedCache); + // Add ledgers from cache to the base ledgers + if (cache.availableLedgers && cache.availableLedgers.length > 0) { + // We should reset and update the injected networks to ensure they will be available for use + Settings.backend_settings.InjectedNetworks = {}; + + for (var i = 0; i < cache.availableLedgers.length; i++) { + if ( + !availableLedgers.some( + (e) => e.name.toLowerCase() === cache.availableLedgers[i].name.toLowerCase() + ) + ) { + const ledgerFromCache = new LedgerTemplate(cache.availableLedgers[i]); + Settings.addInjectedNetwork(ledgerFromCache); + availableLedgers.push(ledgerFromCache); + } + } + } + callback(availableLedgers); + }); +} diff --git a/packages/extension/src/background/utils/multisig.ts b/packages/extension/src/background/utils/multisig.ts index b91f2caa..6d910694 100644 --- a/packages/extension/src/background/utils/multisig.ts +++ b/packages/extension/src/background/utils/multisig.ts @@ -1,67 +1,76 @@ -const algosdk = require("algosdk"); -import logging from "@algosigner/common/logging"; -import { getSupportedLedgers } from "@algosigner/common/types/ledgers"; -import { MultisigNoMatch, MultisigAlreadySigned, MultisigInvalidMsig } from "../../errors/transactionSign"; - +/* eslint-disable @typescript-eslint/ban-types */ +import logging from '@algosigner/common/logging'; +import { + MultisigNoMatch, + MultisigAlreadySigned, + MultisigInvalidMsig, +} from '../../errors/transactionSign'; /// // Checks if the provided transaction is a multisig transaction /// -export function isMultisig(transaction):boolean { - if(transaction.msig && ((transaction.msig.subsig && transaction.msig.v && transaction.msig.thr) - || (transaction.msig.addrs && transaction.msig.version && transaction.msig.threshold))) { - // TODO: Need to have a check here for various multisig validity concerns including bad from hash - return true; - } - return false; +export function isMultisig(transaction): boolean { + if ( + transaction.msig && + ((transaction.msig.subsig && transaction.msig.v && transaction.msig.thr) || + (transaction.msig.addrs && transaction.msig.version && transaction.msig.threshold)) + ) { + // TODO: Need to have a check here for various multisig validity concerns including bad from hash + return true; + } + return false; } /// // Attempt to find an AlgoSign address to sign with in the transaction msig component. /// -export function getSigningAccounts(ledgerAddresses:any, transaction:any):{accounts?: Array, error?:MultisigInvalidMsig|MultisigAlreadySigned|MultisigNoMatch}{ - var foundAccount = false; - let multisigAccounts = {accounts: [], error: undefined}; +export function getSigningAccounts( + ledgerAddresses: any, + transaction: any +): { + accounts?: Array; + error?: MultisigInvalidMsig | MultisigAlreadySigned | MultisigNoMatch; +} { + var foundAccount = false; + const multisigAccounts = { accounts: [], error: undefined }; - // Verify that this transaction includes multisig components - if(!isMultisig(transaction)) { - multisigAccounts.error = new MultisigInvalidMsig('Multisig parameters are invalid.'); - // Return immediately - No need to continue if this is the case as the msig component is required - return multisigAccounts; - } + // Verify that this transaction includes multisig components + if (!isMultisig(transaction)) { + multisigAccounts.error = new MultisigInvalidMsig('Multisig parameters are invalid.'); + // Return immediately - No need to continue if this is the case as the msig component is required + return multisigAccounts; + } - // Cycle the msig accounts to match addresses to those in AlgoSigner - let subsig_addrs = transaction.msig.subsig || transaction.msig.addrs; - try { - if(subsig_addrs && subsig_addrs.length > 0) { - subsig_addrs.forEach((account) => { - for (var i = ledgerAddresses.length - 1; i >= 0; i--) { - if (ledgerAddresses[i].address === account || ledgerAddresses[i].address === account.pk) { - // We found an account so indicate the value as true, used in the case that the only found accounts are already signed - foundAccount = true; - if(!(account.s)) { - // We found an unsigned account that we own, add it to the list of accounts we can now sign - multisigAccounts.accounts.push(ledgerAddresses[i]); - } - } - } - }); + // Cycle the msig accounts to match addresses to those in AlgoSigner + const subsig_addrs = transaction.msig.subsig || transaction.msig.addrs; + try { + if (subsig_addrs && subsig_addrs.length > 0) { + subsig_addrs.forEach((account) => { + for (var i = ledgerAddresses.length - 1; i >= 0; i--) { + if (ledgerAddresses[i].address === account || ledgerAddresses[i].address === account.pk) { + // We found an account so indicate the value as true, used in the case that the only found accounts are already signed + foundAccount = true; + if (!account.s) { + // We found an unsigned account that we own, add it to the list of accounts we can now sign + multisigAccounts.accounts.push(ledgerAddresses[i]); + } + } } + }); } - catch(e) { - logging.log(e); - } + } catch (e) { + logging.log(e); + } - // If we didn't find an account to return then return an error. - // This error depends on if we found the address at all or not. - if(multisigAccounts.accounts.length === 0) { - if(foundAccount) { - multisigAccounts.error = new MultisigAlreadySigned('Matching addresses have already signed.'); - } - else { - multisigAccounts.error = new MultisigNoMatch('No address match found.'); - } + // If we didn't find an account to return then return an error. + // This error depends on if we found the address at all or not. + if (multisigAccounts.accounts.length === 0) { + if (foundAccount) { + multisigAccounts.error = new MultisigAlreadySigned('Matching addresses have already signed.'); + } else { + multisigAccounts.error = new MultisigNoMatch('No address match found.'); } + } - return multisigAccounts; -} \ No newline at end of file + return multisigAccounts; +} diff --git a/packages/extension/src/background/utils/session.ts b/packages/extension/src/background/utils/session.ts index 36696f6f..995dfe11 100644 --- a/packages/extension/src/background/utils/session.ts +++ b/packages/extension/src/background/utils/session.ts @@ -1,34 +1,47 @@ -import {Ledger} from '../messaging/types'; - export default class Session { - private _wallet: any; - private _ledger: any; + private _wallet: any; + private _ledger: any; + private _availableLedgers: any; - public set wallet(v : any) { - this._wallet = v; - } + public set wallet(v: any) { + this._wallet = v; + } - public get wallet() : any { - return this._wallet; - } + public get wallet(): any { + return this._wallet; + } - public set ledger(v : any) { - this._ledger = v; - } + public set ledger(v: any) { + this._ledger = v; + } - public get ledger() : any { - return this._ledger; - } + public get ledger(): any { + return this._ledger; + } - public get session() : any { - return { - wallet: this._wallet, - ledger: this._ledger, - } - } + public set availableLedgers(v: any) { + this._availableLedgers = v; + } - public clearSession() { - this._wallet = undefined; - this._ledger = undefined; + public get availableLedgers(): any { + if (this._availableLedgers) { + return this._availableLedgers; + } else { + return []; } + } + + public get session(): any { + return { + wallet: this._wallet, + ledger: this._ledger, + availableLedgers: this._availableLedgers || [], + }; + } + + public clearSession(): void { + this._wallet = undefined; + this._ledger = undefined; + this._availableLedgers = undefined; + } } diff --git a/packages/extension/src/background/utils/validator.ts b/packages/extension/src/background/utils/validator.ts index 5d928fee..efe4a849 100644 --- a/packages/extension/src/background/utils/validator.ts +++ b/packages/extension/src/background/utils/validator.ts @@ -1,139 +1,170 @@ -const algosdk = require("algosdk"); -import { getSupportedLedgers } from "@algosigner/common/types/ledgers"; +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-unused-vars */ +const algosdk = require('algosdk'); +import { getBaseSupportedLedgers } from '@algosigner/common/types/ledgers'; +import { Settings } from '../config'; /// -// Validation Status +// Validation Status /// export enum ValidationStatus { - Valid = 0, // Field is valid or not one of the validated fields - Invalid = 1, // Field value is invalid and should not be used - Warning = 2, // Field is out of normal parameters and should be inspected closely - Dangerous = 3 // Field has risky or costly fields with values and should be inspected very closely + Valid = 0, // Field is valid or not one of the validated fields + Invalid = 1, // Field value is invalid and should not be used + Warning = 2, // Field is out of normal parameters and should be inspected closely + Dangerous = 3, // Field has risky or costly fields with values and should be inspected very closely } /// // Helper to convert a validation status into a classname for display purposes /// -function _convertFieldResponseToClassname(validationStatus: ValidationStatus): string { - switch(validationStatus) { - case ValidationStatus.Dangerous: - return 'tx-field-danger'; - case ValidationStatus.Warning: - return 'tx-field-warning'; - default: - return ''; - } +function _convertFieldResponseToClassname(validationStatus: ValidationStatus): string { + switch (validationStatus) { + case ValidationStatus.Dangerous: + return 'tx-field-danger'; + case ValidationStatus.Warning: + return 'tx-field-warning'; + default: + return ''; + } } /// // Validation responses. /// export class ValidationResponse { - status: ValidationStatus; - info: string; - className: string; - constructor(props) { - this.status = props.status; - this.info = props.info; - this.className = _convertFieldResponseToClassname(this.status); - } + status: ValidationStatus; + info: string; + className: string; + constructor(props) { + this.status = props.status; + this.info = props.info; + this.className = _convertFieldResponseToClassname(this.status); + } } /// // Return field if valid based on type. /// export function Validate(field: any, value: any): ValidationResponse { - switch(field) { - // Validate the addresses are accurate - case "to": - if(!algosdk.isValidAddress(value)) { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Address does not adhear to a valid structure.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } - case "from": - if(!algosdk.isValidAddress(value)) { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Address does not adhear to a valid structure.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } - case "amount": - case "assetIndex": - case "firstRound": - case "lastRound": - case "voteFirst": - case "voteLast": - case "voteKeyDilution": - if (value && (!Number.isSafeInteger(value) || parseInt(value) < 0)){ - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } + switch (field) { + // Validate the addresses are accurate + case 'to': + if (!algosdk.isValidAddress(value)) { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Address does not adhear to a valid structure.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } + case 'from': + if (!algosdk.isValidAddress(value)) { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Address does not adhear to a valid structure.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } + case 'amount': + case 'assetIndex': + case 'firstRound': + case 'lastRound': + case 'voteFirst': + case 'voteLast': + case 'voteKeyDilution': + if (value && (!Number.isSafeInteger(value) || parseInt(value) < 0)) { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Value unable to be cast correctly to a numeric value.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } - // Warn on fee amounts above minimum, send dangerous response on those above 1 Algo. - case "fee": - try { - console.log(`Getting ready to test fee: ${value}, type:${typeof value}`); - if(!Number.isSafeInteger(value) || parseInt(value) < 0) { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); - } - else if(parseInt(value) > 1000000) { - return new ValidationResponse({status:ValidationStatus.Dangerous, info:'The associated fee is very high compared to the minimum value.'}); - } - else if(parseInt(value) > 1000) { - return new ValidationResponse({status:ValidationStatus.Warning, info:'The fee is higher than the minimum value.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } - } - catch { - // For any case where the parse int may fail. - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); - } + // Warn on fee amounts above minimum, send dangerous response on those above 1 Algo. + case 'fee': + try { + console.log(`Getting ready to test fee: ${value}, type:${typeof value}`); + if (!Number.isSafeInteger(value) || parseInt(value) < 0) { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Value unable to be cast correctly to a numeric value.', + }); + } else if (parseInt(value) > 1000000) { + return new ValidationResponse({ + status: ValidationStatus.Dangerous, + info: 'The associated fee is very high compared to the minimum value.', + }); + } else if (parseInt(value) > 1000) { + return new ValidationResponse({ + status: ValidationStatus.Warning, + info: 'The fee is higher than the minimum value.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } + } catch { + // For any case where the parse int may fail. + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Value unable to be cast correctly to a numeric value.', + }); + } - // Close to types should issue a Dangerous validation warning if they contain values. - case "closeRemainderTo": - if(value) { - return new ValidationResponse({status:ValidationStatus.Dangerous, info:'A close to address is associated to this transaction.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } + // Close to types should issue a Dangerous validation warning if they contain values. + case 'closeRemainderTo': + if (value) { + return new ValidationResponse({ + status: ValidationStatus.Dangerous, + info: 'A close to address is associated to this transaction.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } - case "reKeyTo": - if(value) { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'Rekey transactions are not currently accepted in AlgoSigner.'}); - } - else { - return new ValidationResponse({status:ValidationStatus.Valid}); - } + case 'reKeyTo': + if (value) { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'Rekey transactions are not currently accepted in AlgoSigner.', + }); + } else { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } - // Genesis ID must be present and one of the approved values - case "genesisID": - if(getSupportedLedgers().some(l => value === l["genesisId"])){ - return new ValidationResponse({status:ValidationStatus.Valid}); - } - else { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'The associated genesis id does not match a supported ledger.'}); - } - - // Genesis hash must be present and one of the approved values - case "genesisHash": - if(value) { - return new ValidationResponse({status:ValidationStatus.Valid}); - } - else { - return new ValidationResponse({status:ValidationStatus.Invalid, info:'The genesis hash value is invalid or does not exist.'}); - } - - default: - // Our field isn't one of the listed ones, so we can mark it as valid - return new ValidationResponse({status:ValidationStatus.Valid}); - } + // Genesis ID must be present and one of the approved values + case 'genesisID': + if ( + getBaseSupportedLedgers().some((l) => value === l['genesisId']) || + Settings.getCleansedInjectedNetworks().find((l) => value === l['genesisId']) + ) { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } else { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'The associated genesis id does not match a supported ledger.', + }); + } - // If for some reason the case falls through mark the field invalid. - return new ValidationResponse({status:ValidationStatus.Invalid, info:'An unknown error has occurred during validation.'}); -} \ No newline at end of file + // Genesis hash must be present and one of the approved values + case 'genesisHash': + if (value) { + return new ValidationResponse({ status: ValidationStatus.Valid }); + } else { + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'The genesis hash value is invalid or does not exist.', + }); + } + + default: + // Our field isn't one of the listed ones, so we can mark it as valid + return new ValidationResponse({ status: ValidationStatus.Valid }); + } + + // If for some reason the case falls through mark the field invalid. + return new ValidationResponse({ + status: ValidationStatus.Invalid, + info: 'An unknown error has occurred during validation.', + }); +} diff --git a/packages/storage/package.json b/packages/storage/package.json index 33c8206e..d0753ca7 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-storage", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "Storage wrapper for saving and retrieving extention information in Algosigner.", "devDependencies": { diff --git a/packages/storage/src/extensionStorage.ts b/packages/storage/src/extensionStorage.ts index 17dc8c14..87a690a2 100644 --- a/packages/storage/src/extensionStorage.ts +++ b/packages/storage/src/extensionStorage.ts @@ -1,86 +1,88 @@ /** * @license - * Copyright 2020 + * Copyright 2020 * ========================================= -*/ + */ import { extensionBrowser } from '@algosigner/common/chrome'; import { logging } from '@algosigner/common/logging'; /// -// Handles the setting and retrieval of data into the browser storage.local location. +// Handles the setting and retrieval of data into the browser storage.local location. /// export class ExtensionStorage { - constructor(){} - - /// - // Takes an objectName and saveObject and sets or overrides a storage.local instance of this combo. - // Callback: Callback will return a boolean of true if storage sets without error or false otherwise. - /// - public setStorage(objectName: string, saveObject: object, callback: Function){ - extensionBrowser.storage.local.set({ [objectName]: saveObject }, () => { - let isSuccessful = !extensionBrowser.runtime.lastError; - if(!isSuccessful) { - logging.log(extensionBrowser.runtime.lastError && `Chrome error: ${extensionBrowser.runtime.lastError.message}`); - } - - callback && callback(isSuccessful); - }); - } + /// + // Takes an objectName and saveObject and sets or overrides a + // storage.local instance of this combo. + // Callback: Callback will return a boolean of true if storage sets without error + // or false otherwise. + /// + public setStorage(objectName: string, saveObject: Object, callback: Function) { + extensionBrowser.storage.local.set({ [objectName]: saveObject }, () => { + const isSuccessful = !extensionBrowser.runtime.lastError; + if (!isSuccessful) { + logging.log( + extensionBrowser.runtime.lastError && + `Chrome error: ${extensionBrowser.runtime.lastError.message}` + ); + } - /// - // Uses the provided objectName and returns any associated storage.local item. - // Callback: Callback will return a boolean of true if an account exists or false if no account is present. - /// - public getStorage(objectName: string, callback: Function) { - extensionBrowser.storage.local.get([objectName], (result: any) => { - callback && callback(result[objectName]); - }); - } + callback && callback(isSuccessful); + }); + } - /// - // Check for the existance of a wallet account. - // Callback: Callback will return a boolean of true if an account exists or false if no account is present. - /// - public noAccountExistsCheck(objectName: string, callback: Function) { - extensionBrowser.storage.local.get([objectName], function(result: any) { - if(result[objectName]) { - callback && callback(true); - } - else { - callback && callback(false) - } - }); - } + /// + // Uses the provided objectName and returns any associated storage.local item. + // Callback: Callback will return a boolean of true if an account exists + // or false if no account is present. + /// + public getStorage(objectName: string, callback: Function) { + extensionBrowser.storage.local.get([objectName], (result: any) => { + callback && callback(result[objectName]); + }); + } + /// + // Check for the existance of a wallet account. + // Callback: Callback will return a boolean of true if an account exists + // or false if no account is present. + /// + public noAccountExistsCheck(objectName: string, callback: Function) { + extensionBrowser.storage.local.get([objectName], function (result: any) { + if (result[objectName]) { + callback && callback(true); + } else { + callback && callback(false); + } + }); + } - /// - // Clear storage.local extension data. - // Callback: Callback will return true if successful, false if there is an error. - /// - public clearStorageLocal(callback: Function) { - extensionBrowser.storage.local.clear(() => { - if(!extensionBrowser.runtime.lastError) { - callback && callback(true); - } - else { - callback && callback(false); - } - }); - } + /// + // Clear storage.local extension data. + // Callback: Callback will return true if successful, false if there is an error. + /// + public clearStorageLocal(callback: Function) { + extensionBrowser.storage.local.clear(() => { + if (!extensionBrowser.runtime.lastError) { + callback && callback(true); + } else { + callback && callback(false); + } + }); + } - /// - // **Testing Method** - // View raw storage.local extension data. - // Callback: Callback will return all data stored for the extension. - /// - protected getStorageLocal(callback: Function) { - extensionBrowser.storage.local.get(null, (result: any)=> { - if(!extensionBrowser.runtime.lastError){ - callback(JSON.stringify(result)); - } - }); - } + /// + // **Testing Method** + // View raw storage.local extension data. + // Callback: Callback will return all data stored for the extension. + /// + protected getStorageLocal(callback: Function) { + extensionBrowser.storage.local.get(null, (result: any) => { + if (!extensionBrowser.runtime.lastError) { + callback(JSON.stringify(result)); + } + }); + } } const extensionStorage = new ExtensionStorage(); -export default extensionStorage; \ No newline at end of file +export default extensionStorage; diff --git a/packages/storage/webpack.config.js b/packages/storage/webpack.config.js index 1e067a31..cf43272c 100644 --- a/packages/storage/webpack.config.js +++ b/packages/storage/webpack.config.js @@ -1,43 +1,43 @@ var path = require('path'); function srcPath(subdir) { - return path.join(__dirname, "./", subdir); + return path.join(__dirname, './', subdir); } module.exports = { - mode: 'production', - entry: { - extensionStorage: './src/extensionStorage.ts' + mode: 'production', + entry: { + extensionStorage: './src/extensionStorage.ts', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + globalObject: 'this', + }, + resolve: { + alias: { + '@storage': srcPath('~/'), + '@algosigner/common': srcPath('../common/src'), }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: '[name].js', - globalObject: 'this' - }, - resolve: { - alias: { - '@storage': srcPath('~/'), - '@algosigner/common': srcPath('../common/src') - }, - extensions: ['.ts', '.tsx', '.js'] - }, - //devtool: 'source-map', - optimization: { - minimize: false, - namedModules: true - }, - module: { - rules: [ - { - test: /\.(ts|js)x?$/, - exclude: /node_modules/, - use: [ - { - loader: "ts-loader", - options: {} - } - ] - } - ] - } + extensions: ['.ts', '.tsx', '.js'], + }, + //devtool: 'source-map', + optimization: { + minimize: false, + namedModules: true, + }, + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader', + options: {}, + }, + ], + }, + ], + }, }; diff --git a/packages/test-project/package.json b/packages/test-project/package.json index bafc0e93..116b4b73 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -1,8 +1,9 @@ { "name": "algorand-test-project", - "version": "1.3.0", + "version": "1.4.0", "description": "Repository for tests", "devDependencies": { + "algosdk": "1.8.1", "jest": "^26.6.3", "jest-runner-groups": "^2.0.1", "puppeteer": "^5.5.0", @@ -13,6 +14,8 @@ "basic-dapp": "jest --group=basic-dapp", "basic-ui": "jest --group=basic-ui", "app-dapp": "jest --group=app-dapp", + "dapp/multisig": "jest --group=dapp/multisig", + "dapp": "jest --group=dapp", "coveragetest": "jest --coverage=true --coverageDirectory ../test-project/coverage --projects ../crypto ../extension ../storage ../common ../dapp --runInBand && echo \"Test info at: ./test-project/coverage/locv-report/index.html\"", "test": "jest -i --group=-github --group=-dapp-storage" } diff --git a/packages/test-project/puppeteer.environment.js b/packages/test-project/puppeteer.environment.js index 69a8b554..464e6270 100644 --- a/packages/test-project/puppeteer.environment.js +++ b/packages/test-project/puppeteer.environment.js @@ -6,6 +6,7 @@ const NodeEnvironment = require('jest-environment-node'); function srcPath(subpath) { return path.resolve(__dirname, '../../' + subpath); } +const SAMPLE_PAGE = 'https://google.com/'; class PuppeteerEnvironment extends NodeEnvironment { constructor(config) { @@ -28,7 +29,10 @@ class PuppeteerEnvironment extends NodeEnvironment { ], }); const pages = await this.global.browser.pages(); - this.global.page = pages[0]; + this.global.dappPage = pages[0]; + // We use a sample page because algosigner.min.js doesn't load on empty pages + this.global.dappPage.goto(SAMPLE_PAGE); + this.global.extensionPage = await this.global.browser.newPage(); } async teardown() { diff --git a/packages/test-project/tests/common/constants.js b/packages/test-project/tests/common/constants.js new file mode 100644 index 00000000..949f2dc7 --- /dev/null +++ b/packages/test-project/tests/common/constants.js @@ -0,0 +1,40 @@ +module.exports = { + wallet: { + password: 'c5brJp5f', + }, + extension: { + name: 'AlgoSigner', + html: 'index.html', + }, + accounts: { + ui: { + name: 'E2E-Tests', + mnemonic: + 'grape topple reform pistol excite salute loud spike during draw drink planet naive high treat captain dutch cloth more bachelor attend attract magnet ability heavy', + address: 'MTHFSNXBMBD4U46Z2HAYAOLGD2EV6GQBPXVTL727RR3G44AJ3WVFMZGSBE', + }, + multisig: { + address: 'DZ7POUYOOYW4PEKD3LZE7ZZTBT5JGIYZ3M7VECEPZ2HLHE7RGTGJIORBCI', + subaccounts: [ + { + name: 'Multisig 1', + mnemonic: + 'response faculty obtain crowd dismiss cool clean breeze clinic pulp flash faculty worth mention layer rare reduce hand width crowd near hawk goddess about sail', + address: 'LKBQQZQ7LQFNO5477GRPMY6UOGVJJOIN7WSIPY7YQIRAHKXVYQVT6EXOGY', + }, + { + name: 'Multisig 2', + mnemonic: + 'obscure obscure allow drink write country merry ahead ordinary gallery reunion start roof antique orchard chicken shy write rebuild infant bone segment material above treat', + address: '2SLXGKWLIGSDDLC7RZY7DMGCXOAWMT6GAGO3AJM22T6Q4ZGYTNQHSOLSWA', + }, + { + name: 'Multisig 3', + mnemonic: + 'silent cram muffin differ poet spoon two bench tray inmate ribbon slogan vacuum area amateur thought obvious arena kiwi turkey seminar flush consider abstract monster', + address: 'KQVFM6F6ZNPO76XGPNG7QT5E5UJK62ZFICFMMH3HI4GNWYZD5RFHGAJSPQ', + }, + ], + }, + }, +}; diff --git a/packages/test-project/tests/common/helpers.js b/packages/test-project/tests/common/helpers.js new file mode 100644 index 00000000..b218e477 --- /dev/null +++ b/packages/test-project/tests/common/helpers.js @@ -0,0 +1,172 @@ +const algosdk = require('algosdk'); +const { extension } = require('./constants'); + +// UI Helpers +async function openExtension() { + const targets = await browser.targets(); + + const extensionTarget = targets.find(({ _targetInfo }) => { + return _targetInfo.title === extension.name && _targetInfo.type === 'background_page'; + }); + + const extensionUrl = extensionTarget._targetInfo.url || ''; + const [, , extensionID] = extensionUrl.split('/'); + + const baseUrl = `chrome-extension://${extensionID}/${extension.html}`; + + extensionPage.on('console', (msg) => console.log('EXTENSION PAGE LOG:', msg.text())); + dappPage.on('console', (msg) => console.log('DAPP PAGE LOG:', msg.text())); + await extensionPage.goto(baseUrl); +} + +async function selectAccount(account) { + const accountSelector = '#account_' + account.name.replace(/\s/g, ''); + await extensionPage.waitForSelector(accountSelector); + await extensionPage.click(accountSelector); + await extensionPage.waitForTimeout(500); +} + +async function goBack() { + await extensionPage.click('#goBack'); + await extensionPage.waitForTimeout(500); +} + +async function closeModal() { + const modalSelector = '.modal.is-active button.modal-close'; + await extensionPage.waitForSelector(modalSelector); + await extensionPage.click(modalSelector); +} + +// Dapp Helpers +async function getPopup() { + await dappPage.waitForTimeout(1500); + const pages = await browser.pages(); + return pages[pages.length - 1]; +} + +async function getLedgerParams() { + const params = await dappPage.evaluate(() => { + return Promise.resolve( + AlgoSigner.algod({ + ledger: 'TestNet', + path: '/v2/transactions/params', + }) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }) + ); + }); + + expect(params).toHaveProperty('consensus-version'); + expect(params).toHaveProperty('fee'); + expect(params.fee).toEqual(0); + expect(params).toHaveProperty('min-fee'); + expect(params).toHaveProperty('genesis-hash'); + expect(params).toHaveProperty('genesis-id'); + expect(params).toHaveProperty('last-round'); + return params; +} + +async function signTransaction(transaction) { + const signedTransaction = await dappPage.evaluate(async (transaction) => { + const signPromise = AlgoSigner.signMultisig(transaction) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + await window.authorizeSign(); + return await Promise.resolve(signPromise); + }, transaction); + await expect(signedTransaction).toHaveProperty('txID'); + await expect(signedTransaction).toHaveProperty('blob'); + return signedTransaction; +} + +async function sendTransaction(blob) { + const sendBody = { + ledger: 'TestNet', + tx: blob, + }; + const result = await dappPage.evaluate(async (sendBody) => { + return Promise.resolve( + AlgoSigner.send(sendBody) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }) + ); + }, sendBody); + return result; +} + +function base64ToByteArray(blob) { + return new Uint8Array( + Buffer.from(blob, 'base64') + .toString('binary') + .split('') + .map((x) => x.charCodeAt(0)) + ); +} + +function byteArrayToBase64(array) { + return Buffer.from(String.fromCharCode.apply(null, array), 'binary').toString('base64'); +} + +function decodeObject(obj) { + return algosdk.decodeObj(obj); +} + +function decodeBase64Blob(blob) { + return decodeObject(base64ToByteArray(blob)); +} + +function encodeAddress(address) { + return algosdk.encodeAddress(address); +} + +function decodeAddress(address) { + return algosdk.decodeAddress(address); +} + +function mergeMultisigTransactions(signedTransactionsArray) { + const convertedArray = signedTransactionsArray.map((s) => base64ToByteArray(s.blob)); + const mergedTx = algosdk.mergeMultisigTransactions(convertedArray); + return byteArrayToBase64(mergedTx); +} + +function appendSignToMultisigTransaction(partialTransaction, msigParams, mnemonic) { + const byteArrayTransaction = base64ToByteArray(partialTransaction.blob); + const params = { + version: msigParams.v, + threshold: msigParams.thr, + addrs: msigParams.subsig.map((acc) => acc.pk), + }; + const secretKey = algosdk.mnemonicToSecretKey(mnemonic).sk; + return algosdk.appendSignMultisigTransaction(byteArrayTransaction, params, secretKey); +} + +module.exports = { + openExtension, + selectAccount, + goBack, + closeModal, + getPopup, + getLedgerParams, + signTransaction, + sendTransaction, + base64ToByteArray, + byteArrayToBase64, + decodeObject, + decodeBase64Blob, + encodeAddress, + decodeAddress, + mergeMultisigTransactions, + appendSignToMultisigTransaction, +}; diff --git a/packages/test-project/tests/common/tests.js b/packages/test-project/tests/common/tests.js new file mode 100644 index 00000000..cc2db8b7 --- /dev/null +++ b/packages/test-project/tests/common/tests.js @@ -0,0 +1,112 @@ +const { wallet, extension } = require('./constants'); +const { selectAccount, goBack, closeModal, getPopup } = require('./helpers'); + +// Common Tests +async function WelcomePage() { + test('Welcome Page Title', async () => { + await expect(extensionPage.title()).resolves.toMatch(extension.name); + }); + + test('Create New Wallet', async () => { + await extensionPage.waitForSelector('#setPassword'); + await extensionPage.click('#setPassword'); + await extensionPage.waitForSelector('#createWallet'); + await expect(extensionPage.$eval('.mt-2', (e) => e.innerText)).resolves.toMatch( + 'my_1st_game_was_GALAGA!' + ); + }); +} + +async function SetPassword() { + test('Set new Wallet Password', async () => { + await extensionPage.type('#setPassword', wallet.password); + await extensionPage.type('#confirmPassword', wallet.password); + await extensionPage.click('#createWallet'); + }); +} + +async function SelectTestNetLedger() { + test('Switch Ledger', async () => { + await extensionPage.waitForSelector('#selectLedger'); + await extensionPage.click('#selectLedger'); + await extensionPage.waitForSelector('#selectTestNet'); + await extensionPage.click('#selectTestNet'); + }); +} + +async function CreateWallet() { + WelcomePage(); + SetPassword(); + SelectTestNetLedger(); +} + +async function ImportAccount(account) { + test(`Import Account ${account.name}`, async () => { + await extensionPage.waitForSelector('#addAccount'); + await extensionPage.click('#addAccount'); + await extensionPage.waitForSelector('#importAccount'); + await extensionPage.click('#importAccount'); + await extensionPage.waitForSelector('#accountName'); + await extensionPage.type('#accountName', account.name); + await extensionPage.type('#enterMnemonic', account.mnemonic); + await extensionPage.click('#nextStep'); + await extensionPage.waitForSelector('#enterPassword'); + await extensionPage.type('#enterPassword', wallet.password); + await extensionPage.click('#authButton'); + }); + + test(`Verify Account Info (${account.name})`, async () => { + await selectAccount(account); + await extensionPage.waitForSelector('#accountName'); + await expect(extensionPage.$eval('#accountName', (e) => e.innerText)).resolves.toBe( + account.name + ); + await extensionPage.click('#showDetails'); + await expect(extensionPage.$eval('#accountAddress', (e) => e.innerText)).resolves.toBe( + account.address + ); + await closeModal(); + await goBack(); + }); +} + +// Dapp Tests +async function ConnectAlgoSigner() { + test('Expose Authorize Functions', async () => { + async function authorizeDapp() { + const popup = await getPopup(); + await popup.waitForSelector('#grantAccess'); + await popup.click('#grantAccess'); + } + await dappPage.exposeFunction('authorizeDapp', authorizeDapp); + + async function authorizeSign() { + const popup = await getPopup(); + await popup.waitForSelector('#approveTx'); + await popup.click('#approveTx'); + await popup.waitForSelector('#enterPassword'); + await popup.type('#enterPassword', wallet.password); + await popup.waitForSelector('#authButton'); + await popup.click('#authButton'); + } + await dappPage.exposeFunction('authorizeSign', authorizeSign); + }); + + test('Connect Dapp through content.js', async () => { + const connected = await dappPage.evaluate(async () => { + const connectPromise = AlgoSigner.connect(); + await window.authorizeDapp(); + return await connectPromise; + }); + await expect(connected).toEqual({}); + }); +} + +module.exports = { + WelcomePage, + SetPassword, + SelectTestNetLedger, + CreateWallet, + ImportAccount, + ConnectAlgoSigner, +}; diff --git a/packages/test-project/tests/dapp-multisig.test.js b/packages/test-project/tests/dapp-multisig.test.js new file mode 100644 index 00000000..e0b7d61b --- /dev/null +++ b/packages/test-project/tests/dapp-multisig.test.js @@ -0,0 +1,157 @@ +/** + * dapp e2e tests for the AlgoSigner Multisig functionality + * + * @group dapp/multisig + */ + +const { accounts } = require('./common/constants'); +const { + openExtension, + getLedgerParams, + signTransaction, + sendTransaction, + decodeBase64Blob, + byteArrayToBase64, + encodeAddress, + mergeMultisigTransactions, + appendSignToMultisigTransaction, +} = require('./common/helpers'); +const { CreateWallet, ConnectAlgoSigner, ImportAccount } = require('./common/tests'); + +const msigAccount = accounts.multisig; + +let ledgerParams; +let multisigTransaction; +let signedTransactions = []; + +jest.setTimeout(10000); + +describe('Wallet Setup', () => { + beforeAll(async () => { + await openExtension(); + }); + + CreateWallet(); +}); + +describe('dApp Setup', () => { + ConnectAlgoSigner(); + + test('Get TestNet params', async () => { + ledgerParams = await getLedgerParams(); + console.log(`TestNet transaction params: [last-round: ${ledgerParams['last-round']}, ...]`); + multisigTransaction = { + msig: { + subsig: msigAccount.subaccounts.map((acc) => { + return { pk: acc.address }; + }), + thr: 2, + v: 1, + }, + txn: { + type: 'pay', + from: msigAccount.address, + to: accounts.ui.address, + amount: Math.ceil(Math.random() * 1000), + fee: ledgerParams['fee'], + firstRound: ledgerParams['last-round'], + lastRound: ledgerParams['last-round'] + 1000, + genesisID: ledgerParams['genesis-id'], + genesisHash: ledgerParams['genesis-hash'], + }, + }; + }); +}); + +describe('MultiSig Use cases', () => { + test('Fail on Sign with no Valid Addresses', async () => { + // We try to sign with no accounts loaded + await expect( + dappPage.evaluate((transaction) => { + return Promise.resolve(AlgoSigner.signMultisig(transaction)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, multisigTransaction) + ).resolves.toBe('No address match found.'); + }); + + ImportAccount(msigAccount.subaccounts[0]); + + test('Sign MultiSig Transaction with First account', async () => { + signedTransactions.push(await signTransaction(multisigTransaction)); + + // Verify signature is added + const decodedTransaction = decodeBase64Blob(signedTransactions[0].blob); + expect(decodedTransaction).toHaveProperty('txn'); + expect(decodedTransaction).toHaveProperty('msig'); + expect(decodedTransaction.msig).toHaveProperty('subsig'); + expect(decodedTransaction.msig.subsig.length).toBe(3); + expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); + }); + + test('Should fail signature treshold validation', async () => { + const result = await sendTransaction(signedTransactions[0].blob); + expect(result).toMatchObject({ + message: expect.stringContaining('multisig validation failed'), + }); + }); + + ImportAccount(msigAccount.subaccounts[1]); + + test('Sign MultiSig Transaction with Second account', async () => { + // Merge first signature to original transaction + const decodedFirstTransaction = decodeBase64Blob(signedTransactions[0].blob); + multisigTransaction = { + ...multisigTransaction, + msig: { + ...multisigTransaction.msig, + subsig: decodedFirstTransaction.msig.subsig.map((subsig) => { + const data = subsig; + data.pk = encodeAddress(data.pk); + return data; + }), + }, + }; + signedTransactions.push(await signTransaction(multisigTransaction)); + + // Verify signature is added + const decodedTransaction = decodeBase64Blob(signedTransactions[1].blob); + expect(decodedTransaction).toHaveProperty('txn'); + expect(decodedTransaction).toHaveProperty('msig'); + expect(decodedTransaction.msig).toHaveProperty('subsig'); + expect(decodedTransaction.msig.subsig.length).toBe(3); + expect(decodedTransaction.msig.subsig[1]).toHaveProperty('s'); + }); + + test('Send SDK-merged Multisig Transaction', async () => { + const sdkMerge = mergeMultisigTransactions(signedTransactions); + const result = await sendTransaction(sdkMerge); + expect(result).not.toBeNull(); + expect(result).toHaveProperty('txId'); + expect(result).not.toHaveProperty('message'); + signedTransactions.push(sdkMerge); + }); + + test('Append Second signature with SDK', async () => { + const appendedMultisig = appendSignToMultisigTransaction( + signedTransactions[1], + multisigTransaction.msig, + msigAccount.subaccounts[0].mnemonic + ); + expect(appendedMultisig).not.toBeNull(); + expect(appendedMultisig).toHaveProperty('txID'); + expect(appendedMultisig).toHaveProperty('blob'); + // We can't send it since it shares ID with the other + signedTransactions.push(appendedMultisig); + }); + + test('Compare merge results', async () => { + const decodedFirstMerge = decodeBase64Blob(signedTransactions[2]); + const decodedSecondMerge = decodeBase64Blob(byteArrayToBase64(signedTransactions[3].blob)); + expect(decodedFirstMerge).toStrictEqual(decodedSecondMerge); + }); +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index 914888ba..a2cfe0c7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-ui", - "version": "1.3.0", + "version": "1.4.0", "author": "https://developer.purestake.io", "description": "User interface built for AlgoSigner.", "private": true, diff --git a/packages/ui/src/components/Account/AccountDetails.ts b/packages/ui/src/components/Account/AccountDetails.ts index 20abc814..ff528858 100644 --- a/packages/ui/src/components/Account/AccountDetails.ts +++ b/packages/ui/src/components/Account/AccountDetails.ts @@ -39,8 +39,9 @@ const AccountDetails: FunctionalComponent = (props: any) => { break; } } else { - store.updateWallet(response); - route('/wallet'); + store.updateWallet(response, () => { + route('/wallet'); + }); } }); }; diff --git a/packages/ui/src/components/AccountPreview.ts b/packages/ui/src/components/AccountPreview.ts index 94e581ea..5394663f 100644 --- a/packages/ui/src/components/AccountPreview.ts +++ b/packages/ui/src/components/AccountPreview.ts @@ -1,5 +1,5 @@ import { html } from 'htm/preact'; -import { FunctionalComponent } from "preact"; +import { FunctionalComponent } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import { route } from 'preact-router'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; @@ -16,36 +16,33 @@ const AccountPreview: FunctionalComponent = (props: any) => { ledger: ledger, address: account.address, }; - sendMessage(JsonRpcMethod.AccountDetails, params, function(response) { + sendMessage(JsonRpcMethod.AccountDetails, params, function (response) { setResults(response); }); - } + }; useEffect(() => { fetchApi(); }, []); return html` - - - ${showDetails && - html` - - `} ` ); }; diff --git a/packages/ui/src/components/Logo.ts b/packages/ui/src/components/Logo.ts new file mode 100644 index 00000000..a93985a2 --- /dev/null +++ b/packages/ui/src/components/Logo.ts @@ -0,0 +1,14 @@ +import { FunctionalComponent } from 'preact'; +import { html } from 'htm/preact'; + +import logotype from 'assets/logotype.png'; + +const Logo: FunctionalComponent = () => { + return html` +
+ +
+ `; +}; + +export default Logo; diff --git a/packages/ui/src/components/MainHeader.ts b/packages/ui/src/components/MainHeader.ts new file mode 100644 index 00000000..0b47163a --- /dev/null +++ b/packages/ui/src/components/MainHeader.ts @@ -0,0 +1,19 @@ +import { FunctionalComponent } from 'preact'; +import { html } from 'htm/preact'; + +import HeaderComponent from './HeaderComponent'; +import LedgerSelect from './LedgerSelect'; +import SettingsMenu from './SettingsMenu'; +import Logo from './Logo'; + +const MainHeader: FunctionalComponent = () => { + return html` + <${HeaderComponent}> + <${Logo} /> + <${LedgerSelect} /> + <${SettingsMenu} /> + + `; +}; + +export default MainHeader; diff --git a/packages/ui/src/components/SettingsMenu.ts b/packages/ui/src/components/SettingsMenu.ts new file mode 100644 index 00000000..5c14e7f6 --- /dev/null +++ b/packages/ui/src/components/SettingsMenu.ts @@ -0,0 +1,101 @@ +import { FunctionalComponent } from 'preact'; +import { html } from 'htm/preact'; +import { useState } from 'preact/hooks'; +import { route } from 'preact-router'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; + +import HeaderComponent from './HeaderComponent'; +import DeleteWallet from 'components/DeleteWallet'; +import Logo from './Logo'; +import { sendMessage } from 'services/Messaging'; +import LedgerNetworksConfiguration from './LedgerNetworksConfiguration'; + +const SettingsMenu: FunctionalComponent = () => { + const [active, setActive] = useState(false); + const [currentMenu, setCurrentMenu] = useState('settings'); + + let menuClass: string = 'menu'; + if (active) menuClass += ' is-active'; + + const flip = () => { + setActive(!active); + }; + + const logout = () => { + sendMessage(JsonRpcMethod.Logout, {}, function () { + route('/login'); + }); + }; + + const getSubmenu = () => { + switch (currentMenu) { + case 'networkConfiguration': + return html`<${LedgerNetworksConfiguration} + closeFunction=${() => { + setCurrentMenu('settings'); + flip(); + }} + />`; + case 'delete': + return html`<${DeleteWallet} />`; + default: + return ''; + } + }; + + return html` +
+ +