diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3c778438..45f9f2f2 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,5 @@ +name-template: "Patch $NEXT_PATCH_VERSION" +tag-template: "v$NEXT_PATCH_VERSION" template: | ## Updates diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 25cc8e2a..9a719ccd 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -6,13 +6,13 @@ on: types: [opened,closed,synchronize] jobs: - CLAssistant: + CLA_assistant: runs-on: ubuntu-latest steps: - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Beta Release - uses: cla-assistant/github-action@v2.1.3-beta + uses: contributor-assistant/github-action@v2.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret @@ -24,7 +24,7 @@ jobs: branch: 'main' allowlist: janmarcano,fxgamundi,purestaketdb,PureBrent,mmaurello,ekenigs - #below are the optional inputs - If the optional inputs are not given, then default values will be taken + #below are the optional inputs - If the optional inputs are not given, then default values will be taken remote-organization-name: purestake remote-repository-name: algosigner-cla #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8979a276..a0ac2b34 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 686be5b8..d6d40a8f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,10 +12,10 @@ jobs: env: CI: 'true' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2-beta + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: '12.15' + node-version: '16.15' - name: Install build dependencies run: npm run install:extension - name: Run unit tests @@ -26,10 +26,10 @@ jobs: env: CI: 'true' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2-beta + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: '12.15' + node-version: '16.15' - name: Install test dependencies env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 'true' @@ -37,12 +37,12 @@ jobs: - name: Build run: npm run build - name: Run headful puppeteer tests - uses: djp3/puppeteer-headful@master + uses: mujo-code/puppeteer-headful@16.6.0 with: args: npm run test:github - name: Upload screenshots if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: screenshot path: ./packages/test-project/screenshots/*.png @@ -53,16 +53,16 @@ jobs: env: CI: 'true' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2-beta + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: '12.15' + node-version: '16.15' - name: Install build dependencies run: npm run install:extension - name: Build run: npm run build - name: Upload build artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: build path: ./dist/* \ No newline at end of file diff --git a/.github/workflows/zip-release.yml b/.github/workflows/zip-release.yml index 88d0bcb4..a0cd776e 100644 --- a/.github/workflows/zip-release.yml +++ b/.github/workflows/zip-release.yml @@ -6,13 +6,13 @@ on: - released jobs: - test_zip_upload: + build_zip_upload: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2-beta + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: '12.15' + node-version: '16.15' - name: Delete all dist run: npm run clean - name: Install dependencies @@ -22,12 +22,9 @@ jobs: - name: Zip up release id: zip_up run: zip -r AlgoSigner.zip ./dist/* - - name: Upload - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload build as asset + uses: svenstaro/upload-release-action@v2 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./AlgoSigner.zip - asset_name: AlgoSigner.zip - asset_content_type: application/zip \ No newline at end of file + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref }} + file: AlgoSigner.zip \ No newline at end of file diff --git a/README.md b/README.md index 593d3544..497de8c6 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,28 @@ _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.10.1 Release +### Important -### Main updates -As part of the process of supporting the [Algorand Foundations ARCs](https://arc.algorand.foundation/), in 1.10.0, a number of non-breaking additions have been made to support how dApps will work with AlgoSigner. In time, the existing legacy features will be deprecated. - -- A new top level object, `window.algorand` is made available and can be accessed by the dapp to make calls to AlgoSigner. The existing `window.AlgoSigner` object remains but will be deprecated over the next year. -- An updated connection flow and address discovery process for dApps is in place, using `algorand.enable()`. **The existing connection flow persists but will be deprecated over the next 3-6 months.** -- Dapps may also now request for AlgoSigner to directly post signed transactions to the network and not return the signed blob to the dApp for handling. -- Additional documentation regarding the use of `authAddr` for signing transactions with rekeyed accounts. +As part of the process of supporting the [Algorand Foundations ARCs](https://arc.algorand.foundation/), in 1.10.0, a number of non-breaking additions have been made to support how dApps will work with AlgoSigner. In time, the existing legacy features will be deprecated. An interactive transition guide is available [here](https://purestake.github.io/algosigner-dapp-example/arcTransitionGuide.html) to aid in the migration of existing functionalities. -### Other updates (1.10.1): -- Transaction signing with Ledger devices now support signing multiple transactions in a single group at once. Also updated the Ledger libraries. -- Added support for the `boxes` field on application transactions and the `stateProofKey` & `nonParticipation` fields for key registration transactions. -- Various bugfixes: - - Fixed an issue where custom networks weren't working correctly on `algorand.enable()` calls. - - Fixed an issue AlgoSigner becoming unresponsive when trying to sign a transaction with a Reference Account with no `authAddr` provided. +## 1.11.0 Release + +### Main updates + +- Multiple improvements to Custom Networks including: + - Automatic fetching of genesis ID and hashes for the user. + - Achieved full compatibility with `enable()` calls. + - Other UI/UX changes. +- Updated a lot of error messages and data structures to better communicate the causes of errors. + +### Other updates and bugfixes: +- Connecting to a dApp using an `enable()` call while logged out no longer closes the connection window after inputting your password. +- Improved validations for `keyreg` transactions regarding nonParticipation. +- Improved validations for `acfg` transactions regarding the distinctions between 'Create', 'Config' and 'Destroy'. +- Fixed account permission bug on `enable()` calls. +- SDK support updated to v2.0.0. ## New Users diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index 48a55436..e2f7f462 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -582,9 +582,9 @@ Some of the following error codes may be returned when interacting with AlgoSign | 4000 | An unknown error occured. | N/A | | 4001 | The user rejected the signature request. | N/A | | 4100 | The requested operation and/or account has not been authorized by the user. | This is usually due to the connection between the dApp and the wallet becoming stale and the user [needing to reconnect](connection-issues.md). Otherwise, it may signal that you are trying to sign with private keys not found on AlgoSigner. | -| 4200 | The wallet does not support the requested operation. | N/A | +| 4200 | The wallet does not support the requested operation. | Users need to have imported or created an account on AlgoSigner before connecting to dApps, as well as succesfully having configured any custom networks required. | | 4201 | The wallet does not support signing that many transactions at a time. | The max number of transactions per group is 16. For Ledger devices, they currently can't sign more than one transaction at the same time. | -| 4202 | The wallet was not initialized properly beforehand. | Users need to have imported or created an account on AlgoSigner before connecting to dApps. | +| 4202 | The wallet was not initialized properly beforehand. | The extension user has not authorized requests from this website. | | 4300 | The input provided is invalid. | AlgoSigner rejected some of the transactions due to invalid fields. | | 4400 | Some transactions were not sent properly. | Some, but not all of the transactions were able to be posted to the network. The IDs of the succesfully posted transactions as well as information on the failing ones are provided on the error. diff --git a/docs/legacy-dApp-integration.md b/docs/legacy-dApp-integration.md index e9228dad..44377839 100644 --- a/docs/legacy-dApp-integration.md +++ b/docs/legacy-dApp-integration.md @@ -524,8 +524,8 @@ AlgoSigner.send({ - A non-matching ledger name will result in a error: - The provided ledger is not supported (Code: 4200). - 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":"thetanet-v1.0"}]. -- Transaction requests will require a valid matching "genesisId", even for custom networks. + - Ledger not provided. Please use a base ledger: [TestNet,MainNet] or an available custom one [{"name":"Theta","genesisID":"thetanet-v1.0"}]. +- Transaction requests will require a valid matching "genesisID", even for custom networks. ## Signature Rejection Messages @@ -536,9 +536,9 @@ AlgoSigner may return some of the following error codes when requesting signatur | 4000 | An unknown error occured. | N/A | | 4001 | The user rejected the signature request. | N/A | | 4100 | The requested operation and/or account has not been authorized by the user. | This is usually due to the connection between the dApp and the wallet becoming stale and the user [needing to reconnect](connection-issues.md). Otherwise, it may signal that you are trying to sign with private keys not found on AlgoSigner. | -| 4200 | The wallet does not support the requested operation. | N/A | +| 4200 | The wallet does not support the requested operation. | Users need to have imported or created an account on AlgoSigner before connecting to dApps, as well as succesfully having configured any custom networks required. | | 4201 | The wallet does not support signing that many transactions at a time. | The max number of transactions per group is 16. For Ledger devices, they can't sign more than one transaction at the same time. | -| 4202 | The wallet was not initialized properly beforehand. | Users need to have imported or created an account on AlgoSigner before connecting to dApps | +| 4202 | The wallet was not initialized properly beforehand. | The extension user has not authorized requests from this website. | | 4300 | The input provided is invalid. | AlgoSigner rejected some of the transactions due to invalid fields. | Additional information, if available, would be provided in the `data` field of the error object. diff --git a/package.json b/package.json index 799d7713..12086589 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosigner", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/common/package.json b/packages/common/package.json index 9111f9a4..8590e6ad 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/common", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "description": "Common library functions for AlgoSigner.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/common/src/common.test.ts b/packages/common/src/common.test.ts index 83ee3526..a869d2c9 100644 --- a/packages/common/src/common.test.ts +++ b/packages/common/src/common.test.ts @@ -44,8 +44,8 @@ test('RequestError - Structure', () => { code: 4100, }); - expect(RequestError.SigningError(4000, [testError])).toMatchObject({ - message: 'There was a problem signing the transaction(s).', + expect(RequestError.SigningValidationError(4000, [testError])).toMatchObject({ + message: 'There was a problem validating the transaction(s) to be signed. Please refer to the data property for the reasons behind each transaction.', code: 4000, data: [testError], }); diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index 5dbdcce6..4ed7d1b9 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -1,11 +1,12 @@ -import { Namespace, Ledger, Alias } from './types'; +import { Namespace, Alias } from './types'; +import { Network } from './types/network'; const MAX_ALIASES_PER_NAMESPACE = 6; // prettier-ignore interface ConfigTemplate { name: string; // Formatted name, used for titles - ledgers: any; // Object holding supported Ledgers as keys, templated API URL as value + networks: any; // Object holding supported networks as keys, templated API URL as value findAliasedAddresses: Function; // How to process the API response to get the aliased addresses array apiTimeout: number; // Amount in ms to wait for the API to respond } @@ -17,25 +18,25 @@ const noop = (): void => { export class AliasConfig { static [Namespace.AlgoSigner_Contacts]: ConfigTemplate = { name: 'AlgoSigner Contact', - ledgers: null, + networks: null, findAliasedAddresses: noop, apiTimeout: 0, }; static [Namespace.AlgoSigner_Accounts]: ConfigTemplate = { name: 'AlgoSigner Account', - ledgers: null, + networks: null, findAliasedAddresses: noop, apiTimeout: 0, }; static [Namespace.NFD]: ConfigTemplate = { name: 'NFDomains', - ledgers: { - [Ledger.TestNet]: + networks: { + [Network.TestNet]: 'https://api.testnet.nf.domains/nfd?prefix=${term}&requireAddresses=true' + `&limit=${MAX_ALIASES_PER_NAMESPACE}`, - [Ledger.MainNet]: + [Network.MainNet]: 'https://api.nf.domains/nfd?prefix=${term}&requireAddresses=true' + `&limit=${MAX_ALIASES_PER_NAMESPACE}`, }, @@ -50,11 +51,11 @@ export class AliasConfig { static [Namespace.ANS]: ConfigTemplate = { name: 'Algorand Name Service', - ledgers: { - [Ledger.TestNet]: + networks: { + [Network.TestNet]: 'https://testnet.api.algonameservice.com/names?pattern=${term}' + `&limit=${MAX_ALIASES_PER_NAMESPACE}`, - [Ledger.MainNet]: + [Network.MainNet]: 'https://api.algonameservice.com/names?pattern=${term}' + `&limit=${MAX_ALIASES_PER_NAMESPACE}`, }, @@ -69,12 +70,12 @@ export class AliasConfig { apiTimeout: 2000, }; - public static getMatchingNamespaces(ledger: string): Array { + public static getMatchingNamespaces(network: string): Array { const matchingNamespaces: Array = []; for (const n in Namespace) { if ( AliasConfig[n] && - (AliasConfig[n].ledgers === null || Object.keys(AliasConfig[n].ledgers).includes(ledger)) + (AliasConfig[n].networks === null || Object.keys(AliasConfig[n].networks).includes(network)) ) { matchingNamespaces.push(n as Namespace); } diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index 9f90ab4d..f5f5e636 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -9,19 +9,14 @@ export class RequestError { static None = new RequestError('', 0); static Undefined = new RequestError('An undefined error occurred.', 4000); + static ApplySignatureError = (data?: any): RequestError => + new RequestError('There was a problem signing the transaction(s).', 4000, data); static UserRejected = new RequestError( 'The extension user does not authorize the request.', 4001 ); - static EnableRejected = (data: object): RequestError => new RequestError( - 'The extension user does not authorize the request.', - 4001, - data - ); - static SiteNotAuthorizedByUser = new RequestError( - 'The extension user has not authorized requests from this website.', - 4100 - ); + static EnableRejected = (data: object): RequestError => + new RequestError('The extension user does not authorize the request.', 4001, data); static NoMnemonicAvailable = (address: string): RequestError => new RequestError( `The user does not possess the required private key to sign with for address: "${address}".`, @@ -32,7 +27,13 @@ export class RequestError { `No matching account found on AlgoSigner for address "${address}" on network ${ledger}.`, 4100 ); + static NoLedgerProvided = (base: string, injected: string): RequestError => + new RequestError( + `Ledger not provided. Please use a base ledger: [${base}] or an available custom one ${injected}.`, + 4200 + ); static UnsupportedLedger = new RequestError('The provided ledger is not supported.', 4200); + static UnsupportedNetwork = new RequestError('The provided network is not supported.', 4200); static PendingTransaction = new RequestError('Another query processing', 4201); static LedgerMultipleGroups = new RequestError( 'Ledger hardware device signing is only available for one transaction group at a time.', @@ -44,12 +45,16 @@ export class RequestError { ); static AlgoSignerNotInitialized = new RequestError( 'AlgoSigner was not initialized properly beforehand.', + 4200 + ); + static SiteNotAuthorizedByUser = new RequestError( + 'The extension user has not authorized requests from this website.', 4202 ); - static InvalidFields = (data?: any): RequestError => - new RequestError('Validation failed for transaction due to invalid properties.', 4300, data); - static InvalidTransactionStructure = (data?: any): RequestError => - new RequestError('Validation failed for transaction due to invalid structure.', 4300, data); + static InvalidFields = (fields?: any): RequestError => + new RequestError(`Validation failed for transaction since it has invalid properties: [${fields.join(', ')}]`, 4300); + static InvalidTransactionStructure = (reason?: any): RequestError => + new RequestError(`Validation failed for transaction since it has an invalid structure: ${reason}`, 4300); static InvalidSignTxnsFormat = new RequestError( 'Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', 4300 @@ -99,7 +104,7 @@ export class RequestError { "There are no transactions to sign as the provided ones are for reference-only ('{ signers: [] }').", 4300 ); - static InvalidStructure = new RequestError( + static InvalidWalletTxnStructure = new RequestError( "The provided transaction object doesn't adhere to the correct structure.", 4300 ); @@ -129,12 +134,18 @@ export class RequestError { 'All transactions provided in a same group need to have matching group IDs.', 4300 ); - static NoDifferentLedgers = new RequestError( - 'All transactions need to belong to the same ledger.', + static NoDifferentNetworks = new RequestError( + 'All transactions need to belong to the same network.', 4300 ); - static PartiallySuccessfulPost = (successTxnIDs: string[], data: any): PostError => - new PostError( + static FailedPost = (data: any): RequestError => + new RequestError( + "The transaction was unable to be posted. The reason returned by the network is found inside the 'data' property.", + 4400, + data + ); + static PartiallySuccessfulPost = (successTxnIDs: string[], data: any): PartialPostError => + new PartialPostError( successTxnIDs, "Some of the groups of transactions were unable to be posted. The reason for each unsuccessful group is in it's corresponding position inside the 'data' array.", 4400, @@ -144,8 +155,8 @@ export class RequestError { 'The transaction(s) were succesfully sent to the network, but there was an issue while waiting for confirmation. Please verify that they were commited to the network before trying again.', 4400 ); - static SigningError = (code: number, data?: any): RequestError => - new RequestError('There was a problem signing the transaction(s).', code, data); + static SigningValidationError = (code: number, data?: any): RequestError => + new RequestError('There was a problem validating the transaction(s) to be signed. Please refer to the data property for the reasons behind each transaction.', code, data); protected constructor(message: string, code: number, data?: any) { this.name = 'AlgoSignerRequestError'; @@ -157,7 +168,7 @@ export class RequestError { } } -class PostError extends RequestError { +class PartialPostError extends RequestError { successTxnIDs: string[]; public constructor(successTxnIDs: string[], message: string, code: number, data?: any) { diff --git a/packages/common/src/interfaces/keyreg.ts b/packages/common/src/interfaces/keyreg.ts index 2049ec57..9002208a 100644 --- a/packages/common/src/interfaces/keyreg.ts +++ b/packages/common/src/interfaces/keyreg.ts @@ -7,11 +7,11 @@ import { IBaseTx } from './baseTx'; // prettier-ignore export interface IKeyRegistrationTx extends IBaseTx { type: string, // "keyreg" - voteKey: string, // ed25519PublicKey "votekey" The root participation public key. - selectionKey: string, // VrfPubkey "selkey" The VRF public key. + voteKey?: string, // ed25519PublicKey "votekey" The root participation public key. + selectionKey?: string, // VrfPubkey "selkey" The VRF public key. stateProofKey?: string, // MerkleSignature "sprfkey" The 64 byte state proof public key commitment. - voteFirst: number, // uint64 "votefst" The first round that the participation key is valid. Not to be confused with the FirstValid round of the keyreg transaction. - voteLast: number, // uint64 "votelst" The last round that the participation key is valid. Not to be confused with the LastValid round of the keyreg transaction. + voteFirst?: number, // uint64 "votefst" The first round that the participation key is valid. Not to be confused with the FirstValid round of the keyreg transaction. + voteLast?: number, // uint64 "votelst" The last round that the participation key is valid. Not to be confused with the LastValid round of the keyreg transaction. voteKeyDilution?: number, // uint64 "votekd" This is the dilution for the 2-level participation key. nonParticipation?: boolean, // bool "nonpart" All new Algorand accounts are participating by default. } diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index 201c6cf4..248116e6 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -34,11 +34,11 @@ export enum JsonRpcMethod { AssetsAPIList = 'assets-api-list', AssetsVerifiedList = 'assets-verified-list', SignSendTransaction = 'sign-send-transaction', - ChangeLedger = 'change-ledger', + ChangeNetwork = 'change-network', SaveNetwork = 'save-network', CheckNetwork = 'check-network', DeleteNetwork = 'delete-network', - GetLedgers = 'get-ledgers', + GetNetworks = 'get-networks', GetContacts = 'get-contacts', SaveContact = 'save-contact', DeleteContact = 'delete-contact', @@ -73,6 +73,7 @@ export enum MessageSource { Router = 'router', UI = 'ui', } + export type MessageBody = { readonly source: MessageSource; readonly body: JsonRpcBody; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index b0736341..e190a6a8 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,4 +1,6 @@ /* eslint-disable no-unused-vars */ +import { Network, NetworkTemplate } from "./types/network"; + export type Field = string | number; export type TAccount = Field; @@ -12,11 +14,6 @@ export type Transaction = { readonly to: TAccount; }; -export enum Ledger { - TestNet = 'TestNet', - MainNet = 'MainNet', -} - export type WalletMultisigMetadata = { readonly version: number; readonly threshold: number; @@ -60,3 +57,24 @@ export type NamespaceConfig = { namespace: Namespace; toggle: boolean; }; + +export type SafeAccount = { + address: string; + isRef: boolean; + name: string; + details?: any; +} + +export type SensitiveAccount = SafeAccount & { + mnemonic: string; +} + +// { network: [...accounts] } +export type WalletStorage = { [key: string]: Array }; + +export type SessionObject = { + wallet: WalletStorage; + network: Network; + availableNetworks: Array; + txnRequest: any; +} \ No newline at end of file diff --git a/packages/common/src/types/ledgers.ts b/packages/common/src/types/ledgers.ts deleted file mode 100644 index b27939bb..00000000 --- a/packages/common/src/types/ledgers.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 [ - new LedgerTemplate({ - name: 'MainNet', - genesisId: 'mainnet-v1.0', - genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', - }), - new LedgerTemplate({ - name: 'TestNet', - genesisId: 'testnet-v1.0', - genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', - }), - ]; -} diff --git a/packages/common/src/types/network.ts b/packages/common/src/types/network.ts new file mode 100644 index 00000000..316b3421 --- /dev/null +++ b/packages/common/src/types/network.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-unused-vars */ +export enum Network { + TestNet = 'TestNet', + MainNet = 'MainNet', +} + +export enum NetworkSelectionType { + NoneProvided, + OnlyIDProvided, + BothProvided, +} + +export type Connection = { + headers: {}, + algod: ConnectionDetails, + indexer: ConnectionDetails, +} + +export type ConnectionDetails = { + url: string, + port: string, + apiKey: {}, + headers: {}, +} + +export class NetworkTemplate { + name: string; + readonly isEditable: boolean; + genesisID?: string; + genesisHash?: string; + symbol?: string; + algodUrl?: string; + indexerUrl?: string; + headers?: string; + + 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; + this.genesisHash = genesisHash; + this.symbol = symbol; + this.algodUrl = algodUrl; + this.indexerUrl = indexerUrl; + this.headers = headers; + // We protect the default networks from being overriden + this.isEditable = !Object.values(Network).includes(name as Network); + } +} + +export function getBaseSupportedNetworks(): Array { + // Need to add access to additional network types from import + return [ + new NetworkTemplate({ + name: Network.MainNet, + genesisID: 'mainnet-v1.0', + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + }), + new NetworkTemplate({ + name: Network.TestNet, + genesisID: 'testnet-v1.0', + genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', + }), + ]; +} diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 00090d29..5f467af3 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,5 +1,5 @@ import { extensionBrowser } from './chrome'; -import { getBaseSupportedLedgers } from './types/ledgers'; +import { getBaseSupportedNetworks } from './types/network'; export function isFromExtension(origin: string): boolean { const s = origin.split('://'); @@ -21,12 +21,12 @@ export function removeEmptyFields(obj: { [index: string]: any }): any { } /** - * Check if a ledger belongs to our base supported ledgers (e.g: links to GoalSeeker) - * @param ledger + * Check if a network belongs to our base supported network (e.g: links to GoalSeeker) + * @param network * @returns boolean */ -export function isLedgerBaseSupported(ledger: string): boolean { - return getBaseSupportedLedgers().map((l) => l.name.toLowerCase()).includes(ledger.toLowerCase()); +export function isBaseSupportedNetwork(network: string): boolean { + return getBaseSupportedNetworks().map((l) => l.name.toLowerCase()).includes(network.toLowerCase()); } /** diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 272e5c38..bec3cdad 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-crypto", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "description": "Cryptographic wrapper for saving and retrieving extention information in AlgoSigner.", "repository": { diff --git a/packages/dapp/package.json b/packages/dapp/package.json index a6e6fc07..f0a0fc56 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/dapp", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 02df31bd..29d12289 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.10.1", + "version": "1.11.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 fd1f4b4d..76aec0fe 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-extension", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", @@ -20,7 +20,7 @@ "webpack-cli": "^4.9.0" }, "dependencies": { - "algosdk": "1.24.1", + "algosdk": "2.0.0", "buffer": "^6.0.3" }, "scripts": { diff --git a/packages/extension/src/background/config.ts b/packages/extension/src/background/config.ts index d8a80659..8b19ef99 100644 --- a/packages/extension/src/background/config.ts +++ b/packages/extension/src/background/config.ts @@ -1,5 +1,5 @@ -import { LedgerTemplate } from '@algosigner/common/types/ledgers'; -import { Ledger } from '@algosigner/common/types'; +import { Connection, ConnectionDetails, NetworkTemplate } from '@algosigner/common/types/network'; +import { Network } from '@algosigner/common/types/network'; import { logging, LogLevel } from '@algosigner/common/logging'; import { Backend, API } from './messaging/types'; import { parseUrlServerAndPort } from './utils/networkUrlParser'; @@ -8,7 +8,7 @@ export class Settings { static backend: Backend = Backend.PureStake; static backend_settings: { [key: string]: any } = { [Backend.PureStake]: { - [Ledger.TestNet]: { + [Network.TestNet]: { [API.Algod]: { url: 'https://algosigner.api.purestake.io/testnet/algod', port: '', @@ -18,7 +18,7 @@ export class Settings { port: '', }, }, - [Ledger.MainNet]: { + [Network.MainNet]: { [API.Algod]: { url: 'https://algosigner.api.purestake.io/mainnet/algod', port: '', @@ -33,41 +33,40 @@ export class Settings { 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() { + public static getCleansedInjectedNetworks(): Array { const injectedNetworks = []; - const injectedNetworkKeys = Object.keys(this.backend_settings.InjectedNetworks); - for (var i = 0; i < injectedNetworkKeys.length; i++) { + const injectedNetworkNames = Object.keys(this.backend_settings.InjectedNetworks); + for (let i = 0; i < injectedNetworkNames.length; i++) { injectedNetworks.push({ - name: this.backend_settings.InjectedNetworks[injectedNetworkKeys[i]].name, - genesisId: this.backend_settings.InjectedNetworks[injectedNetworkKeys[i]].genesisId, + name: this.backend_settings.InjectedNetworks[injectedNetworkNames[i]].name, + genesisID: this.backend_settings.InjectedNetworks[injectedNetworkNames[i]].genesisID, + genesisHash: this.backend_settings.InjectedNetworks[injectedNetworkNames[i]].genesisHash, }); } return injectedNetworks; } - private static setInjectedHeaders(ledger: LedgerTemplate, isCheckOnly?: boolean) { - if (!this.backend_settings.InjectedNetworks[ledger.name] && !isCheckOnly) { - console.log('Error: Ledger headers can not be updated. Ledger not available.'); - return; - } - + public static getConnectionFromTemplate(network: NetworkTemplate): Connection { // Initialize headers for apiKey and individuals if there let headers = {}; let headersAlgod = undefined; let headersIndexer = undefined; - if (ledger['headers']) { + if (network['headers']) { // Set the headers to the base level first, this allows a string key to be used - headers = ledger['headers']; + headers = network['headers']; // Then try to parse the headers, in the case it is a string object. try { - headers = JSON.parse(ledger['headers']); + headers = JSON.parse(network['headers']); + // Get individual sub headers if they are available + if (headers['Algod']) { + headersAlgod = headers['Algod']; + } + if (headers['Indexer']) { + headersIndexer = headers['Indexer']; + } } catch (e) { // Use headers default value, but use it as a token if it is a string if (typeof headers === 'string') { @@ -76,104 +75,73 @@ export class Settings { 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'; } // Setup port splits for algod and indexer - used in sandbox installs - const parsedAlgodUrlObj = parseUrlServerAndPort(ledger.algodUrl); - const parsedIndexerUrlObj = parseUrlServerAndPort(ledger.indexerUrl); + const parsedAlgodUrl = parseUrlServerAndPort(network.algodUrl); + const parsedIndexerUrl = parseUrlServerAndPort(network.indexerUrl); - // Add algod links - const injectedAlgod = { - url: parsedAlgodUrlObj.server || `${defaultUrl}/algod`, - port: parsedAlgodUrlObj.port, + // Add algod connection + const injectedAlgod: ConnectionDetails = { + url: parsedAlgodUrl.server, + port: parsedAlgodUrl.port, apiKey: headersAlgod || headers, headers: headersAlgod || headers, }; - // Add the indexer links - const injectedIndexer = { - url: parsedIndexerUrlObj.server || `${defaultUrl}/indexer`, - port: parsedIndexerUrlObj.port, + // Add the indexer connection + const injectedIndexer: ConnectionDetails = { + url: parsedIndexerUrl.server, + port: parsedIndexerUrl.port, apiKey: headersIndexer || headers, headers: headersIndexer || headers, }; - if (isCheckOnly) { - return { - algod: injectedAlgod, - indexer: injectedIndexer, - }; - } else { - this.backend_settings.InjectedNetworks[ledger.name][API.Algod] = injectedAlgod; - this.backend_settings.InjectedNetworks[ledger.name][API.Indexer] = injectedIndexer; - this.backend_settings.InjectedNetworks[ledger.name].headers = headers; - } + return { + headers: headers, + algod: injectedAlgod, + indexer: injectedIndexer, + }; } - 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 || '', - }; + public static addInjectedNetwork(network: NetworkTemplate): void { + const targetName = network.name; - this.setInjectedHeaders(ledger); - logging.log( - `Added Network:\n${JSON.stringify( - this.backend_settings.InjectedNetworks[ledger.name], - null, - 1 - )}`, - LogLevel.Debug - ); + // Create settings entry and update w/ headers + this.backend_settings.InjectedNetworks[targetName] = { + name: targetName, + genesisID: network.genesisID, + genesisHash: network.genesisHash, + }; + const connection = this.getConnectionFromTemplate(network); + this.backend_settings.InjectedNetworks[targetName][API.Algod] = connection.algod; + this.backend_settings.InjectedNetworks[targetName][API.Indexer] = connection.indexer; + this.backend_settings.InjectedNetworks[targetName].headers = connection.headers; + logging.log(`Added Network ${targetName}:`, LogLevel.Debug); + logging.log(this.backend_settings.InjectedNetworks[targetName], LogLevel.Debug); } - public static updateInjectedNetwork(updatedLedger: LedgerTemplate, previousName: string = '') { - const targetName = updatedLedger.uniqueName; - - if (previousName) { + public static updateInjectedNetwork(updatedNetwork: NetworkTemplate, previousName: string): void { + if (!this.backend_settings.InjectedNetworks[previousName]) { + logging.log(`Unable to overwrite Network ${previousName}.`, LogLevel.Debug); + } else { + logging.log(`Overwriting Network ${previousName}.`, LogLevel.Debug); this.deleteInjectedNetwork(previousName); - this.backend_settings.InjectedNetworks[targetName] = {}; + this.addInjectedNetwork(updatedNetwork); } - this.backend_settings.InjectedNetworks[targetName].genesisId = updatedLedger.genesisId; - this.backend_settings.InjectedNetworks[targetName].symbol = updatedLedger.symbol; - this.backend_settings.InjectedNetworks[targetName].genesisHash = - updatedLedger.genesisHash; - this.backend_settings.InjectedNetworks[targetName].algodUrl = updatedLedger.algodUrl; - this.backend_settings.InjectedNetworks[targetName].indexerUrl = - updatedLedger.indexerUrl; - this.setInjectedHeaders(updatedLedger); + } - logging.log( - `Updated Network:\n${JSON.stringify( - this.backend_settings.InjectedNetworks[targetName], - null, - 1 - )}`, - LogLevel.Debug - ); + public static deleteInjectedNetwork(network: string): void { + delete this.backend_settings.InjectedNetworks[network]; + logging.log(`Deleted Network ${network}:`, LogLevel.Debug); } - public static getBackendParams(ledger: string, api: API) { + public static getBackendParams(network: string, api: API): ConnectionDetails { // If we are using the PureStake backend we can return the url, port, and apiKey - if (this.backend_settings[this.backend][ledger]) { + if (this.backend_settings[this.backend][network]) { return { - url: this.backend_settings[this.backend][ledger][api].url, - port: this.backend_settings[this.backend][ledger][api].port, + url: this.backend_settings[this.backend][network][api].url, + port: this.backend_settings[this.backend][network][api].port, apiKey: this.backend_settings[this.backend].apiKey, headers: {}, }; @@ -181,15 +149,10 @@ export class Settings { // Here we have to grab data from injected networks instead of the backend return { - url: this.backend_settings.InjectedNetworks[ledger][api].url, - port: this.backend_settings.InjectedNetworks[ledger][api].port, - apiKey: this.backend_settings.InjectedNetworks[ledger][api].apiKey, - headers: this.backend_settings.InjectedNetworks[ledger][api].headers, + url: this.backend_settings.InjectedNetworks[network][api].url, + port: this.backend_settings.InjectedNetworks[network][api].port, + apiKey: this.backend_settings.InjectedNetworks[network][api].apiKey, + headers: this.backend_settings.InjectedNetworks[network][api].headers, }; } - - public static checkNetwork(ledger: LedgerTemplate) { - const networks = this.setInjectedHeaders(ledger, true); - return networks; - } } diff --git a/packages/extension/src/background/messaging/internalMethods.test.ts b/packages/extension/src/background/messaging/internalMethods.test.ts index 26b8a73a..e1fd67cb 100644 --- a/packages/extension/src/background/messaging/internalMethods.test.ts +++ b/packages/extension/src/background/messaging/internalMethods.test.ts @@ -1,5 +1,5 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { Ledger } from '@algosigner/common/types'; +import { Network } from '@algosigner/common/types/network'; import encryptionWrap from '../encryptionWrap'; import { InternalMethods } from './internalMethods'; import algosdk from 'algosdk'; @@ -94,11 +94,11 @@ describe('wallet flow', () => { InternalMethods[JsonRpcMethod.CreateWallet](request, sendResponse); expect(sendResponse).toHaveBeenCalledWith({ - availableLedgers: [ + availableNetworks: [ { algodUrl: undefined, genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', - genesisId: 'mainnet-v1.0', + genesisID: 'mainnet-v1.0', headers: undefined, indexerUrl: undefined, isEditable: false, @@ -108,7 +108,7 @@ describe('wallet flow', () => { { algodUrl: undefined, genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', - genesisId: 'testnet-v1.0', + genesisID: 'testnet-v1.0', headers: undefined, indexerUrl: undefined, isEditable: false, @@ -116,7 +116,7 @@ describe('wallet flow', () => { symbol: undefined, }, ], - ledger: Ledger.MainNet, + network: Network.MainNet, wallet: { TestNet: [], MainNet: [], @@ -137,12 +137,12 @@ describe('wallet flow', () => { }; const sendResponse = jest.fn(); - const session = { - availableLedgers: [ + const sessionObject = { + availableNetworks: [ { algodUrl: undefined, genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', - genesisId: 'mainnet-v1.0', + genesisID: 'mainnet-v1.0', headers: undefined, indexerUrl: undefined, isEditable: false, @@ -152,7 +152,7 @@ describe('wallet flow', () => { { algodUrl: undefined, genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', - genesisId: 'testnet-v1.0', + genesisID: 'testnet-v1.0', headers: undefined, indexerUrl: undefined, isEditable: false, @@ -160,7 +160,7 @@ describe('wallet flow', () => { symbol: undefined, }, ], - ledger: Ledger.MainNet, + network: Network.MainNet, wallet: { TestNet: [], MainNet: [], @@ -176,7 +176,7 @@ describe('wallet flow', () => { InternalMethods[JsonRpcMethod.GetSession](request, sendResponse); - expect(sendResponse).toHaveBeenCalledWith({ exist: true, session: session }); + expect(sendResponse).toHaveBeenCalledWith({ exist: true, session: sessionObject }); }); test('a TestNet account can be added', () => { @@ -197,7 +197,7 @@ describe('wallet flow', () => { params: { ...account, mnemonic: mnemonic, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, @@ -238,7 +238,7 @@ describe('wallet flow', () => { params: { name: testImportAccount.name, mnemonic: testImportAccount.mnemonic, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, @@ -283,7 +283,7 @@ describe('wallet flow', () => { method: method, params: { address: testImportAccount.address, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, @@ -329,7 +329,7 @@ describe('algosdk methods', () => { method: method, params: { address: testImportAccount.address, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, @@ -371,7 +371,7 @@ describe('algosdk methods', () => { method: method, params: { address: testImportAccount.address, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, @@ -505,7 +505,7 @@ describe('algosdk methods', () => { method: method, params: { assetId: 12008492, - ledger: Ledger.TestNet, + ledger: Network.TestNet, }, id: '17402bbaa89', }, diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 212f11b3..78968763 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -1,50 +1,64 @@ import algosdk from 'algosdk'; -import { JsonRpcMethod } from '@algosigner/common/messaging/types'; + +import { extensionBrowser } from '@algosigner/common/chrome'; +import { AliasConfig } from '@algosigner/common/config'; +import { RequestError } from '@algosigner/common/errors'; import { logging, LogLevel } from '@algosigner/common/logging'; +import { Alias, Namespace, NamespaceConfig, SessionObject, SensitiveAccount, WalletStorage } from '@algosigner/common/types'; +import { ConnectionDetails, getBaseSupportedNetworks, Network, NetworkTemplate } from '@algosigner/common/types/network'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; -import { Alias, Ledger, Namespace, NamespaceConfig } from '@algosigner/common/types'; -import { RequestError } from '@algosigner/common/errors'; -import { AliasConfig } from '@algosigner/common/config'; -import { Task } from './task'; -import { API, Cache } from './types'; + import { Settings } from '../config'; import encryptionWrap from '../encryptionWrap'; import Session from '../utils/session'; import AssetsDetailsHelper from '../utils/assetsDetailsHelper'; -import { initializeCache, getAvailableLedgersExt } from '../utils/helper'; +import { initializeCache, getAvailableNetworksFromCache } from '../utils/helper'; import { ValidationStatus } from '../utils/validator'; import { calculateEstimatedFee, getValidatedTxnWrap, - getLedgerFromGenesisId, - getLedgerFromMixedGenesis, + getNetworkNameFromGenesisID, + getNetworkFromMixedGenesis, } from '../transaction/actions'; import { BaseValidatedTxnWrap } from '../transaction/baseValidatedTxnWrap'; import { buildTransaction } from '../utils/transactionBuilder'; -import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; -import { extensionBrowser } from '@algosigner/common/chrome'; + +import { Task } from './task'; +import { API, Cache } from './types'; const session = new Session(); export class InternalMethods { private static _encryptionWrap: encryptionWrap | undefined; - public static getAlgod(ledger: string): algosdk.Algodv2 { - const params = Settings.getBackendParams(ledger, API.Algod); - return new algosdk.Algodv2(params.apiKey, params.url, params.port); + public static getAlgod(network: string | NetworkTemplate): algosdk.Algodv2 { + let connection: ConnectionDetails; + if (typeof network === 'string') { + connection = Settings.getBackendParams(network, API.Algod); + } else { + connection = Settings.getConnectionFromTemplate(network).algod; + } + return new algosdk.Algodv2(connection.apiKey, connection.url, connection.port); } - public static getIndexer(ledger: string): algosdk.Indexer { - const params = Settings.getBackendParams(ledger, API.Indexer); - return new algosdk.Indexer(params.apiKey, params.url, params.port); + + public static getIndexer(network: string): algosdk.Indexer { + let connection: ConnectionDetails; + if (typeof network === 'string') { + connection = Settings.getBackendParams(network, API.Indexer); + } else { + connection = Settings.getConnectionFromTemplate(network).indexer; + } + return new algosdk.Indexer(connection.apiKey, connection.url, connection.port); } - private static safeWallet(wallet: any) { - // Intialize the safe wallet then add the wallet ledgers in as empty arrays - const safeWallet = {}; + private static safeWallet(wallet: WalletStorage): WalletStorage { + // Intialize the safe wallet then add the wallet networks in as empty arrays + const safeWallet: WalletStorage = {}; Object.keys(wallet).forEach((key) => { safeWallet[key] = []; - // Afterwards we can add in all the non-private keys and names into the safewallet + // Afterwards we can add in all the non-private keys and names into the safeWallet for (let j = 0; j < wallet[key].length; j++) { const { address, name, isRef } = wallet[key][j]; safeWallet[key].push({ @@ -77,19 +91,19 @@ export class InternalMethods { }); } - for (const l in Ledger) { + for (const n of Object.keys(wallet)) { // Format accounts as aliases - const ledgerAccountAliases = []; - for (const acc of wallet[l]) { - ledgerAccountAliases.push({ + const networkAccountAliases = []; + for (const acc of wallet[n]) { + networkAccountAliases.push({ name: acc.name, address: acc.address, namespace: Namespace.AlgoSigner_Accounts, }); } // Save accounts and contacts as aliases - aliases[l] = { - [Namespace.AlgoSigner_Accounts]: ledgerAccountAliases, + aliases[n] = { + [Namespace.AlgoSigner_Accounts]: networkAccountAliases, [Namespace.AlgoSigner_Contacts]: contactAliases, }; } @@ -98,21 +112,21 @@ export class InternalMethods { }); } - // Checks if an account for the given address exists on AlgoSigner for a given ledger. + // Checks if an account for the given address exists on AlgoSigner for a given network. public static checkAccountIsImported(genesisID: string, address: string): void { - const ledger: string = getLedgerFromGenesisId(genesisID); + const network: string = getNetworkNameFromGenesisID(genesisID); let found = false; - for (let i = session.wallet[ledger].length - 1; i >= 0; i--) { - if (session.wallet[ledger][i].address === address) { + for (let i = session.wallet[network].length - 1; i >= 0; i--) { + if (session.wallet[network][i].address === address) { found = true; break; } } - if (!found) throw RequestError.NoAccountMatch(address, ledger); + if (!found) throw RequestError.NoAccountMatch(address, network); } - private static loadAccountAssetsDetails(address: string, ledger: Ledger) { - const algod = this.getAlgod(ledger); + private static loadAccountAssetsDetails(address: string, network: Network) { + const algod = this.getAlgod(network); algod .accountInformation(address) .do() @@ -120,7 +134,7 @@ export class InternalMethods { if ('assets' in res && res.assets.length > 0) { AssetsDetailsHelper.add( res.assets.map((x) => x['asset-id']), - ledger + network ); } }) @@ -129,8 +143,8 @@ export class InternalMethods { }); } - public static getHelperSession(): Session { - return session.session; + public static getSessionObject(): SessionObject { + return session.asObject(); } public static clearSession(): void { @@ -144,7 +158,7 @@ export class InternalMethods { if (!exist) { sendResponse({ exist: false }); } else { - if (session.wallet) sendResponse({ exist: true, session: session.session }); + if (session.wallet) sendResponse({ exist: true, session: session.asObject() }); else sendResponse({ exist: true }); } }); @@ -162,7 +176,7 @@ export class InternalMethods { }; extensionStorage.setStorage( 'aliases', - { [Ledger.MainNet]: emptyAliases, [Ledger.MainNet]: emptyAliases }, + { [Network.MainNet]: emptyAliases, [Network.MainNet]: emptyAliases }, null ); const namespaceConfigs: Array = []; @@ -176,19 +190,20 @@ export class InternalMethods { } extensionStorage.setStorage('namespaces', namespaceConfigs, null); + const cache = initializeCache(); + extensionStorage.setStorage('cache', cache, null); + this._encryptionWrap = new encryptionWrap(request.body.params.passphrase); const newWallet = { - [Ledger.MainNet]: [], - [Ledger.TestNet]: [], + [Network.MainNet]: [], + [Network.TestNet]: [], }; this._encryptionWrap?.lock(JSON.stringify(newWallet), (isSuccessful: any) => { if (isSuccessful) { - getAvailableLedgersExt((availableLedgers) => { - session.availableLedgers = availableLedgers; - session.wallet = this.safeWallet(newWallet); - session.ledger = Ledger.MainNet; - sendResponse(session.session); - }); + session.availableNetworks = getBaseSupportedNetworks(); + session.wallet = this.safeWallet(newWallet); + session.network = Network.MainNet; + sendResponse(session.asObject()); } else { sendResponse({ error: 'Lock failed' }); } @@ -225,28 +240,46 @@ export class InternalMethods { sendResponse(response); } else { const wallet = this.safeWallet(response); - getAvailableLedgersExt((availableLedgers) => { + getAvailableNetworksFromCache((cachedNetworks: Array) => { const extensionStorage = new ExtensionStorage(); // Load Accounts details from Cache - extensionStorage.getStorage('cache', (storedCache: any) => { + extensionStorage.getStorage('cache', async (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]; + const cachedNetworksWithAccounts = Object.keys(cache.accounts); + + for (let j = cachedNetworksWithAccounts.length - 1; j >= 0; j--) { + const network = cachedNetworksWithAccounts[j]; + if (wallet[network]) { + for (let i = wallet[network].length - 1; i >= 0; i--) { + if (wallet[network][i].address in cache.accounts[network]) { + wallet[network][i].details = cache.accounts[network][wallet[network][i].address]; } } } } + const updatedNetworks: Array = []; + logging.log('Checking for networks to update genesis info.', LogLevel.Debug); + for (const [index, network] of cachedNetworks.entries()) { + if (network.isEditable) { + logging.log(`Fetching updated genesis info for network '${network.name}'`, LogLevel.Debug); + try { + const txnParams = await this.getAlgod(network.name).getTransactionParams().do(); + network.genesisID = txnParams.genesisID; + network.genesisHash = txnParams.genesisHash; + } catch (e) { + logging.log(e.message, LogLevel.Debug); + } + } + updatedNetworks[index] = network; + } + cache.availableLedgers = updatedNetworks; + extensionStorage.setStorage('cache', cache, null); + // Setup session session.wallet = wallet; - session.ledger = Ledger.MainNet; - session.availableLedgers = availableLedgers; + session.network = Network.MainNet; + session.availableNetworks = updatedNetworks; // Load internal aliases && namespace configurations this.reloadAliases(); @@ -271,7 +304,7 @@ export class InternalMethods { extensionStorage.setStorage('namespaces', namespaceConfigs, null); }); - sendResponse(session.session); + sendResponse(session.asObject()); }); }); } @@ -288,20 +321,27 @@ export class InternalMethods { public static [JsonRpcMethod.ClearCache](request: any, sendResponse: Function) { const extensionStorage = new ExtensionStorage(); - extensionStorage.setStorage('cache', initializeCache(), (isSuccessful) => - sendResponse(isSuccessful) - ); + extensionStorage.getStorage('cache', (storedCache: Cache) => { + const cache: Cache = initializeCache(storedCache); + storedCache.availableLedgers.forEach((network) => { + cache.accounts[network.name] = {}; + cache.assets[network.name] = {}; + }); + extensionStorage.setStorage('cache', cache, (isSuccessful) => { + sendResponse(isSuccessful); + }); + }); return true; } public static [JsonRpcMethod.CreateAccount](request: any, sendResponse: Function) { - var keys = algosdk.generateAccount(); - var mnemonic = algosdk.secretKeyToMnemonic(keys.sk); + const keys = algosdk.generateAccount(); + const mnemonic = algosdk.secretKeyToMnemonic(keys.sk); sendResponse([mnemonic, keys.addr]); } public static [JsonRpcMethod.SaveAccount](request: any, sendResponse: Function) { - const { mnemonic, name, ledger, address, passphrase } = request.body.params; + const { mnemonic, name, ledger: network, address, passphrase } = request.body.params; this._encryptionWrap = new encryptionWrap(passphrase); this._encryptionWrap.unlock((unlockedValue: any) => { @@ -314,11 +354,11 @@ export class InternalMethods { name: name, }; - if (!unlockedValue[ledger]) { - unlockedValue[ledger] = []; + if (!unlockedValue[network]) { + unlockedValue[network] = []; } - unlockedValue[ledger].push(newAccount); + unlockedValue[network].push(newAccount); this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { if (isSuccessful) { session.wallet = this.safeWallet(unlockedValue); @@ -334,7 +374,7 @@ export class InternalMethods { } public static [JsonRpcMethod.DeleteAccount](request: any, sendResponse: Function) { - const { ledger, address, passphrase } = request.body.params; + const { ledger: network, address, passphrase } = request.body.params; this._encryptionWrap = new encryptionWrap(passphrase); this._encryptionWrap.unlock((unlockedValue: any) => { @@ -342,9 +382,9 @@ export class InternalMethods { sendResponse(unlockedValue); } else { // Find address to delete - for (var i = unlockedValue[ledger].length - 1; i >= 0; i--) { - if (unlockedValue[ledger][i].address === address) { - unlockedValue[ledger].splice(i, 1); + for (var i = unlockedValue[network].length - 1; i >= 0; i--) { + if (unlockedValue[network][i].address === address) { + unlockedValue[network].splice(i, 1); break; } } @@ -363,12 +403,12 @@ export class InternalMethods { } public static [JsonRpcMethod.ImportAccount](request: any, sendResponse: Function) { - const { mnemonic, address, isRef, name, ledger } = request.body.params; + const { mnemonic, address, isRef, name, ledger: network } = request.body.params; this._encryptionWrap = new encryptionWrap(request.body.params.passphrase); - let newAccount; + let newAccount: SensitiveAccount; try { - const existingAccounts = session.wallet[ledger]; + const existingAccounts = session.wallet[network]; let targetAddress = address; if (!isRef) { @@ -379,11 +419,11 @@ export class InternalMethods { for (let i = 0; i < existingAccounts.length; i++) { if (existingAccounts[i].address === targetAddress) { throw new Error( - `An account with this address already exists in your ${ledger} wallet.` + `An account with this address already exists in your ${network} wallet.` ); } if (existingAccounts[i].name === name) { - throw new Error(`An account named '${name}' already exists in your ${ledger} wallet.`); + throw new Error(`An account named '${name}' already exists in your ${network} wallet.`); } } } @@ -403,14 +443,14 @@ export class InternalMethods { if ('error' in unlockedValue) { sendResponse(unlockedValue); } else { - if (!unlockedValue[ledger]) { - unlockedValue[ledger] = []; + if (!unlockedValue[network]) { + unlockedValue[network] = []; } - unlockedValue[ledger].push(newAccount); + unlockedValue[network].push(newAccount); this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { if (isSuccessful) { - this.loadAccountAssetsDetails(newAccount.address, ledger); + this.loadAccountAssetsDetails(newAccount.address, network); session.wallet = this.safeWallet(unlockedValue); this.reloadAliases(); sendResponse(session.wallet); @@ -424,11 +464,11 @@ export class InternalMethods { } public static [JsonRpcMethod.LedgerLinkAddress](request: any, sendResponse: Function) { - const ledger = request.body.params.ledger; + const network = request.body.params.ledger; extensionBrowser.tabs.create( { active: true, - url: extensionBrowser.extension.getURL(`/index.html#/${ledger}/ledger-hardware-connector`), + url: extensionBrowser.extension.getURL(`/index.html#/${network}/ledger-hardware-connector`), }, (tab) => { // Tab object is created here, but extension popover will close. @@ -500,9 +540,9 @@ export class InternalMethods { sendResponse({ message: message }); } else if (session.txnRequest.source === 'ui') { // If this is an UI transaction then we need to submit to the network - const ledger = getLedgerFromGenesisId(decodedTxn.txn.genesisID); + const network = getNetworkNameFromGenesisID(decodedTxn.txn.genesisID); - const algod = this.getAlgod(ledger); + const algod = this.getAlgod(network); algod .sendRawTransaction(txnBuffer) .do() @@ -545,11 +585,11 @@ export class InternalMethods { // Transaction wrap will contain response message if from dApp and structure will be different const txn = session.txnObject.transactionWraps[0].transaction; - const ledger = getLedgerFromMixedGenesis(txn.genesisID, txn.genesisHash); + const network = getNetworkFromMixedGenesis(txn.genesisID, txn.genesisHash); extensionBrowser.tabs.create( { active: true, - url: extensionBrowser.extension.getURL(`/index.html#/${ledger.name}/ledger-hardware-sign`), + url: extensionBrowser.extension.getURL(`/index.html#/${network.name}/ledger-hardware-sign`), }, (tab) => { // Tab object is created here, but extension popover will close. @@ -559,7 +599,7 @@ export class InternalMethods { } public static [JsonRpcMethod.LedgerSaveAccount](request: any, sendResponse: Function) { - const { name, ledger, passphrase } = request.body.params; + const { name, ledger: network, passphrase } = request.body.params; // The value returned from the Ledger device is hex. // This is passed directly to save and needs to be converted. const address = algosdk.encodeAddress(Buffer.from(request.body.params.hexAddress, 'hex')); @@ -575,11 +615,11 @@ export class InternalMethods { isHardware: true, }; - if (!unlockedValue[ledger]) { - unlockedValue[ledger] = []; + if (!unlockedValue[network]) { + unlockedValue[network] = []; } - unlockedValue[ledger].push(newAccount); + unlockedValue[network].push(newAccount); this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { if (isSuccessful) { session.wallet = this.safeWallet(unlockedValue); @@ -594,31 +634,31 @@ export class InternalMethods { } public static [JsonRpcMethod.AccountDetails](request: any, sendResponse: Function) { - const { ledger, address } = request.body.params; - const algod = this.getAlgod(ledger); + const { ledger: network, address } = request.body.params; + const algod = this.getAlgod(network); algod .accountInformation(address) .do() .then((res: any) => { const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (storedCache: any) => { - const cache: Cache = initializeCache(storedCache, ledger); + const cache: Cache = initializeCache(storedCache, network); // Check for asset details saved in storage, if needed if ('assets' in res && res.assets.length > 0) { const assetDetails = []; for (var i = res.assets.length - 1; i >= 0; i--) { const assetId = res.assets[i]['asset-id']; - if (assetId in cache.assets[ledger]) { + if (assetId in cache.assets[network]) { res.assets[i] = { - ...cache.assets[ledger][assetId], + ...cache.assets[network][assetId], ...res.assets[i], }; } assetDetails.push(assetId); } - if (assetDetails.length > 0) AssetsDetailsHelper.add(assetDetails, ledger); + if (assetDetails.length > 0) AssetsDetailsHelper.add(assetDetails, network); res.assets.sort((a, b) => a['asset-id'] - b['asset-id']); } @@ -626,16 +666,16 @@ export class InternalMethods { sendResponse(res); // Save account updated account details in cache - cache.accounts[ledger][address] = res; + cache.accounts[network][address] = res; extensionStorage.setStorage('cache', cache, null); // Add details to session 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; + // Validate the network still exists in the wallet + if (network && wallet[network]) { + for (var i = wallet[network].length - 1; i >= 0; i--) { + if (wallet[network][i].address === address) { + wallet[network][i].details = res; } } } @@ -648,9 +688,9 @@ export class InternalMethods { } public static [JsonRpcMethod.Transactions](request: any, sendResponse: Function) { - const { ledger, address, limit, 'next-token': token } = request.body.params; - const indexer = this.getIndexer(ledger); - const algod = this.getAlgod(ledger); + const { ledger: network, address, limit, 'next-token': token } = request.body.params; + const indexer = this.getIndexer(network); + const algod = this.getAlgod(network); const txList = indexer.lookupAccountTransactions(address); const pendingTxList = algod.pendingTransactionByAddress(address); if (limit) txList.limit(limit); @@ -670,7 +710,7 @@ export class InternalMethods { // asset or appl id const id = pend['txn']['xaid'] || pend['txn']['faid'] || pend['txn']['apid']; const cachedAsset = - (pend['txn']['xaid'] || pend['txn']['faid']) && cache.assets[ledger][id]; + (pend['txn']['xaid'] || pend['txn']['faid']) && cache.assets[network][id]; const assetName = cachedAsset && (cachedAsset['unit-name'] || cachedAsset['name'] || cachedAsset['asset-id']); @@ -702,9 +742,9 @@ export class InternalMethods { } public static [JsonRpcMethod.AssetDetails](request: any, sendResponse: Function) { + const { ledger: network } = request.body.params; const assetId = request.body.params['asset-id']; - const { ledger } = request.body.params; - const indexer = this.getIndexer(ledger); + const indexer = this.getIndexer(network); indexer .lookupAssetByID(assetId) .do() @@ -714,10 +754,10 @@ export class InternalMethods { const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (cache: any) => { if (cache === undefined) cache = new Cache(); - if (!(ledger in cache.assets)) cache.assets[ledger] = {}; + if (!(network in cache.assets)) cache.assets[network] = {}; - if (!(assetId in cache.assets[ledger])) { - cache.assets[ledger][assetId] = res.asset.params; + if (!(assetId in cache.assets[network])) { + cache.assets[network][assetId] = res.asset.params; extensionStorage.setStorage('cache', cache, null); } }); @@ -752,8 +792,8 @@ export class InternalMethods { }); } - const { ledger, filter, nextToken } = request.body.params; - const indexer = this.getIndexer(ledger); + const { ledger: network, filter, nextToken } = request.body.params; + const indexer = this.getIndexer(network); // 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)) { @@ -774,9 +814,9 @@ export class InternalMethods { } public static [JsonRpcMethod.AssetsVerifiedList](request: any, sendResponse: Function) { - const { ledger } = request.body.params; + const { ledger: network } = request.body.params; - if (ledger === Ledger.MainNet) { + if (network === Network.MainNet) { fetch('https://mobile-api.algorand.com/api/assets/?status=verified') .then((response) => { return response.json().then((json) => { @@ -799,9 +839,9 @@ export class InternalMethods { } public static [JsonRpcMethod.SignSendTransaction](request: any, sendResponse: Function) { - const { ledger, address, passphrase, txnParams } = request.body.params; + const { ledger: network, address, passphrase, txnParams } = request.body.params; this._encryptionWrap = new encryptionWrap(passphrase); - const algod = this.getAlgod(ledger); + const algod = this.getAlgod(network); this._encryptionWrap.unlock(async (unlockedValue: any) => { if ('error' in unlockedValue) { @@ -822,13 +862,13 @@ export class InternalMethods { if ('note' in txn) txn.note = new Uint8Array(Buffer.from(txn.note)); let account; - const authAddr = await Task.getChainAuthAddress({ address, ledger }); + const authAddr = await Task.getChainAuthAddress({ address, network }); const signAddress = authAddr || address; // Find address to send algos from - for (var i = unlockedValue[ledger].length - 1; i >= 0; i--) { - if (unlockedValue[ledger][i].address === signAddress) { - account = unlockedValue[ledger][i]; + for (var i = unlockedValue[network].length - 1; i >= 0; i--) { + if (unlockedValue[network][i].address === signAddress) { + account = unlockedValue[network][i]; break; } } @@ -915,7 +955,7 @@ export class InternalMethods { return false; } - this.getAlgod(ledger) + this.getAlgod(network) .sendRawTransaction(signedTxn.blob) .do() .then((resp: any) => { @@ -941,30 +981,31 @@ export class InternalMethods { return true; } - public static [JsonRpcMethod.ChangeLedger](request: any, sendResponse: Function) { - session.ledger = request.body.params['ledger']; - sendResponse({ ledger: session.ledger }); + public static [JsonRpcMethod.ChangeNetwork](request: any, sendResponse: Function) { + session.network = request.body.params['ledger']; + sendResponse({ ledger: session.network }); } 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); + const networkName = request.body.params['name']; + getAvailableNetworksFromCache((availiableNetworks) => { + const matchingNetwork = availiableNetworks.find( + (network) => network.name === networkName + ); - if (!matchingLedger || !matchingLedger.isEditable) { - sendResponse({ error: 'This ledger can not be deleted.' }); + if (!matchingNetwork || !matchingNetwork.isEditable) { + sendResponse({ error: 'This network can not be deleted.' }); } else { - // Delete ledger from availableLedgers and assign to new array // - const remainingLedgers = availiableLedgers.filter( - (avls) => avls.uniqueName !== ledgerUniqueName + // Delete network from availableNetworks and assign to new array + const remainingNetworks = availiableNetworks.filter( + (network) => network.name !== networkName ); - // Delete Accounts from wallet // + // 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']]; + // Remove existing accounts in session.wallet + const existingAccounts = session.wallet[request.body.params['ledger']]; if (existingAccounts) { delete session.wallet[request.body.params['ledger']]; } @@ -974,9 +1015,9 @@ export class InternalMethods { if ('error' in unlockedValue) { sendResponse(unlockedValue); } else { - if (unlockedValue[ledger]) { - // The unlocked value contains ledger information - delete it - delete unlockedValue[ledger]; + if (unlockedValue[networkName]) { + // The unlocked value contains network information - delete it + delete unlockedValue[networkName]; } // Resave the updated wallet value @@ -988,25 +1029,25 @@ export class InternalMethods { } }) .then(() => { - // Update cache with remaining ledgers// + // Update cache with remaining networks const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (cache: any) => { if (cache === undefined) { cache = initializeCache(cache); } if (cache) { - cache.availableLedgers = remainingLedgers; + cache.availableLedgers = remainingNetworks; extensionStorage.setStorage('cache', cache, () => {}); } - // Update the session // - session.availableLedgers = remainingLedgers; + // Update the session + session.availableNetworks = remainingNetworks; - // Delete from the injected ledger settings // - Settings.deleteInjectedNetwork(ledgerUniqueName); + // Delete from the injected network settings + Settings.deleteInjectedNetwork(networkName); - // Send back remaining ledgers // - sendResponse({ availableLedgers: remainingLedgers }); + // Send back remaining networks + sendResponse({ availableNetworks: remainingNetworks }); }); }); } @@ -1018,8 +1059,8 @@ export class InternalMethods { public static [JsonRpcMethod.CheckNetwork](request: any, sendResponse: Function) { try { - const networks = Settings.checkNetwork(request.body.params); - sendResponse(networks); + const connection = Settings.getConnectionFromTemplate(request.body.params); + sendResponse(connection); } catch (e) { sendResponse({ error: e.message }); } @@ -1031,66 +1072,104 @@ export class InternalMethods { // 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 (params['passphrase'] !== undefined) { - this._encryptionWrap = new encryptionWrap(request.body.params['passphrase']); + this._encryptionWrap = new encryptionWrap(params['passphrase']); this._encryptionWrap.unlock((unlockedValue: any) => { if ('error' in unlockedValue) { sendResponse(unlockedValue); + return true; } // We have evaluated the passphrase and it was valid. }); } - const previousName = params['previousName'].toLowerCase(); - const targetName = previousName ? previousName : params['name'].toLowerCase(); - const addedLedger = new LedgerTemplate({ + const addedNetwork = new NetworkTemplate({ name: params['name'], - genesisId: params['genesisId'], - genesisHash: params['genesisHash'], symbol: params['symbol'], algodUrl: params['algodUrl'], indexerUrl: params['indexerUrl'], headers: 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 === targetName)) { - comboLedgers.push(addedLedger); - - // Also add the ledger to the injected ledgers in settings - Settings.addInjectedNetwork(addedLedger); - } else { - // If the new ledger name does exist, we sould update the values as long as it is not a default ledger. - const matchingLedger = comboLedgers.find((cledg) => cledg.uniqueName === targetName); - if (!defaultLedgers.some((dledg) => dledg.uniqueName === matchingLedger.uniqueName)) { - Settings.updateInjectedNetwork(addedLedger, previousName); - matchingLedger.name = addedLedger.name; - matchingLedger.genesisId = addedLedger.genesisId; - matchingLedger.symbol = addedLedger.symbol; - matchingLedger.genesisHash = addedLedger.genesisHash; - matchingLedger.algodUrl = addedLedger.algodUrl; - matchingLedger.indexerUrl = addedLedger.indexerUrl; - matchingLedger.headers = addedLedger.headers; - } + // If network is not editable, it means they're trying to use a reserved name + if (!addedNetwork.isEditable) { + sendResponse({ error: `Default networks cannot be overwritten, please use a different name.` }); + return true; + } + logging.log(`Saving network '${addedNetwork.name}'`, LogLevel.Debug); + logging.log(addedNetwork, LogLevel.Debug); + + // Fetch currently stored networks from cache + getAvailableNetworksFromCache(async (cacheNetworks) => { + const availableNetworks = [...cacheNetworks]; + + // It's not a default network, fetch the updated genesis info + logging.log(`Fetching genesis info for network '${addedNetwork.name}'`, LogLevel.Debug); + try { + const txnParams = await this.getAlgod(addedNetwork).getTransactionParams().do(); + addedNetwork.genesisID = txnParams.genesisID; + addedNetwork.genesisHash = txnParams.genesisHash; + logging.log(txnParams, LogLevel.Debug); + } catch (e) { + logging.log(`Unable to fetch genesis info for network '${addedNetwork.name}'`, LogLevel.Debug); } - // 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); + extensionStorage.getStorage('cache', (storedCache: Cache) => { + const cache = initializeCache(storedCache); + const previousName = params['previousName']; + + const prepareAndSendFinalResponse = (activeNetwork: NetworkTemplate, networks: Array, session: Session, cache: Cache) => { + // Update the cache and session before sending response + session.network = activeNetwork.name as Network; + session.availableNetworks = networks; + cache.availableLedgers = networks; + extensionStorage.setStorage('cache', cache, null); + sendResponse(session.asObject()); } - if (cache) { - cache.availableLedgers = comboLedgers; - extensionStorage.setStorage('cache', cache, () => {}); + + // If there's a previous name, then we're overwriting an existing network + if (!previousName) { + // It's a new network, add into Settings. + availableNetworks.push(addedNetwork); + Settings.addInjectedNetwork(addedNetwork); + logging.log(`Adding new network '${addedNetwork.name}'`, LogLevel.Normal); + prepareAndSendFinalResponse(addedNetwork, availableNetworks, session, cache); + } else { + logging.log(`Updating network w/ previous name '${previousName}'`, LogLevel.Normal); + logging.log(availableNetworks, LogLevel.Normal); + // Otherwise overwrite existing network + const matchingIndex = availableNetworks.findIndex((n) => n.name === previousName); + availableNetworks[matchingIndex] = addedNetwork; + Settings.updateInjectedNetwork(addedNetwork, previousName); + + // If renaming, update storage, cache & session wallet keys + if (previousName !== addedNetwork.name) { + const targetName = addedNetwork.name; + logging.log(`Rename wallet network from '${previousName}' to '${targetName}'`, LogLevel.Debug); + + this._encryptionWrap.unlock((unlockedValue: WalletStorage) => { + // We have already validated the passphrase + unlockedValue[targetName] = [...unlockedValue[previousName]] + delete unlockedValue[previousName]; + this._encryptionWrap?.lock(JSON.stringify(unlockedValue), (isSuccessful: any) => { + if (isSuccessful) { + // Update network key on session, cache & reload aliases + session.wallet[targetName] = [...session.wallet[previousName]] + delete session.wallet[previousName]; + cache.accounts[targetName] = cache.accounts[previousName]; + delete cache.accounts[previousName]; + cache.assets[targetName] = cache.assets[previousName]; + delete cache.assets[previousName]; + this.reloadAliases(); + prepareAndSendFinalResponse(addedNetwork, availableNetworks, session, cache); + } else { + sendResponse({ error: 'Lock failed' }); + } + }); + }); + } else { + prepareAndSendFinalResponse(addedNetwork, availableNetworks, session, cache); + } } }); }); @@ -1100,9 +1179,9 @@ export class InternalMethods { } } - public static [JsonRpcMethod.GetLedgers](request: any, sendResponse: Function) { - getAvailableLedgersExt((availableLedgers) => { - sendResponse(availableLedgers); + public static [JsonRpcMethod.GetNetworks](request: any, sendResponse: Function) { + getAvailableNetworksFromCache((availableNetworks) => { + sendResponse(availableNetworks); }); return true; @@ -1177,15 +1256,15 @@ export class InternalMethods { } public static [JsonRpcMethod.GetAliasedAddresses](request: any, sendResponse: Function) { - const { ledger, searchTerm } = request.body.params; + const { ledger: network, searchTerm } = request.body.params; // Check if the term matches any of our namespaces - const matchingNamespaces: Array = AliasConfig.getMatchingNamespaces(ledger); + const matchingNamespaces: Array = AliasConfig.getMatchingNamespaces(network); const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('aliases', async (aliases: any) => { - // aliases: { ledger: { namespace: [...aliases] } } - await extensionStorage.getStorage( + // aliases: { network: { namespace: [...aliases] } } + extensionStorage.getStorage( 'namespaces', async (storedConfigs: Array) => { const availableExternalNamespaces: Array = []; @@ -1194,12 +1273,12 @@ export class InternalMethods { .map((config) => availableExternalNamespaces.push(config.namespace)); // Search the storage for the aliases stored for the matching namespaces - const returnedAliasedAddresses: Record> = {}; + const returnedAliasedAddresses: { [key: string]: Array } = {}; const apiFetches = []; for (const namespace of matchingNamespaces) { const aliasesMatchingInNamespace: Array = []; - if (aliases[ledger][namespace]) { - for (const alias of aliases[ledger][namespace]) { + if (aliases[network][namespace]) { + for (const alias of aliases[network][namespace]) { if (alias.name.toLowerCase().includes(searchTerm.toLowerCase())) { aliasesMatchingInNamespace.push({ name: alias.name, @@ -1214,11 +1293,11 @@ export class InternalMethods { if ( searchTerm.length && availableExternalNamespaces.includes(namespace) && - AliasConfig[namespace].ledgers && - AliasConfig[namespace].ledgers[ledger]?.length > 0 + AliasConfig[namespace].networks && + AliasConfig[namespace].networks[network]?.length > 0 ) { // If we find enabled external namespaces, we prepare an API fetch - const apiURL = AliasConfig[namespace].ledgers[ledger].replace('${term}', searchTerm); + const apiURL = AliasConfig[namespace].networks[network].replace('${term}', searchTerm); const apiTimeout = AliasConfig[namespace].apiTimeout; // We set a max timeout for each call diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index c36b4eef..3881044a 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -1,25 +1,27 @@ import algosdk, { MultisigMetadata, Transaction } from 'algosdk'; -import { OptsKeys, WalletTransaction } from '@algosigner/common/types'; +import { extensionBrowser } from '@algosigner/common/chrome'; +import { base64ToByteArray, byteArrayToBase64 } from '@algosigner/common/encoding'; import { RequestError } from '@algosigner/common/errors'; +import { logging, LogLevel } from '@algosigner/common/logging'; +import { OptsKeys, WalletTransaction } from '@algosigner/common/types'; +import { Connection, Network, NetworkSelectionType, NetworkTemplate } from '@algosigner/common/types/network'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { Ledger } from '@algosigner/common/types'; -import { API } from './types'; +import { areBuffersEqual } from '@algosigner/common/utils'; + import { getValidatedTxnWrap, - getLedgerFromGenesisId, - getLedgerFromMixedGenesis, + getNetworkNameFromGenesisID, + getNetworkFromMixedGenesis, } from '../transaction/actions'; import { BaseValidatedTxnWrap } from '../transaction/baseValidatedTxnWrap'; import { ValidationResponse, ValidationStatus } from '../utils/validator'; -import { InternalMethods } from './internalMethods'; -import { MessageApi } from './api'; import encryptionWrap from '../encryptionWrap'; import { Settings } from '../config'; -import { extensionBrowser } from '@algosigner/common/chrome'; -import { logging, LogLevel } from '@algosigner/common/logging'; -import { base64ToByteArray, byteArrayToBase64 } from '@algosigner/common/encoding'; -import { areBuffersEqual } from '@algosigner/common/utils'; + +import { MessageApi } from './api'; +import { InternalMethods } from './internalMethods'; +import { API } from './types'; // Popup properties accounts for additional space needed for the title bar const titleBarHeight = 28; @@ -33,29 +35,35 @@ const popupProperties = { export class Task { private static requests: { [key: string]: any } = {}; private static authorized_pool: Array = []; - private static authorized_pool_details: any = {} + private static authorized_pool_details: any = {}; public static isAuthorized(origin: string): boolean { return Task.authorized_pool.indexOf(origin) > -1; } - public static isPreAuthorized(origin: string, genesisID: string, requestedAccounts: Array): boolean { + public static isPreAuthorized( + origin: string, + genesisID: string, + requestedAccounts: Array + ): boolean { // Validate the origin is in the authorized pool if (Task.authorized_pool.indexOf(origin) === -1) { return false; } - // Validate the genesisID is the authorized one - // Note: The arc-0006 requires "genesisID" which matches the transaction, but we use "genesisId" internally in some places - if (!Task.authorized_pool_details[origin] || !(Task.authorized_pool_details[origin]['genesisID'] === genesisID)) { + // Validate the genesisID is previously authorized + if ( + !Task.authorized_pool_details[origin] || + !(Task.authorized_pool_details[origin]['genesisID'] === genesisID) + ) { return false; } // Validate the requested accounts exist in the pool detail for (let i = 0; i < requestedAccounts.length; i++) { - if (!(Task.authorized_pool_details[origin].accounts.includes(requestedAccounts[i]))) { + if (!Task.authorized_pool_details[origin].accounts.includes(requestedAccounts[i])) { return false; - } + } } // We made it through negative checks to accounts are currently authroized @@ -63,14 +71,24 @@ export class Task { } // Checks for the originId authorization in details then call to make sure the account exists in Algosigner. - private static checkAccountIsImportedAndAuthorized(genesisID: string, address: string, originId: string): void { - // Legacy authorized and internal calls will not have an originId in authorized pool details - if (Task.authorized_pool_details[originId]) { + private static checkAccountIsImportedAndAuthorized( + network: string, + genesisID: string, + genesisHash: string, + address: string, + origin: string + ): void { + // Legacy authorized and internal calls will not have an origin in authorized pool details + if (Task.authorized_pool_details[origin]) { // This must be a dApp using enable - verify the ledger and address are authorized - if ((Task.authorized_pool_details[originId]['genesisID'] !== genesisID) - || (!Task.authorized_pool_details[originId]['accounts'].includes(address))) { - throw RequestError.NoAccountMatch(address, genesisID); - } + if ( + Task.authorized_pool_details[origin]['genesisID'] !== genesisID || + Task.authorized_pool_details[origin]['ledger'] !== network || + (genesisHash && Task.authorized_pool_details[origin]['genesisHash'] !== genesisHash) || + !Task.authorized_pool_details[origin]['accounts'].includes(address) + ) { + throw RequestError.NoAccountMatch(address, network); + } } // Call the normal account check InternalMethods.checkAccountIsImported(genesisID, address); @@ -102,10 +120,10 @@ export class Task { public static getChainAuthAddress = async (transaction: any): Promise => { // The ledger and address will be provided differently from UI and dapp - const ledger = transaction.ledger || getLedgerFromGenesisId(transaction.genesisID); + const network = transaction.network || getNetworkNameFromGenesisID(transaction.genesisID); const address = transaction.address || transaction.from; - const conn = Settings.getBackendParams(ledger, API.Algod); + const conn = Settings.getBackendParams(network, API.Algod); const sendPath = `/v2/accounts/${address}`; const fetchParams: any = { headers: { @@ -156,8 +174,8 @@ export class Task { if (transactionWrap.transaction['type'] === 'axfer') { const assetIndex = transactionWrap.transaction['assetIndex']; - const ledger = getLedgerFromGenesisId(transactionWrap.transaction['genesisID']); - const conn = Settings.getBackendParams(ledger, API.Algod); + const network = getNetworkNameFromGenesisID(transactionWrap.transaction['genesisID']); + const conn = Settings.getBackendParams(network, API.Algod); const sendPath = `/v2/assets/${assetIndex}`; const fetchAssets: any = { headers: { @@ -257,7 +275,7 @@ export class Task { (walletTx.msig && typeof walletTx.msig !== 'object') ) { logging.log('Invalid Wallet Transaction Structure'); - throw RequestError.InvalidStructure; + throw RequestError.InvalidWalletTxnStructure; } else if ( // prettier-ignore walletTx.msig && ( @@ -289,8 +307,27 @@ export class Task { processedTxArray[index] = processedTx; const wrap = getValidatedTxnWrap(processedTx, processedTx['type']); transactionWraps[index] = wrap; - const genesisID = wrap.transaction.genesisID; + if ( + wrap.validityObject && + Object.values(wrap.validityObject).some( + (value) => value['status'] === ValidationStatus.Invalid + ) + ) { + // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. + const invalidKeys = []; + Object.entries(wrap.validityObject).forEach(([key, value]) => { + if (value['status'] === ValidationStatus.Invalid) { + invalidKeys.push(`${key}: ${value['info']}`); + } + }); + throw RequestError.InvalidFields(invalidKeys); + } + + const network = getNetworkFromMixedGenesis( + wrap.transaction.genesisID, + wrap.transaction.genesisHash + ); const signers: Array = walletTransactions[index].signers; const signedTxn: string = walletTransactions[index].stxn; const msigData: MultisigMetadata = walletTransactions[index].msig; @@ -304,7 +341,13 @@ export class Task { if (!algosdk.isValidAddress(authAddr)) { throw RequestError.InvalidAuthAddress(authAddr); } - Task.checkAccountIsImportedAndAuthorized(genesisID, authAddr, request.originTabID); + Task.checkAccountIsImportedAndAuthorized( + network.name, + network.genesisID, + network.genesisHash, + authAddr, + request.origin + ); } // If we have msigData, we validate the addresses and fetch the resulting msig address @@ -369,7 +412,13 @@ export class Task { // We make sure we have the available accounts for signing signers.forEach((address) => { try { - Task.checkAccountIsImportedAndAuthorized(genesisID, address, request.originTabID); + Task.checkAccountIsImportedAndAuthorized( + network.name, + network.genesisID, + network.genesisHash, + address, + request.origin + ); } catch (e) { throw RequestError.CantMatchMsigSigners(e.message); } @@ -398,7 +447,13 @@ export class Task { } else { // There's no signers field, we validate the sender if there's no msig if (!msigData) { - Task.checkAccountIsImportedAndAuthorized(genesisID, wrap.transaction.from, request.originTabID); + Task.checkAccountIsImportedAndAuthorized( + network.name, + network.genesisID, + network.genesisHash, + wrap.transaction.from, + request.origin + ); } } @@ -429,52 +484,21 @@ export class Task { !transactionWraps.length || transactionWraps.some((w) => w === undefined || w === null) ) { - // We don't have transaction wraps or we have an building error, reject the transaction. - let data = ''; + // We don't have transaction wraps or we have a validation error, reject the transaction. + const data = []; let code = 4300; // We format the validation errors for better readability and clarity validationErrors.forEach((error, index) => { - // Concatenate the errors in a single formatted message - data = data + `Validation failed for transaction ${index} due to: ${error.message}. `; + // Store the error message in the corresponding array position + data[index] = error.message; // Take the lowest error code as they're _more_ generic the higher they go code = error.code < code ? error.code : code; }); - throw RequestError.SigningError(code, data.trim()); - } else if ( - transactionWraps.some( - (tx) => - tx.validityObject && - Object.values(tx.validityObject).some( - (value) => value['status'] === ValidationStatus.Invalid - ) - ) - ) { - // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. - const invalidKeys = {}; - transactionWraps.forEach((tx, index) => { - invalidKeys[index] = []; - Object.entries(tx.validityObject).forEach(([key, value]) => { - if (value['status'] === ValidationStatus.Invalid) { - invalidKeys[index].push(`${key}: ${value['info']}`); - } - }); - if (!invalidKeys[index].length) delete invalidKeys[index]; - }); - - let data = ''; - Object.keys(invalidKeys).forEach((index) => { - data = - data + - `Validation failed for transaction #${index} because of invalid properties [${invalidKeys[ - index - ].join(', ')}]. `; - }); - throw RequestError.InvalidFields(data); + throw RequestError.SigningValidationError(code, data); } else { - /** + /** * Group validations */ const providedGroupId: string = transactionWraps[0].transaction.group; @@ -482,10 +506,12 @@ export class Task { if (transactionWraps.length > 1) { if ( !transactionWraps.every( - (wrap) => transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID + (wrap) => + transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID || + transactionWraps[0].transaction.genesisHash === wrap.transaction.genesisHash ) ) { - throw RequestError.NoDifferentLedgers; + throw RequestError.NoDifferentNetworks; } if (!providedGroupId || !transactionWraps.every((wrap) => wrap.transaction.group)) { @@ -512,7 +538,9 @@ export class Task { } // If we only receive reference transactions, we reject - if (!transactionWraps.some((wrap) => !wrap.signers || (wrap.signers && wrap.signers.length))) { + if ( + !transactionWraps.some((wrap) => !wrap.signers || (wrap.signers && wrap.signers.length)) + ) { throw RequestError.NoTxsToSign; } @@ -543,21 +571,19 @@ export class Task { ); } } catch (e) { - let data = ''; + const errorResponse = { ...e }; - if (groupsToSign.length === 1) { - data += e.data; - } else { - data += `On group ${currentGroup}: [${e.data}]. `; + if (groupsToSign.length > 1) { + errorResponse.message = `On group ${currentGroup}: ${e.message}`; } - logging.log(data); - request.error = e; - request.error.data = data; + logging.log(errorResponse); + request.error = errorResponse; // Clean class saved request delete Task.requests[request.originTabID]; MessageApi.send(request); + return; } }; @@ -600,7 +626,7 @@ export class Task { }, // Enable function as defined in ARC-006 [JsonRpcMethod.EnableAuthorization]: (d: any) => { - const { accounts } = d.body.params; + const { accounts: requestedAccounts } = d.body.params; let { genesisID, genesisHash } = d.body.params; logging.log('Enable params:', LogLevel.Debug); logging.log(d.body.params, LogLevel.Debug); @@ -608,172 +634,120 @@ export class Task { // Delete any previous request made from the Tab that it's trying to connect. delete Task.requests[d.originTabID]; - // Get an internal session - if unavailable then we will connect and deny - const session = InternalMethods.getHelperSession(); - logging.log('Session:', LogLevel.Debug); - logging.log(session, LogLevel.Debug); - // Set a flag for a specified network - let networkSpecifiedType = 0; + let specifiedNetworkType: NetworkSelectionType = NetworkSelectionType.NoneProvided; if (genesisID && genesisHash) { - networkSpecifiedType = 1; - } else if (genesisID || genesisHash) { - networkSpecifiedType = 2; + specifiedNetworkType = NetworkSelectionType.BothProvided; + } else if (genesisID) { + specifiedNetworkType = NetworkSelectionType.OnlyIDProvided; } - d.body.params.networkSpecifiedType = networkSpecifiedType; + d.body.params.specifiedNetworkType = specifiedNetworkType; - // If session is missing then we should throw an error, but still popup the login screen - if (session.availableLedgers.length === 0) { - // No ledgers are available. The user is logged out so just prompt them to login. - extensionBrowser.windows.create({ - url: extensionBrowser.runtime.getURL('index.html#/close'), - ...popupProperties, - }); - - // Let the dApp know there was an issue with a generic unauthorized - d.error = RequestError.SiteNotAuthorizedByUser; + // Get ledger/hash/id from the genesisID and/or hash + const network: NetworkTemplate = getNetworkFromMixedGenesis(genesisID, genesisHash); + logging.log('Network from genesis info', LogLevel.Debug); + logging.log(network, LogLevel.Debug); - // Set the timeout higher to allow for the previous popup before responding + // Validate that the genesis id and hash if provided match the resulting one + // This is because a dapp may request an id and hash from different ledgers + if ( + (genesisID && genesisID !== network.genesisID) || + (genesisHash && genesisHash !== network.genesisHash) + ) { + d.error = RequestError.UnsupportedNetwork; setTimeout(() => { MessageApi.send(d); - }, 2000); - } else { - // Get ledger/hash/id from the genesisID and/or hash - const ledgerTemplate = getLedgerFromMixedGenesis(genesisID, genesisHash); - - // Validate that the genesis id and hash if provided match the resulting one - // This is because a dapp may request an id and hash from different ledgers - if ((genesisID && genesisID !== ledgerTemplate.genesisId) - || (genesisHash && ledgerTemplate.genesisHash && genesisHash !== ledgerTemplate.genesisHash)) { - d.error = RequestError.UnsupportedLedger; - setTimeout(() => { - MessageApi.send(d); - }, 500); - return; - } + }, 500); + return; + } - // We've validated the ledger information - // So we can set the ledger, genesisID, and genesisHash - const ledger = ledgerTemplate.name; - genesisID = ledgerTemplate.genesisId; - genesisHash = ledgerTemplate.genesisHash; - // Then reflect those changes for the page - d.body.params.ledger = ledger; // For legacy name use - d.body.params.genesisID = genesisID; - d.body.params.genesisHash = genesisHash; - - // If we already have the ledger authorized for this origin then check the shared accounts - if (Task.isAuthorized(d.origin)) { - // First check that we actually still have the addresses requested - try { - accounts.forEach(account => { - InternalMethods.checkAccountIsImported(genesisID, account); - }); + // We've validated the ledger information + // So we can set the ledger, genesisID, and genesisHash + const ledger = network.name; + genesisID = network.genesisID; + genesisHash = network.genesisHash; + + // Then reflect those changes for the page + d.body.params.ledger = ledger; + d.body.params.genesisID = genesisID; + d.body.params.genesisHash = genesisHash; + + // If we already have the ledger authorized for this origin then check the shared accounts + if (Task.isAuthorized(d.origin)) { + // First check that we actually still have the addresses requested + try { + requestedAccounts.forEach((account) => { + InternalMethods.checkAccountIsImported(genesisID, account); + }); - // If the ledger and ALL accounts are available then respond with the cached data - if (Task.isPreAuthorized(d.origin, genesisID, accounts)) { - // We have the accounts and may include additional, but just make sure the order is maintained - const sharedAccounts = []; - accounts.forEach(account => { + // If the ledger and ALL accounts are available then respond with the cached data + if (Task.isPreAuthorized(d.origin, genesisID, requestedAccounts)) { + // We have the accounts and may include additional, but just make sure the order is maintained + const sharedAccounts = []; + requestedAccounts.forEach((account) => { + // Make sure we don't include accounts that have been deleted + InternalMethods.checkAccountIsImported(genesisID, account); + sharedAccounts.push(account); + }); + Task.authorized_pool_details[d.origin]['accounts'].forEach((account) => { + if (!sharedAccounts.includes(account)) { // Make sure we don't include accounts that have been deleted InternalMethods.checkAccountIsImported(genesisID, account); sharedAccounts.push(account); - }); - Task.authorized_pool_details[d.origin]['accounts'].forEach(account => { - if (!(sharedAccounts.includes(account))) { - // Make sure we don't include accounts that have been deleted - InternalMethods.checkAccountIsImported(genesisID, account); - sharedAccounts.push(account); - } - }); + } + }); - // Now we can set the response, but don't need to update the cache - d.response = { - 'genesisID': genesisID, - 'genesisHash': genesisHash, - accounts: sharedAccounts - }; - MessageApi.send(d); - return; - } + // Now we can set the response, but don't need to update the cache + d.response = { + genesisID: genesisID, + genesisHash: genesisHash, + accounts: sharedAccounts, + }; + MessageApi.send(d); + return; } - catch { - // Failure means we won't auto authorize, but we can sink the error as we are re-prompting - } + } catch { + // Failure means we won't auto authorize, but we can sink the error as we are re-prompting } + } - // We haven't immediately failed and don't have preAuthorization so we need to prompt accounts. - const promptedAccounts = []; - - // Add any requested accounts so they can be in the proper order to start - if (accounts) { - for (let i = 0; i < accounts.length; i++) { - // We initially push accounts as missing and don't have them selected - // If we also own the address it will be modified to not missing and selected by default - const requestedAddress = accounts[i]; - promptedAccounts.push({ - address: requestedAddress, - missing: true, - requested: true, - }); - } - } + // We haven't immediately failed and don't have preAuthorization so we need to prompt accounts. + const promptedAccounts = []; - // Get wallet accounts for the specified ledger - const walletAccounts = session.wallet[ledger]; - - // If we need a requested a ledger but don't have it, respond with an error - if (walletAccounts === undefined) { - d.error = RequestError.UnsupportedLedger; + // Add any requested accounts so they can be in the proper order to start + if (requestedAccounts) { + for (let i = 0; i < requestedAccounts.length; i++) { + // We initially push accounts as missing and don't have them selected + // If we also own the address it will be modified to not missing and selected by default + const requestedAddress = requestedAccounts[i]; + promptedAccounts.push({ + address: requestedAddress, + missing: true, + requested: true, + }); + } + } - setTimeout(() => { - MessageApi.send(d); - }, 500); - return; - } - - // Add all the walletAccounts we have for the ledger - for (let i = 0; i < walletAccounts.length; i++) { - const walletAccount = walletAccounts[i].address; - const accountIndex = promptedAccounts.findIndex(e => e.address === walletAccount); - - if (accountIndex > -1) { - // If we have the account then mark it as valid - promptedAccounts[accountIndex]['missing'] = false; - promptedAccounts[accountIndex]['selected'] = true; - } - else { - // If we are missing the address then this is an account that the dApp did not request - // but we can push the value an the additional choices from the user before returning - promptedAccounts.push({ - address: walletAccount, - requested: false, - selected: false - }); - } - } - - // Add the prompted accounts to the params that will go to the page - d.body.params['promptedAccounts'] = promptedAccounts; + // Add the prompted accounts to the params that will go to the page + d.body.params['promptedAccounts'] = promptedAccounts; - extensionBrowser.windows.create( - { - url: extensionBrowser.runtime.getURL('index.html#/enable'), - ...popupProperties, - }, - function (w: any) { - if (w) { - Task.requests[d.originTabID] = { - window_id: w.id, - message: d, - }; - setTimeout(function () { - extensionBrowser.runtime.sendMessage(d); - }, 500); - } + extensionBrowser.windows.create( + { + url: extensionBrowser.runtime.getURL('index.html#/enable'), + ...popupProperties, + }, + function (w: any) { + if (w) { + Task.requests[d.originTabID] = { + window_id: w.id, + message: d, + }; + setTimeout(function () { + extensionBrowser.runtime.sendMessage(d); + }, 500); } - ); - } + } + ); }, // sign-wallet-transaction [JsonRpcMethod.SignWalletTransaction]: async ( @@ -939,8 +913,8 @@ export class Task { } else { const genesisID = Task.authorized_pool_details[d.origin]['genesisID']; const genesisHash = Task.authorized_pool_details[d.origin]['genesisHash']; - const ledger = getLedgerFromMixedGenesis(genesisID, genesisHash).name; - const conn = Settings.getBackendParams(ledger, API.Algod); + const network = getNetworkFromMixedGenesis(genesisID, genesisHash).name; + const conn = Settings.getBackendParams(network, API.Algod); const sendPath = '/v2/transactions'; const fetchParams: any = { headers: { @@ -959,14 +933,16 @@ export class Task { }); Promise.allSettled(fetchPromises).then((results) => { - const algod = InternalMethods.getAlgod(ledger); + const algod = InternalMethods.getAlgod(network); const confirmationPromises = []; results.forEach((res, index) => { if (res.status === 'fulfilled') { const txID = res.value.txId; confirmationPromises[index] = algosdk.waitForConfirmation(algod, txID, 2); } else { - fetchErrors[index] = res.reason.message; + logging.log('Failed post response:', LogLevel.Debug); + logging.log(res, LogLevel.Debug); + fetchErrors[index] = res.reason; fetchResponses[index] = null; } }); @@ -990,9 +966,14 @@ export class Task { // Check if there's any non-null errors if (fetchErrors.filter(Boolean).length) { const successTxnIDs = fetchResponses; - const reasons = new Array(groupsToSend.length); - fetchErrors.forEach((e, i) => (reasons[i] = e ? e.message : null)); - d.error = RequestError.PartiallySuccessfulPost(successTxnIDs, reasons); + if (groupsToSend.length > 1) { + const reasons = new Array(groupsToSend.length); + fetchErrors.forEach((e, i) => (reasons[i] = e ? e.message : null)); + d.error = RequestError.PartiallySuccessfulPost(successTxnIDs, reasons); + } else { + const reason = fetchErrors[0].message; + d.error = RequestError.FailedPost(reason); + } resolve(d); } else { d.response = { txnIDs: fetchResponses }; @@ -1062,16 +1043,15 @@ export class Task { }, // Accounts [JsonRpcMethod.Accounts]: (d: any, resolve: Function, reject: Function) => { - const session = InternalMethods.getHelperSession(); + const session = InternalMethods.getSessionObject(); // 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); + if (!d.body.params?.ledger) { + const baseNetworks = Object.keys(Network); const injectedNetworks = Settings.getCleansedInjectedNetworks(); - d.error = { - message: `Ledger not provided. Please use a base ledger: [${baseNetworks}] or an available custom one ${JSON.stringify( - injectedNetworks - )}.`, - }; + d.error = RequestError.NoLedgerProvided( + baseNetworks.toString(), + JSON.stringify(injectedNetworks) + ); reject(d); return; } @@ -1097,7 +1077,8 @@ export class Task { private: { // authorization-allow [JsonRpcMethod.AuthorizationAllow]: (d) => { - const { responseOriginTabID, isEnable, accounts, genesisID, genesisHash } = d.body.params; + const { responseOriginTabID, isEnable, accounts, genesisID, genesisHash, ledger: network } = + d.body.params; const auth = Task.requests[responseOriginTabID]; const message = auth.message; @@ -1114,26 +1095,29 @@ export class Task { const rejectedAccounts = []; const sharedAccounts = []; for (const i in accounts) { - if ((accounts[i]['requested'] && !accounts[i]['selected']) || accounts[i]['missing']) { - rejectedAccounts.push(accounts[i]['address']); - } - else if(accounts[i]['selected']) { + if ( + (accounts[i]['requested'] && !accounts[i]['selected']) || + accounts[i]['missing'] + ) { + rejectedAccounts.push(accounts[i]['address']); + } else if (accounts[i]['selected']) { sharedAccounts.push(accounts[i]['address']); } } if (rejectedAccounts.length > 0) { - message.error = RequestError.EnableRejected({ 'accounts': rejectedAccounts }); - } - else { + message.error = RequestError.EnableRejected({ accounts: rejectedAccounts }); + } else { message.response = { - 'genesisID': genesisID, - 'genesisHash': genesisHash, - accounts: sharedAccounts - } + genesisID: genesisID, + genesisHash: genesisHash, + accounts: sharedAccounts, + }; + + const poolDetails = { ...message.response, ledger: network }; - // Add to the authorized pool details. + // Add to the authorized pool details. // This will be checked to restrict access for enable function users - Task.authorized_pool_details[`${message.origin}`] = message.response; + Task.authorized_pool_details[`${message.origin}`] = poolDetails; } } MessageApi.send(message); @@ -1171,10 +1155,10 @@ export class Task { let holdResponse = false; const signedTxs = []; - const signErrors = []; + const signErrors: Array = []; try { - const ledger = getLedgerFromGenesisId(transactionObjs[0].genesisID); + const network = getNetworkNameFromGenesisID(transactionObjs[0].genesisID); const neededAccounts: Array = []; walletTransactions.forEach((w, i) => { const msig = w.msig; @@ -1218,9 +1202,9 @@ export class Task { extensionBrowser.windows.remove(auth.window_id); const recoveredAccounts = []; - if (unlockedValue[ledger] === undefined) { + if (unlockedValue[network] === undefined) { delete Task.requests[responseOriginTabID]; - message.error = RequestError.UnsupportedLedger; + message.error = RequestError.UnsupportedNetwork; MessageApi.send(message); return; } @@ -1230,17 +1214,17 @@ export class Task { // Find addresses to send algos from // We store them using the public address as dictionary key let addressWithNoMnemonic = ''; - for (let i = unlockedValue[ledger].length - 1; i >= 0; i--) { - const account = unlockedValue[ledger][i]; + for (let i = unlockedValue[network].length - 1; i >= 0; i--) { + const account = unlockedValue[network][i]; if (neededAccounts.includes(account.address)) { if (!account.isHardware) { // Check for an address that we were expected but unable to sign with - if (!unlockedValue[ledger][i].mnemonic) { + if (!unlockedValue[network][i].mnemonic) { addressWithNoMnemonic = account.address; break; } recoveredAccounts[account.address] = algosdk.mnemonicToSecretKey( - unlockedValue[ledger][i].mnemonic + unlockedValue[network][i].mnemonic ); } else { hardwareAccounts.push(account.address); @@ -1255,7 +1239,7 @@ export class Task { return; } - if (hardwareAccounts.length){ + if (hardwareAccounts.length) { message.body.params.ledgerIndexes = []; message.body.params.currentLedgerTransaction = 0; } @@ -1354,19 +1338,18 @@ export class Task { // We check if there were errors signing this group if (signErrors.length) { - let data = ''; - if (transactionObjs.length > 1) { - signErrors.forEach((error, index) => { - data += `On transaction ${index}, the error was: ${error}. `; - }); - } else { - data += signErrors[0]; - } + const data = []; + signErrors.forEach((error, index) => { + data[index] = error; + }); + + const responseError = RequestError.ApplySignatureError(data); if (!singleGroup) { - data = `On group ${currentGroup}: [${data.trim()}]. `; + responseError.message = `On group ${currentGroup}: ${responseError.message}`; } - message.error = RequestError.SigningError(4000, data); - logging.log(data); + + message.error = responseError; + logging.log(responseError); } else { signedGroups[currentGroup] = signedTxs; } @@ -1384,21 +1367,7 @@ export class Task { if (message.body.params.currentGroup + 1 < groupsToSign.length) { // More groups to sign, continue prompting user message.body.params.currentGroup = currentGroup + 1; - try { - await Task.signIndividualGroup(message); - } catch (e) { - let errorMessage = 'There was a problem validating the transaction(s). '; - - if (singleGroup) { - errorMessage += e.message; - } else { - errorMessage += `On group ${currentGroup}: [${e.message}].`; - } - logging.log(errorMessage); - const error = new Error(errorMessage); - sendResponse(error); - return; - } + await Task.signIndividualGroup(message); } else { // No more groups to sign, build final user-facing response if (opts && opts[OptsKeys.sendTxns]) { @@ -1408,7 +1377,7 @@ export class Task { } // Clean class saved request delete Task.requests[responseOriginTabID]; - + // Hardware signing will defer the response if (!holdResponse) { if (opts && opts[OptsKeys.sendTxns]) { @@ -1418,17 +1387,14 @@ export class Task { return; } } else { - InternalMethods[JsonRpcMethod.LedgerSignTransaction]( - message, - (response) => { - // We only have to worry about possible errors here - if ('error' in response) { - // Cancel the hold response since errors needs to be returned - holdResponse = false; - message.error = response.error; - } + InternalMethods[JsonRpcMethod.LedgerSignTransaction](message, (response) => { + // We only have to worry about possible errors here + if ('error' in response) { + // Cancel the hold response since errors needs to be returned + holdResponse = false; + message.error = response.error; } - ); + }); } } }); @@ -1502,62 +1468,57 @@ export class Task { [JsonRpcMethod.SignSendTransaction]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.SignSendTransaction](request, sendResponse); }, - [JsonRpcMethod.ChangeLedger]: (request: any, sendResponse: Function) => { - return InternalMethods[JsonRpcMethod.ChangeLedger](request, sendResponse); + [JsonRpcMethod.ChangeNetwork]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.ChangeNetwork](request, sendResponse); }, [JsonRpcMethod.SaveNetwork]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.SaveNetwork](request, sendResponse); }, [JsonRpcMethod.CheckNetwork]: (request: any, sendResponse: Function) => { - InternalMethods[JsonRpcMethod.CheckNetwork](request, async (networks) => { + InternalMethods[JsonRpcMethod.CheckNetwork](request, async (connection: Connection) => { const algodClient = new algosdk.Algodv2( - networks.algod.apiKey, - networks.algod.url, - networks.algod.port + connection.algod.apiKey, + connection.algod.url, + connection.algod.port ); const indexerClient = new algosdk.Indexer( - networks.indexer.apiKey, - networks.indexer.url, - networks.indexer.port + connection.indexer.apiKey, + connection.indexer.url, + connection.indexer.port ); - const responseAlgod = {}; - const responseIndexer = {}; - - (async () => { - await algodClient - .status() - .do() - .then((response) => { - responseAlgod['message'] = response['message'] || response; - }) - .catch((error) => { - responseAlgod['error'] = error.message || error; - }); - })().then(() => { - (async () => { - await indexerClient - .searchForTransactions() - .limit(1) - .do() - .then((response) => { - responseAlgod['message'] = response['message'] || response; - }) - .catch((error) => { - responseAlgod['error'] = error.message || error; - }); - })().then(() => { - sendResponse({ algod: responseAlgod, indexer: responseIndexer }); + const defaultError = { error: 'Unable to connect'}; + const statusReponse = {}; + + const algodPromise = algodClient + .getTransactionParams() + .do() + .then((response) => { + statusReponse['algod'] = response ? response : defaultError; + }) + .catch((error) => { + statusReponse['algod'] = { error: error.message || error }; }); - }); + + const indexerPromise = indexerClient + .makeHealthCheck() + .do() + .then((response) => { + statusReponse['indexer'] = response ? response : defaultError; + }) + .catch((error) => { + statusReponse['indexer'] = { error: error.message || error }; + }); + await Promise.all([algodPromise, indexerPromise]); + sendResponse(statusReponse); }); return true; }, [JsonRpcMethod.DeleteNetwork]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.DeleteNetwork](request, sendResponse); }, - [JsonRpcMethod.GetLedgers]: (request: any, sendResponse: Function) => { - return InternalMethods[JsonRpcMethod.GetLedgers](request, sendResponse); + [JsonRpcMethod.GetNetworks]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.GetNetworks](request, sendResponse); }, [JsonRpcMethod.LedgerLinkAddress]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.LedgerLinkAddress](request, sendResponse); @@ -1566,56 +1527,66 @@ export class Task { return InternalMethods[JsonRpcMethod.LedgerGetSessionTxn](request, sendResponse); }, [JsonRpcMethod.LedgerSendTxnResponse]: (request: any, sendResponse: Function) => { - InternalMethods[JsonRpcMethod.LedgerSendTxnResponse](request, function (internalResponse) { - logging.log('Internal response from LedgerSendTxnResponse:', LogLevel.Debug); - logging.log(internalResponse, LogLevel.Debug); - - // Message indicates that this response will go to the DApp - if ('message' in internalResponse) { - const message = internalResponse.message; - const { ledgerIndexes, currentLedgerTransaction, currentGroup, signedGroups, opts } = message.body.params; - const signedTxnIndex = ledgerIndexes[currentLedgerTransaction]; - const b64Response = message.response; - - // We add the signed txn to the final response - signedGroups[currentGroup][signedTxnIndex] = - opts && opts[OptsKeys.ARC01Return] - ? b64Response - : { - blob: b64Response, - }; + InternalMethods[JsonRpcMethod.LedgerSendTxnResponse]( + request, + function (internalResponse) { + logging.log('Internal response from LedgerSendTxnResponse:', LogLevel.Debug); + logging.log(internalResponse, LogLevel.Debug); + + // Message indicates that this response will go to the DApp + if ('message' in internalResponse) { + const message = internalResponse.message; + const { + ledgerIndexes, + currentLedgerTransaction, + currentGroup, + signedGroups, + opts, + } = message.body.params; + const signedTxnIndex = ledgerIndexes[currentLedgerTransaction]; + const b64Response = message.response; + + // We add the signed txn to the final response + signedGroups[currentGroup][signedTxnIndex] = + opts && opts[OptsKeys.ARC01Return] + ? b64Response + : { + blob: b64Response, + }; - // We determine if there's more txns to sign before sending a response back - if (currentLedgerTransaction + 1 < ledgerIndexes.length) { - message.body.params.currentLedgerTransaction++; - sendResponse(internalResponse); - } else { - // If there's no more ledger transactions to sign, we prepare to return a response - // First we clear the cached ledger info - message.body.params.ledgerIndexes = undefined; - message.body.params.currentLedgerTransaction = undefined; - - // Then we send an informative response to the UI - const displayResponse = signedGroups[currentGroup].filter((_, index) => ledgerIndexes.includes(index)); - sendResponse(displayResponse); - - // Lastly we prepare & send the dApp response - if (opts && opts[OptsKeys.sendTxns]) { - message.body.params.stxns = signedGroups; - Task.methods().public[JsonRpcMethod.PostTransactions](message, MessageApi.send); + // We determine if there's more txns to sign before sending a response back + if (currentLedgerTransaction + 1 < ledgerIndexes.length) { + message.body.params.currentLedgerTransaction++; + sendResponse(internalResponse); } else { - // Send the response back to the originating page - message.response = signedGroups.length === 1 ? signedGroups[0] : signedGroups; - MessageApi.send(message); - return; + // If there's no more ledger transactions to sign, we prepare to return a response + // First we clear the cached ledger info + message.body.params.ledgerIndexes = undefined; + message.body.params.currentLedgerTransaction = undefined; + + // Then we send an informative response to the UI + const displayResponse = signedGroups[currentGroup].filter((_, index) => + ledgerIndexes.includes(index) + ); + sendResponse(displayResponse); + + // Lastly we prepare & send the dApp response + if (opts && opts[OptsKeys.sendTxns]) { + message.body.params.stxns = signedGroups; + Task.methods().public[JsonRpcMethod.PostTransactions](message, MessageApi.send); + } else { + // Send the response back to the originating page + message.response = signedGroups.length === 1 ? signedGroups[0] : signedGroups; + MessageApi.send(message); + return; + } } + } else { + // Send response to the calling function + sendResponse(internalResponse); } - - } else { - // Send response to the calling function - sendResponse(internalResponse); } - }); + ); return true; }, [JsonRpcMethod.LedgerSaveAccount]: (request: any, sendResponse: Function) => { @@ -1648,7 +1619,7 @@ export class Task { return InternalMethods[JsonRpcMethod.GetGovernanceAddresses](request, sendResponse); }, [JsonRpcMethod.GetEnableAccounts]: (request: any, sendResponse: Function) => { - const { promptedAccounts, ledger } = request.body.params; + const { promptedAccounts, ledger: network } = request.body.params; // Setup new prompted accounts which will be the return values const newPromptedAccounts = []; @@ -1665,41 +1636,43 @@ export class Task { }); } } - } + } // Get an internal session and get wallet accounts for the new chosen ledger - const session = InternalMethods.getHelperSession(); - const walletAccounts = session.wallet[ledger]; - + const session = InternalMethods.getSessionObject(); + const walletAccounts = session.wallet && session.wallet[network]; + // We only need to add accounts if we actually have them if (walletAccounts) { - // Add all the walletAccounts we have for the ledger + // Add all the walletAccounts we have for the ledger for (let i = 0; i < walletAccounts.length; i++) { const walletAccount = walletAccounts[i].address; - const accountIndex = newPromptedAccounts.findIndex(e => e.address === walletAccount); - + const accountIndex = newPromptedAccounts.findIndex( + (e) => e.address === walletAccount + ); + if (accountIndex > -1) { - // If we have the account then mark it as valid + // If we have the account then mark it as valid newPromptedAccounts[accountIndex]['missing'] = false; newPromptedAccounts[accountIndex]['selected'] = true; - } - else { + } else { // If we are missing the address then this is an account that the dApp did not request // but we can push the value an the additional choices from the user before returning newPromptedAccounts.push({ address: walletAccount, requested: false, - selected: false + selected: false, }); } - } + } } - // Replace the prompted accounts on params that will go back to the page - request.body.params['promptedAccounts'] = newPromptedAccounts; - - // Respond with the new params - sendResponse(request.body.params); + // Replace the prompted accounts on params response + const response = { + ...request.body.params, + promptedAccounts: newPromptedAccounts, + }; + sendResponse(response); }, }, }; diff --git a/packages/extension/src/background/messaging/types.ts b/packages/extension/src/background/messaging/types.ts index 07826ffe..8bf03ba1 100644 --- a/packages/extension/src/background/messaging/types.ts +++ b/packages/extension/src/background/messaging/types.ts @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/ban-types */ - -import { LedgerTemplate } from '@algosigner/common/types/ledgers'; +import { NetworkTemplate } from '@algosigner/common/types/network'; // Key and value must match in this enum so we // can compare its existance with i.e. "TestNet" in SupportedLedger @@ -18,31 +16,32 @@ export enum API { export interface Cache { /* - assets: { - ledger: [ - assetId: { - ... - }, + assets: { + network: [ + assetId: { ... - ], + }, ... - }, - accounts: { - ledger: [ - address: { - ... - }, + ], + ... + }, + accounts: { + network: [ + address: { ... - ], - ... - }, - availableLedgers: [ - { + }, ... - } ], - */ + ... + }, + availableLedgers: [ + { + ... + } + ], + */ assets: object; accounts: object; - availableLedgers: Array; + // Legacy name, renaming could break stored caches + availableLedgers: Array; } diff --git a/packages/extension/src/background/transaction/acfgCreateTransaction.ts b/packages/extension/src/background/transaction/acfgCreateTransaction.ts index a9b99560..b4fa3184 100644 --- a/packages/extension/src/background/transaction/acfgCreateTransaction.ts +++ b/packages/extension/src/background/transaction/acfgCreateTransaction.ts @@ -6,7 +6,8 @@ import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; /// class AssetCreateTx implements IAssetCreateTx { assetTotal: BigInt = undefined; - assetDecimals?: number = null; + assetDecimals: number = 0; + assetDefaultFrozen: boolean = false; type: string = undefined; from: string = undefined; fee?: number = 0; @@ -15,7 +16,6 @@ class AssetCreateTx implements IAssetCreateTx { note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - assetDefaultFrozen?: boolean = null; assetUnitName?: string = null; assetName?: string = null; assetURL?: string = null; diff --git a/packages/extension/src/background/transaction/acfgTransaction.ts b/packages/extension/src/background/transaction/acfgTransaction.ts index 90c293cd..5c9fa2d7 100644 --- a/packages/extension/src/background/transaction/acfgTransaction.ts +++ b/packages/extension/src/background/transaction/acfgTransaction.ts @@ -14,48 +14,24 @@ class AssetConfigTx implements IAssetConfigTx { note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; + assetManager: string = undefined; + assetReserve: string = undefined; + assetFreeze: string = undefined; + assetClawback: string = undefined; group?: string = null; lease?: any = null; reKeyTo?: any = null; flatFee?: any = null; name?: string = null; tag?: string = null; - - // Modifications must include one of these - assetTotal?: BigInt = null; - assetDecimals?: number = null; - assetDefaultFrozen?: boolean = null; - assetUnitName?: string = null; - assetName?: string = null; - assetURL?: string = null; - assetMetadataHash?: any = null; - assetManager?: string = null; - assetReserve?: string = null; - assetFreeze?: string = null; - assetClawback?: string = null; } -// A set of params in which at least one must have a value -const requiredAcfgParams: Array = [ - 'assetTotal', - 'assetDecimals', - 'assetDefaultFrozen', - 'assetUnitName', - 'assetName', - 'assetURL', - 'assetMetadataHash', - 'assetManager', - 'assetReserve', - 'assetFreeze', - 'assetClawback', -]; - /// // Mapping, validation and error checking for asset config transactions prior to sign. /// export class AssetConfigTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Modify Asset'; constructor(params: IAssetConfigTx) { - super(params, AssetConfigTx, requiredAcfgParams); + super(params, AssetConfigTx); } } diff --git a/packages/extension/src/background/transaction/actions.test.ts b/packages/extension/src/background/transaction/actions.test.ts index 73c5e9d4..fb796e36 100644 --- a/packages/extension/src/background/transaction/actions.test.ts +++ b/packages/extension/src/background/transaction/actions.test.ts @@ -4,8 +4,9 @@ import { AssetConfigTransaction } from './acfgTransaction'; import { AssetTransferTransaction } from './axferTransaction'; import { AssetCloseTransaction } from './axferCloseTransaction'; import { AssetFreezeTransaction } from './afrzTransaction'; -import { KeyregTransaction } from './keyregTransaction'; import { ApplicationTransaction } from './applTransaction'; +import { OnlineKeyregTransaction } from './keyregOnlineTransaction'; +import { OfflineKeyregTransaction } from './keyregOfflineTransaction'; test('Validate build of pay transaction', () => { const preTransaction = { @@ -57,7 +58,7 @@ test('Validate build of appl transaction', () => { expect(result instanceof ApplicationTransaction).toBe(true); }); -test('Validate build of keygreg transaction', () => { +test('Validate build of keyreg transaction', () => { const preTransaction = { type: 'keyreg', fee: 1000, @@ -67,6 +68,25 @@ test('Validate build of keygreg transaction', () => { stateProofKey: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQNM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', voteFirst: 1, voteLast: 1001, + voteKeyDilution: 1001, + firstRound: 1, + lastRound: 1001, + genesisID: 'testnet-v1.0', + genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', + note: new Uint8Array(0), + }; + + const result = getValidatedTxnWrap(preTransaction, 'keyreg'); + expect(result instanceof BaseValidatedTxnWrap).toBe(true); + expect(result instanceof OnlineKeyregTransaction).toBe(true); +}); + +test('Validate build of offline keyreg transaction', () => { + const preTransaction = { + type: 'keyreg', + fee: 1000, + from: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + stateProofKey: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQNM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', firstRound: 1, lastRound: 1001, genesisID: 'testnet-v1.0', @@ -77,7 +97,7 @@ test('Validate build of keygreg transaction', () => { const result = getValidatedTxnWrap(preTransaction, 'keyreg'); expect(result instanceof BaseValidatedTxnWrap).toBe(true); - expect(result instanceof KeyregTransaction).toBe(true); + expect(result instanceof OfflineKeyregTransaction).toBe(true); }); test('Validate build of acfg transaction', () => { @@ -90,6 +110,10 @@ test('Validate build of acfg transaction', () => { lastRound: 1001, genesisID: 'testnet-v1.0', genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', + assetManager: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + assetReserve: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + assetFreeze: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + assetClawback: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', note: new Uint8Array(0), }; @@ -172,20 +196,63 @@ test('Validate pay transaction required fields', () => { type: 'pay', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'pay'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('to'); - expect(data).toContain('from'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('to'); + expect(message).toContain('from'); +}); + +test('Validate keyreg transaction required fields', () => { + const preTransaction = { + type: 'keyreg', + }; + + let message: string = undefined; + try { + getValidatedTxnWrap(preTransaction, 'keyreg'); + } catch (e) { + message = e.message; + } + expect(message).toContain('Validation failed'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('voteKey'); + expect(message).toContain('selectionKey'); + expect(message).toContain('voteFirst'); + expect(message).toContain('voteLast'); + expect(message).toContain('voteKeyDilution'); +}); + +test('Validate offline keyreg transaction required fields', () => { + const preTransaction = { + type: 'keyreg', + nonParticipation: true, + }; + + let message: string = undefined; + try { + getValidatedTxnWrap(preTransaction, 'keyreg'); + } catch (e) { + message = e.message; + } + expect(message).toContain('Validation failed'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('nonParticipation'); }); test('Validate clawback transaction required fields', () => { @@ -193,21 +260,19 @@ test('Validate clawback transaction required fields', () => { type: 'axfer', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'axfer'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('from'); - expect(data).toContain('assetIndex'); - expect(data).toContain('assetRevocationTarget'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('assetIndex'); + expect(message).toContain('assetRevocationTarget'); }); test('Validate accept transaction required fields', () => { @@ -215,20 +280,18 @@ test('Validate accept transaction required fields', () => { type: 'axfer', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'axfer'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('from'); - expect(data).toContain('assetIndex'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('assetIndex'); }); test('Validate create transaction required fields', () => { @@ -236,20 +299,19 @@ test('Validate create transaction required fields', () => { type: 'acfg', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'acfg'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('from'); - expect(data).toContain('assetTotal'); + expect(message).toContain('AssetCreateTx'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('assetTotal'); }); test('Validate destroy transaction required fields', () => { @@ -257,20 +319,19 @@ test('Validate destroy transaction required fields', () => { type: 'acfg', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'acfg'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('from'); - expect(data).toContain('assetIndex'); + expect(message).toContain('AssetDestroyTx'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('assetIndex'); }); test('Validate modify asset transaction required fields', () => { @@ -278,18 +339,21 @@ test('Validate modify asset transaction required fields', () => { type: 'acfg', }; let message: string = undefined; - let data: string = undefined; try { getValidatedTxnWrap(preTransaction, 'acfg'); } catch (e) { message = e.message; - data = e.data; } expect(message).toContain('Validation failed'); - expect(data).toContain('firstRound'); - expect(data).toContain('lastRound'); - expect(data).toContain('genesisID'); - expect(data).toContain('genesisHash'); - expect(data).toContain('from'); - expect(data).toContain('assetIndex'); + expect(message).toContain('AssetConfigTx'); + expect(message).toContain('firstRound'); + expect(message).toContain('lastRound'); + expect(message).toContain('genesisID'); + expect(message).toContain('genesisHash'); + expect(message).toContain('from'); + expect(message).toContain('assetIndex'); + expect(message).toContain('assetManager'); + expect(message).toContain('assetReserve'); + expect(message).toContain('assetFreeze'); + expect(message).toContain('assetClawback'); }); diff --git a/packages/extension/src/background/transaction/actions.ts b/packages/extension/src/background/transaction/actions.ts index e02088f2..232a7ea7 100644 --- a/packages/extension/src/background/transaction/actions.ts +++ b/packages/extension/src/background/transaction/actions.ts @@ -18,13 +18,14 @@ import { AssetTransferTransaction } from './axferTransaction'; import { AssetAcceptTransaction } from './axferAcceptTransaction'; import { AssetCloseTransaction } from './axferCloseTransaction'; import { AssetClawbackTransaction } from './axferClawbackTransaction'; -import { KeyregTransaction } from './keyregTransaction'; +import { OnlineKeyregTransaction } from './keyregOnlineTransaction'; +import { OfflineKeyregTransaction } from './keyregOfflineTransaction'; import { ApplicationTransaction } from './applTransaction'; import { TransactionType } from '@algosigner/common/types/transaction'; import { RequestError } from '@algosigner/common/errors'; import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; import { Settings } from '../config'; -import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; +import { getBaseSupportedNetworks, NetworkTemplate } from '@algosigner/common/types/network'; import { removeEmptyFields } from '@algosigner/common/utils'; import algosdk from 'algosdk'; @@ -57,7 +58,7 @@ export function getValidatedTxnWrap( try { validatedTxnWrap = new AssetCreateTransaction(txn as IAssetCreateTx); } catch (e) { - e.data = [error.data, e.data].join(' '); + e.message = [error.message, e.message].join(' '); error = e; } } @@ -65,7 +66,7 @@ export function getValidatedTxnWrap( try { validatedTxnWrap = new AssetDestroyTransaction(txn as IAssetDestroyTx); } catch (e) { - e.data = [error.data, e.data].join(' '); + e.message = [error.message, e.message].join(' '); error = e; } } @@ -113,7 +114,24 @@ export function getValidatedTxnWrap( } break; case TransactionType.Keyreg: - validatedTxnWrap = new KeyregTransaction(txn as IKeyRegistrationTx); + // Validate any of the 2 variants of transactions that can occur with keyreg + // Use the second error as the passback error. + try { + validatedTxnWrap = new OnlineKeyregTransaction(txn as IKeyRegistrationTx); + } catch (e) { + error = e; + } + if (!validatedTxnWrap) { + try { + validatedTxnWrap = new OfflineKeyregTransaction(txn as IKeyRegistrationTx); + } catch (e) { + e.message = [e.message, error.message].join(' '); + error = e; + } + } + if (!validatedTxnWrap && error) { + throw error; + } break; case TransactionType.Appl: validatedTxnWrap = new ApplicationTransaction(txn as IApplTx); @@ -125,61 +143,61 @@ export function getValidatedTxnWrap( return validatedTxnWrap; } -export function getLedgerFromGenesisId(genesisId: string): string { - // Default the ledger to mainnet - const defaultLedger = 'MainNet'; +export function getNetworkNameFromGenesisID(genesisID: string): string { + // Default the network to mainnet + const defaultNetwork = '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; + // Check Genesis ID for base networks first + const defaultNetworks = getBaseSupportedNetworks(); + let network = defaultNetworks.find((n) => genesisID === n['genesisID']); + if (network !== undefined) { + return network.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; + network = Settings.getCleansedInjectedNetworks().find((n) => genesisID === n['genesisID']); + if (network !== undefined) { + return network.name; } - return defaultLedger; + return defaultNetwork; } -export function getLedgerFromMixedGenesis(genesisId: string, genesisHash?: string): LedgerTemplate { - // Check Genesis Id and Hash for base supported ledgers first - const defaultLedgers = getBaseSupportedLedgers(); - let ledger; - if (genesisId) { - ledger = defaultLedgers.find((l) => genesisId === l['genesisId']); - if (ledger !== undefined) { - // Found genesisId, make sure the hash matches - if (!genesisHash || genesisHash === ledger.genesisHash) { - return ledger; - } +export function getNetworkFromMixedGenesis(genesisID: string, genesisHash?: string): NetworkTemplate { + const defaultNetworks = getBaseSupportedNetworks(); + const injectedNetworks = Settings.getCleansedInjectedNetworks(); + const availableNetworks = [...defaultNetworks, ...injectedNetworks]; + let network; + let partialMatches; + + // First we check for matching IDs + if (genesisID) { + partialMatches = availableNetworks.filter((network) => genesisID === network['genesisID']); + // Found matching genesisID, make sure the hash matches if available + if (genesisHash) { + network = partialMatches.find((network) => genesisHash === network['genesisHash']); + } else if (partialMatches && partialMatches.length) { + // If no Hash was provided, return the first ID + network = partialMatches[0]; } - - // Injected networks may have additional validations so we check them separately - const injectedNetworks = Settings.getCleansedInjectedNetworks(); - ledger = injectedNetworks.find((network) => network['genesisId'] === genesisId); - if (ledger) { - return ledger; + if (network !== undefined) { + if (!genesisHash || genesisHash === network.genesisHash) { + return network; + } } } // We didn't match on the genesis id so check the hashes if (genesisHash) { - ledger = defaultLedgers.find((l) => genesisHash === l['genesisHash']); - if (ledger !== undefined) { - // Found genesisHash, make sure the id matches - if (!genesisId || genesisId === ledger.genesisId) { - return ledger; + network = availableNetworks.find((network) => genesisHash === network['genesisHash']); + if (network !== undefined) { + if (!genesisHash || genesisHash === network.genesisHash) { + return network; } } - - // We don't currently store the genesisHash of the custom networks } - // Default the ledger to mainnet - return defaultLedgers.find((l) => 'MainNet' === l['name']); + // Default the network to mainnet + return defaultNetworks.find((n) => 'MainNet' === n['name']); } export function calculateEstimatedFee(transactionWrap: BaseValidatedTxnWrap, params: any): void { @@ -188,14 +206,14 @@ export function calculateEstimatedFee(transactionWrap: BaseValidatedTxnWrap, par let estimatedFee = +transaction['fee']; if (transaction['flatFee']) { // If flatFee is enabled, we compare the presented fee by the dApp against the - // Ledger suggested min-fee and use the min-fee if higher than the presented fee + // Network suggested min-fee and use the min-fee if higher than the presented fee if (estimatedFee < minFee) { estimatedFee = minFee; } } else { const dappFee = estimatedFee; if (dappFee === 0) { - // If the dApp doesn't suggest a per-byte fee, we use the Ledger suggested total min-fee + // If the dApp doesn't suggest a per-byte fee, we use the network suggested total min-fee estimatedFee = minFee; } else { /* @@ -203,7 +221,7 @@ export function calculateEstimatedFee(transactionWrap: BaseValidatedTxnWrap, par dummy replica transaction that's similar to what the SDK eventually sends For this we ignore all empty fields and shorten field names to 4 characters so we use smaller field names like the SDK does - i.e.: the SDK uses 'gen' fpr 'genesisId', 'amt' for 'amount', etc + i.e.: the SDK uses 'gen' for 'genesisID', 'amt' for 'amount', etc */ const dummyTransaction = {}; Object.keys(transaction).map((key, index) => { diff --git a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts index 902a7448..136c7cc4 100644 --- a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts +++ b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts @@ -45,10 +45,13 @@ export class BaseValidatedTxnWrap { foundRequiredParams.push(key); } }); - const missingRequiredParams = requiredParamsSet.filter( - (p) => !foundRequiredParams.includes(p) - ); - missingFields.concat(missingRequiredParams); + if (!foundRequiredParams.length) { + throw RequestError.InvalidTransactionStructure( + `Creation of ${ + txnType.name + } requires at least one of these properties: ${missingFields.toString()}.` + ); + } } // Throwing error here so that missing fields can be combined. diff --git a/packages/extension/src/background/transaction/keyregTransaction.ts b/packages/extension/src/background/transaction/keyregOfflineTransaction.ts similarity index 66% rename from packages/extension/src/background/transaction/keyregTransaction.ts rename to packages/extension/src/background/transaction/keyregOfflineTransaction.ts index b6ffb5e6..373c01ee 100644 --- a/packages/extension/src/background/transaction/keyregTransaction.ts +++ b/packages/extension/src/background/transaction/keyregOfflineTransaction.ts @@ -4,15 +4,10 @@ import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; /// // Base implementation of the transactions type interface, for use in the export wrapper class below. /// -class KeyRegistrationTx implements IKeyRegistrationTx { +class OfflineKeyRegistrationTx implements IKeyRegistrationTx { type: string = undefined; - voteKey: string = undefined; - selectionKey: string = undefined; stateProofKey?: string = null; - voteFirst: number = undefined; - voteLast: number = undefined; - voteKeyDilution?: number = null; - nonParticipation?: boolean = false; + nonParticipation: boolean = undefined; from: string = undefined; fee?: number = 0; firstRound: number = undefined; @@ -31,9 +26,9 @@ class KeyRegistrationTx implements IKeyRegistrationTx { /// // Mapping, validation and error checking for keyreg transactions prior to sign. /// -export class KeyregTransaction extends BaseValidatedTxnWrap { - txDerivedTypeText: string = 'Key Registration'; +export class OfflineKeyregTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Offline Key Registration'; constructor(params: IKeyRegistrationTx) { - super(params, KeyRegistrationTx); + super(params, OfflineKeyRegistrationTx); } } diff --git a/packages/extension/src/background/transaction/keyregOnlineTransaction.ts b/packages/extension/src/background/transaction/keyregOnlineTransaction.ts new file mode 100644 index 00000000..8907a246 --- /dev/null +++ b/packages/extension/src/background/transaction/keyregOnlineTransaction.ts @@ -0,0 +1,44 @@ +import { RequestError } from '@algosigner/common/errors'; +import { IKeyRegistrationTx } from '@algosigner/common/interfaces/keyreg'; +import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; + +/// +// Base implementation of the transactions type interface, for use in the export wrapper class below. +/// +class OnlineKeyRegistrationTx implements IKeyRegistrationTx { + type: string = undefined; + voteKey: string = undefined; + selectionKey: string = undefined; + stateProofKey?: string = null; + voteFirst: number = undefined; + voteLast: number = undefined; + voteKeyDilution: number = undefined; + nonParticipation?: boolean = false; + from: string = undefined; + fee?: number = 0; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: string = null; + lease?: any = null; + reKeyTo?: any = null; + flatFee?: any = null; + name?: string = null; + tag?: string = null; +} + +/// +// Mapping, validation and error checking for keyreg transactions prior to sign. +/// +export class OnlineKeyregTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Online Key Registration'; + constructor(params: IKeyRegistrationTx) { + // Additional checks for Key Regs + if (params.nonParticipation) { + throw RequestError.InvalidTransactionStructure(`On creation of OfflineKeyRegistrationTx if nonParticipation is set to true; voteKey, selectionKey, voteFirst, voteLast, and voteKeyDilution must be undefined.`); + } + super(params, OnlineKeyRegistrationTx); + } +} diff --git a/packages/extension/src/background/utils/assetsDetailsHelper.ts b/packages/extension/src/background/utils/assetsDetailsHelper.ts index 793d9d3d..d08cc3f7 100644 --- a/packages/extension/src/background/utils/assetsDetailsHelper.ts +++ b/packages/extension/src/background/utils/assetsDetailsHelper.ts @@ -1,5 +1,5 @@ import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; -import { Ledger } from '@algosigner/common/types'; +import { Network } from '@algosigner/common/types/network'; import { InternalMethods } from '../messaging/internalMethods'; import { Cache } from '../messaging/types'; import { initializeCache } from './helper'; @@ -11,16 +11,16 @@ const TIMEOUT = 500; /// export default class AssetsDetailsHelper { private static assetsToAdd: { [key: string]: Array } = { - [Ledger.TestNet]: [], - [Ledger.MainNet]: [], + [Network.TestNet]: [], + [Network.MainNet]: [], }; private static timeouts = { - [Ledger.TestNet]: null, - [Ledger.MainNet]: null, + [Network.TestNet]: null, + [Network.MainNet]: null, }; - public static add(assets: Array, ledger: Ledger) { + public static add(assets: Array, ledger: Network) { // If this ledger doesn't have assets yet, then default them to an array if (this.assetsToAdd[ledger] === undefined) { this.assetsToAdd[ledger] = []; @@ -31,41 +31,41 @@ export default class AssetsDetailsHelper { this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); } - private static run(ledger: Ledger) { - if (this.assetsToAdd[ledger].length === 0) { - this.timeouts[ledger] = null; + private static run(network: Network) { + if (this.assetsToAdd[network].length === 0) { + this.timeouts[network] = null; return; } const extensionStorage = new ExtensionStorage(); extensionStorage.getStorage('cache', (storedCache: any) => { - const cache: Cache = initializeCache(storedCache, ledger); + const cache: Cache = initializeCache(storedCache, network); - 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; + let assetId = this.assetsToAdd[network][0]; + while (assetId in cache.assets[network]) { + this.assetsToAdd[network].shift(); + if (this.assetsToAdd[network].length === 0) { + this.timeouts[network] = null; return; } - assetId = this.assetsToAdd[ledger][0]; + assetId = this.assetsToAdd[network][0]; } - const indexer = InternalMethods.getIndexer(ledger); + const indexer = InternalMethods.getIndexer(network); indexer .lookupAssetByID(assetId) .do() .then((res: any) => { - cache.assets[ledger][assetId] = res.asset.params; + cache.assets[network][assetId] = res.asset.params; extensionStorage.setStorage('cache', cache, null); }) .catch(() => { // If there's an issue with the request, remove the asset from the queue. // If not done, it will just keep trying to get the same asset over and over. - this.assetsToAdd[ledger].shift(); + this.assetsToAdd[network].shift(); }) .finally(() => { - this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); + this.timeouts[network] = setTimeout(() => this.run(network), TIMEOUT); }); }); } diff --git a/packages/extension/src/background/utils/helper.ts b/packages/extension/src/background/utils/helper.ts index dd577d23..4b7178f8 100644 --- a/packages/extension/src/background/utils/helper.ts +++ b/packages/extension/src/background/utils/helper.ts @@ -1,24 +1,12 @@ -import algosdk from 'algosdk'; -import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; -import { Ledger } from '@algosigner/common/types'; +import { getBaseSupportedNetworks, Network, NetworkTemplate } from '@algosigner/common/types/network'; import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; import { Settings } from '../config'; -import { API, Cache } from '../messaging/types'; - -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: string) { - const params = Settings.getBackendParams(ledger, API.Indexer); - return new algosdk.Indexer(params.apiKey, params.url, params.port, params.headers); -} +import { Cache } from '../messaging/types'; // Helper function to initialize Cache export function initializeCache( c: Cache | undefined = undefined, - ledger: Ledger | undefined = undefined + network: Network | undefined = undefined ): Cache { let cache: Cache; if (c === undefined) { @@ -31,37 +19,38 @@ export function initializeCache( cache = c; } - if (ledger !== undefined) { - if (!(ledger in cache.assets)) cache.assets[ledger] = {}; - if (!(ledger in cache.accounts)) cache.accounts[ledger] = {}; + if (network !== undefined) { + if (!(network in cache.assets)) cache.assets[network] = {}; + if (!(network in cache.accounts)) cache.accounts[network] = {}; } return cache; } -export function getAvailableLedgersExt(callback) { - // Load Accounts details from Cache - const availableLedgers = getBaseSupportedLedgers(); +export function getAvailableNetworksFromCache(callback?: Function): Array | void { + // Load network details from Cache + const availableNetworks = getBaseSupportedNetworks(); const extensionStorage = new ExtensionStorage(); - extensionStorage.getStorage('cache', (storedCache: any) => { + extensionStorage.getStorage('cache', (storedCache) => { const cache: Cache = initializeCache(storedCache); - // Add ledgers from cache to the base ledgers + + // Join networks from cache with the base networks 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); + + for (let i = 0; i < cache.availableLedgers.length; i++) { + const networkFromCache = new NetworkTemplate(cache.availableLedgers[i]); + if (networkFromCache.isEditable) { + Settings.addInjectedNetwork(networkFromCache); + availableNetworks.push(networkFromCache); } } } - callback(availableLedgers); + if (callback) { + callback(availableNetworks); + } else { + return availableNetworks; + } }); } diff --git a/packages/extension/src/background/utils/networkUrlParser.ts b/packages/extension/src/background/utils/networkUrlParser.ts index 6afa15a9..30b3dc95 100644 --- a/packages/extension/src/background/utils/networkUrlParser.ts +++ b/packages/extension/src/background/utils/networkUrlParser.ts @@ -1,11 +1,16 @@ import { logging, LogLevel } from '@algosigner/common/logging'; -export function parseUrlServerAndPort(urlInput: string): any { +type ParsedUrl = { + server: string, + port: string, +} + +export function parseUrlServerAndPort(urlInput: string): ParsedUrl { // In some cases the default URL builder will not work so we default to splitting on failure let urlSplit = false; // Initialize the server and port as strings - const returnUrlObj = { server: '', port: '' }; + const returnUrlObj: ParsedUrl = { server: '', port: '' }; // If the input value is blank just return a blank urlObj if (urlInput.length === 0) { diff --git a/packages/extension/src/background/utils/session.ts b/packages/extension/src/background/utils/session.ts index 4f2277ad..e7d28a2c 100644 --- a/packages/extension/src/background/utils/session.ts +++ b/packages/extension/src/background/utils/session.ts @@ -1,23 +1,26 @@ +import { SessionObject, WalletStorage } from "@algosigner/common/types"; +import { Network, NetworkTemplate } from "@algosigner/common/types/network"; + export default class Session { - private _wallet: any; - private _ledger: any; - private _availableLedgers: any; + private _wallet: WalletStorage; + private _network: Network; + private _availableNetworks: Array; private _txnRequest: any; - public set wallet(w: any) { + public set wallet(w: WalletStorage) { this._wallet = w; } - public get wallet(): any { + public get wallet(): WalletStorage { return this._wallet; } - public set ledger(l: any) { - this._ledger = l; + public set network(n: Network) { + this._network = n; } - public get ledger(): any { - return this._ledger; + public get network(): Network { + return this._network; } public set txnRequest(r: any) { @@ -34,31 +37,31 @@ export default class Session { return r?.body?.params; } - public set availableLedgers(al: any) { - this._availableLedgers = al; + public set availableNetworks(an: Array) { + this._availableNetworks = an; } - public get availableLedgers(): any { - if (this._availableLedgers) { - return this._availableLedgers; + public get availableNetworks(): Array { + if (this._availableNetworks) { + return this._availableNetworks; } else { return []; } } - public get session(): any { + public asObject(): SessionObject { return { wallet: this._wallet, - ledger: this._ledger, - availableLedgers: this._availableLedgers || [], + network: this._network, + availableNetworks: this._availableNetworks || [], txnRequest: this._txnRequest, }; } public clearSession(): void { this._wallet = undefined; - this._ledger = undefined; - this._availableLedgers = undefined; + this._network = undefined; + this._availableNetworks = undefined; this._txnRequest = undefined; } } diff --git a/packages/extension/src/background/utils/validator.ts b/packages/extension/src/background/utils/validator.ts index aafb7857..25f86ca9 100644 --- a/packages/extension/src/background/utils/validator.ts +++ b/packages/extension/src/background/utils/validator.ts @@ -1,5 +1,5 @@ import algosdk from 'algosdk'; -import { getBaseSupportedLedgers } from '@algosigner/common/types/ledgers'; +import { getBaseSupportedNetworks } from '@algosigner/common/types/network'; import { Settings } from '../config'; export const STRING_MAX_LENGTH = 1000; @@ -16,19 +16,6 @@ export enum ValidationStatus { 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 ''; - } -} /// // Validation responses. @@ -36,11 +23,9 @@ function _convertFieldResponseToClassname(validationStatus: ValidationStatus): s export class ValidationResponse { status: ValidationStatus; info: string; - className: string; constructor(props) { this.status = props.status; this.info = props.info; - this.className = _convertFieldResponseToClassname(this.status); } } @@ -217,8 +202,8 @@ export function Validate(field: any, value: any): ValidationResponse { // 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']) + getBaseSupportedNetworks().some((n) => value === n['genesisID']) || + Settings.getCleansedInjectedNetworks().find((n) => value === n['genesisID']) ) { return new ValidationResponse({ status: ValidationStatus.Valid }); } else { diff --git a/packages/extension/src/content/content.ts b/packages/extension/src/content/content.ts index c0602808..d29e708a 100644 --- a/packages/extension/src/content/content.ts +++ b/packages/extension/src/content/content.ts @@ -1,4 +1,3 @@ -const allowed_public_methods = ['get-account-keys']; const BUNDLE = 'AlgoSigner.min.js'; import {extensionBrowser} from '@algosigner/common/chrome'; @@ -13,7 +12,7 @@ class Content { } inject() { - let url = extensionBrowser.runtime.getURL(BUNDLE); + const url = extensionBrowser.runtime.getURL(BUNDLE); const el = document.createElement('script'); el.setAttribute('type', 'text/javascript'); el.setAttribute('src', url); @@ -23,13 +22,13 @@ class Content { // Messages coming from AlgoSigner injected library. // They are sent to background using chrome.runtime. messageChannelListener() { - let ctx = this; + const ctx = this; window.addEventListener("message",(ev) => { var d = ev.data; try { if (typeof d === 'string') { - let result = JSON.parse(d); - let type = Object.prototype.toString.call(result); + 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 } @@ -37,7 +36,7 @@ class Content { else { if(Object.prototype.toString.call(d) === '[object Object]' && "source" in d){ if(d.source == "dapp") { - let eventId: string = d.body.id; + const eventId: string = d.body.id; ctx.events[eventId] = ev; } if(d.source == "dapp" || d.source == "router") { @@ -55,10 +54,10 @@ class Content { // Messages coming from background. // They are sent to the AlgoSigner injected library. chromeRuntimeListener() { - let ctx = this; + const ctx = this; extensionBrowser.runtime.onMessage.addListener((d) => { if( Object.prototype.toString.call(d) === '[object Object]' && "body" in d) { - let body = d.body; + const body = d.body; if(body.id in ctx.events) { ctx.events[body.id].ports[0].postMessage(d); delete ctx.events[body.id]; diff --git a/packages/storage/package.json b/packages/storage/package.json index edae4761..5492fb9a 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-storage", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/storage/src/extensionStorage.ts b/packages/storage/src/extensionStorage.ts index 87a690a2..081eff7e 100644 --- a/packages/storage/src/extensionStorage.ts +++ b/packages/storage/src/extensionStorage.ts @@ -17,7 +17,7 @@ export class ExtensionStorage { // Callback: Callback will return a boolean of true if storage sets without error // or false otherwise. /// - public setStorage(objectName: string, saveObject: Object, callback: Function) { + public setStorage(objectName: string, saveObject: Object, callback: Function): void { extensionBrowser.storage.local.set({ [objectName]: saveObject }, () => { const isSuccessful = !extensionBrowser.runtime.lastError; if (!isSuccessful) { @@ -36,27 +36,12 @@ export class ExtensionStorage { // 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) { + public getStorage(objectName: string, callback?: Function): void { 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. diff --git a/packages/test-project/package.json b/packages/test-project/package.json index ca801c49..0664cf0e 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -1,15 +1,15 @@ { "name": "algorand-test-project", - "version": "1.10.1", + "version": "1.11.0", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", "description": "Repository for tests", "devDependencies": { - "algosdk": "1.24.1", - "jest": "^28.1.0", + "algosdk": "2.0.0", + "jest": "^29.4.1", "jest-runner-groups": "^2.2.0", "puppeteer": "^13.7.0", - "ts-jest": "^28.0.2", + "ts-jest": "^29.0.5", "typescript": "^4.6.4" }, "scripts": { diff --git a/packages/test-project/tests/common/constants.js b/packages/test-project/tests/common/constants.js index 6b84b291..b0996a99 100644 --- a/packages/test-project/tests/common/constants.js +++ b/packages/test-project/tests/common/constants.js @@ -10,8 +10,8 @@ module.exports = { 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', + 'belt middle party tired craft blanket guide want perfect taste couch march goddess render orient chair avocado intact other stand route attend number abandon icon', + address: 'SGQOSQKZQWABMLHY6CPLKFZ4FHURBLVQTMZZQFP4U7LGZHPW3U4MIPXEDU', }, multisig: { name: 'Joined Multisig', diff --git a/packages/test-project/tests/common/tests.js b/packages/test-project/tests/common/tests.js index d1f300c0..c301b0bb 100644 --- a/packages/test-project/tests/common/tests.js +++ b/packages/test-project/tests/common/tests.js @@ -188,7 +188,7 @@ function ConnectWithAlgorandObject(accounts) { }) ).resolves.toMatchObject({ message: expect.stringContaining('The extension user has not'), - code: 4100, + code: 4202, }); }); @@ -315,7 +315,7 @@ function ConnectWithAlgoSignerObject() { }) ).resolves.toMatchObject({ message: expect.stringContaining('The extension user has not'), - code: 4100, + code: 4202, }); }); diff --git a/packages/test-project/tests/dapp-arcs-misc.test.js b/packages/test-project/tests/dapp-arcs-misc.test.js index 05d3e5b0..5db135b4 100644 --- a/packages/test-project/tests/dapp-arcs-misc.test.js +++ b/packages/test-project/tests/dapp-arcs-misc.test.js @@ -20,18 +20,20 @@ const uiAccount = accounts.ui; let sdkParams; -const getBasicSdkTxn = () => buildSdkTx({ - type: 'pay', - from: uiAccount.address, - to: uiAccount.address, - amount: Math.ceil(Math.random() * 100), - ...sdkParams, - fee: 1000, -}); +const getBasicSdkTxn = () => + buildSdkTx({ + type: 'pay', + from: uiAccount.address, + to: uiAccount.address, + amount: Math.ceil(Math.random() * 100), + ...sdkParams, + fee: 1000, + }); const getSdkTxnArray = () => [getBasicSdkTxn(), getBasicSdkTxn()]; -const signWithSDK = (tx) => byteArrayToBase64(tx.signTxn(algosdk.mnemonicToSecretKey(uiAccount.mnemonic).sk)); +const signWithSDK = (tx) => + byteArrayToBase64(tx.signTxn(algosdk.mnemonicToSecretKey(uiAccount.mnemonic).sk)); describe('Wallet Setup', () => { beforeAll(async () => { @@ -48,160 +50,218 @@ describe('Wallet Setup', () => { ConnectWithAlgorandObject([uiAccount]); }); -describe('PostTxns Validations', () => { - test('Error on missing group(s)', async () => { - const sdkTxns = getSdkTxnArray(); - const signedTxns = sdkTxns.map(signWithSDK); - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.postTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, signedTxns) - ).resolves.toMatchObject({ - message: expect.stringContaining('reasons are provided'), - code: 4300, - data: expect.stringContaining('same group') +describe('Enable validations', () => { + const secondaryAccount = accounts.multisig.subaccounts[0]; + test('Error when requesting a signature with a non-approved account', async () => { + const unavailableAccountTxn = buildSdkTx({ + type: 'pay', + from: secondaryAccount.address, + to: secondaryAccount.address, + amount: 0, + ...sdkParams, + fee: 1000, }); + const unsignedTxns = [unavailableAccountTxn.toByte()] + .map(byteArrayToBase64) + .map((b64Txn) => ({ + txn: b64Txn, + })); + const requestResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTxns) + await expect(requestResponse).toMatchObject({ + code: 4100, + message: expect.stringContaining('problem validating'), + data: expect.anything(), + }); + await expect(requestResponse.data).toHaveLength(1); + await expect(requestResponse.data[0]).toContain('No matching account found'); }); +}); - - test('Error on unordered group(s)', async () => { - const sdkTxns = algosdk.assignGroupID(getSdkTxnArray()); - const unsignedTxns = sdkTxns.map(signWithSDK); - const unorderedTxns = [unsignedTxns[1], unsignedTxns[0]]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.postTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unorderedTxns) - ).resolves.toMatchObject({ - message: expect.stringContaining('reasons are provided'), - code: 4300, - data: expect.stringContaining('different order') +describe('PostTxns Validations', () => { + describe('Transaction validations', () => { + test('Error on missing group(s)', async () => { + const sdkTxns = getSdkTxnArray(); + const signedTxns = sdkTxns.map(signWithSDK); + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.postTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, signedTxns) + ).resolves.toMatchObject({ + message: expect.stringContaining('reasons are provided'), + code: 4300, + data: expect.stringContaining('same group'), + }); }); - }); + test('Error on unordered group(s)', async () => { + const sdkTxns = algosdk.assignGroupID(getSdkTxnArray()); + const signedTxns = sdkTxns.map(signWithSDK); + const unorderedTxns = [signedTxns[1], signedTxns[0]]; + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.postTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unorderedTxns) + ).resolves.toMatchObject({ + message: expect.stringContaining('reasons are provided'), + code: 4300, + data: expect.stringContaining('different order'), + }); + }); - test('Error on incomplete group(s)', async () => { - const sdkTxns = algosdk.assignGroupID(getSdkTxnArray()); - const unsignedTxns = sdkTxns.map(signWithSDK); - const incompleteTxns = [unsignedTxns.shift()]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.postTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, incompleteTxns) - ).resolves.toMatchObject({ - message: expect.stringContaining('reasons are provided'), - code: 4300, - data: expect.stringContaining('group is incomplete') + test('Error on incomplete group(s)', async () => { + const sdkTxns = algosdk.assignGroupID(getSdkTxnArray()); + const signedTxns = sdkTxns.map(signWithSDK); + const incompleteTxns = [signedTxns.shift()]; + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.postTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, incompleteTxns) + ).resolves.toMatchObject({ + message: expect.stringContaining('reasons are provided'), + code: 4300, + data: expect.stringContaining('group is incomplete'), + }); }); }); - - jest.setTimeout(20000); - - test('Partially succeded post', async () => { - async function signTxnGroups(transactionsToSign) { - return await dappPage.evaluate( - async (transactionsToSign) => { - const signPromise = algorand.signTxns(transactionsToSign) + + describe('Network validations', () => { + test('Error on failed post', async () => { + const noFeeTxn = getBasicSdkTxn(); + noFeeTxn.fee = 0; + console.log(noFeeTxn); + const sdkTxns = [noFeeTxn]; + const signedTxns = sdkTxns.map(signWithSDK); + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.postTxns(transactions)) .then((data) => { return data; }) .catch((error) => { return error; }); - - const amountOfGroups = Array.isArray(transactionsToSign[0]) ? transactionsToSign.length : 1; + }, signedTxns) + ).resolves.toMatchObject({ + message: expect.stringContaining('unable to be posted. The reason'), + code: 4400, + data: expect.stringContaining('had 0 in fees'), + }); + }); + + jest.setTimeout(25000); + + test('Partially succeded post', async () => { + async function signTxnGroups(transactionsToSign) { + return await dappPage.evaluate(async (transactionsToSign) => { + const signPromise = algorand + .signTxns(transactionsToSign) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + + const amountOfGroups = Array.isArray(transactionsToSign[0]) + ? transactionsToSign.length + : 1; await window['authorizeSignTxnGroups'](amountOfGroups); - + return await Promise.resolve(signPromise); - }, - transactionsToSign, - ); - } + }, transactionsToSign); + } - const noFeeTx = buildSdkTx({ - type: 'pay', - from: uiAccount.address, - to: uiAccount.address, - amount: Math.ceil(Math.random() * 100), - ...sdkParams, - fee: 0, - flatFee: true, - }); + const noFeeTx = buildSdkTx({ + type: 'pay', + from: uiAccount.address, + to: uiAccount.address, + amount: Math.ceil(Math.random() * 100), + ...sdkParams, + fee: 0, + flatFee: true, + }); - const sdkTxns = [noFeeTx, getBasicSdkTxn()]; - const unsignedTxns = sdkTxns.map(prepareWalletTx); - const nestedTxns = unsignedTxns.map((tx) => [tx]); - await extensionPage.waitForTimeout(2000); - const signedTxns = await signTxnGroups(nestedTxns); - const postResponse = await dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.postTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, signedTxns); + const sdkTxns = [noFeeTx, getBasicSdkTxn()]; + const unsignedTxns = sdkTxns.map(prepareWalletTx); + const nestedTxns = unsignedTxns.map((tx) => [tx]); + await extensionPage.waitForTimeout(2000); + const signedTxns = await signTxnGroups(nestedTxns); + const postResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.postTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, signedTxns); - const txID = algosdk.decodeSignedTransaction(base64ToByteArray(signedTxns[1][0])).txn.txID(); - await expect(postResponse).toMatchObject({ - message: expect.stringContaining('unsuccessful group'), - code: 4400, - data: expect.anything(), - successTxnIDs: expect.anything(), + const txID = algosdk.decodeSignedTransaction(base64ToByteArray(signedTxns[1][0])).txn.txID(); + await expect(postResponse).toMatchObject({ + message: expect.stringContaining('unsuccessful group'), + code: 4400, + data: expect.anything(), + successTxnIDs: expect.anything(), + }); + await expect(postResponse.successTxnIDs).toHaveLength(2); + await expect(postResponse.successTxnIDs[1]).toHaveLength(1); + await expect(postResponse.successTxnIDs[1]).toContain(txID); + await expect(postResponse.data).toHaveLength(2); }); - await expect(postResponse.successTxnIDs).toHaveLength(2); - await expect(postResponse.successTxnIDs[1]).toHaveLength(1); - await expect(postResponse.successTxnIDs[1]).toContain(txID); - await expect(postResponse.data).toHaveLength(2); - }); - test('Sign and Send commits txs to the network', async () => { - async function signAndPostTxns(transactionsToSign) { - return await dappPage.evaluate( - async (transactionsToSign) => { - console.log('before sign'); - const signPostPromise = algorand.signAndPostTxns(transactionsToSign) + jest.setTimeout(30000); + + test('Sign and Send commits txs to the network', async () => { + async function signAndPostTxns(transactionsToSign) { + return await dappPage.evaluate(async (transactionsToSign) => { + const signPostPromise = algorand + .signAndPostTxns(transactionsToSign) .then((data) => { return data; }) .catch((error) => { return error; }); - + await window['authorizeSignTxn'](); return await Promise.resolve(signPostPromise); - }, - transactionsToSign, - ); - } - - const sdkTxn = getBasicSdkTxn(); - const unsignedTxns = [sdkTxn].map(prepareWalletTx); - const txID = sdkTxn.txID(); - await extensionPage.waitForTimeout(2000); - const signPostResponse = await signAndPostTxns(unsignedTxns); - await expect(signPostResponse).toMatchObject({ - txnIDs: expect.arrayContaining([txID]), + }, transactionsToSign); + } + + const sdkTxn = getBasicSdkTxn(); + const unsignedTxns = [sdkTxn].map(prepareWalletTx); + const txID = sdkTxn.txID(); + await extensionPage.waitForTimeout(2000); + const signPostResponse = await signAndPostTxns(unsignedTxns); + await expect(signPostResponse).toMatchObject({ + txnIDs: expect.arrayContaining([txID]), + }); }); - }); -}); \ No newline at end of file +}); diff --git a/packages/test-project/tests/dapp-arcs-signtxns.test.js b/packages/test-project/tests/dapp-arcs-signtxns.test.js index b5c21afd..c6ff3340 100644 --- a/packages/test-project/tests/dapp-arcs-signtxns.test.js +++ b/packages/test-project/tests/dapp-arcs-signtxns.test.js @@ -103,22 +103,24 @@ describe('Txn Signing Validation errors', () => { ); unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4100, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(invalidAccount), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(invalidAccount); + expect(signResponse.data[0]).toContain('No matching account found'); }); // Signers validations @@ -167,22 +169,23 @@ describe('Txn Signing Validation errors', () => { txn.signers = [account2.address]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining("When a single-address 'signers'"), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain("a single-address 'signers' is provided"); }); test('Error on Single signer not matching authAddr', async () => { @@ -200,22 +203,23 @@ describe('Txn Signing Validation errors', () => { txn.authAddr = account2.address; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining("When a single-address 'signers'"), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain("a single-address 'signers' is provided"); }); test('Error on Invalid signer address', async () => { @@ -233,22 +237,23 @@ describe('Txn Signing Validation errors', () => { txn.signers = [fakeAccount]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(`Signers array contains the invalid address "${fakeAccount}"`), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(`Signers array contains the invalid address "${fakeAccount}"`); }); // AuthAddr validations @@ -267,22 +272,23 @@ describe('Txn Signing Validation errors', () => { txn.authAddr = fakeAccount; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(`'authAddr' contains the invalid address "${fakeAccount}"`), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(`'authAddr' contains the invalid address "${fakeAccount}"`); }); // Msig validations @@ -292,22 +298,24 @@ describe('Txn Signing Validation errors', () => { txn.signers = [account1.address, invalidAccount]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(algorand.signTxns(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(algorand.signTxns(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(invalidAccount), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(invalidAccount); + expect(signResponse.data[0]).toContain('does not currently possess one of'); }); // // @TODO: Wallet Transaction Structure check tests diff --git a/packages/test-project/tests/dapp-legacy-signtxn.test.js b/packages/test-project/tests/dapp-legacy-signtxn.test.js index fec76c61..209e9103 100644 --- a/packages/test-project/tests/dapp-legacy-signtxn.test.js +++ b/packages/test-project/tests/dapp-legacy-signtxn.test.js @@ -103,22 +103,24 @@ describe('Txn Signing Validation errors', () => { ); unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4100, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(invalidAccount), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(invalidAccount); + expect(signResponse.data[0]).toContain('No matching account found'); }); // Signers validations @@ -167,22 +169,23 @@ describe('Txn Signing Validation errors', () => { txn.signers = [account2.address]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining("When a single-address 'signers'"), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain("a single-address 'signers' is provided"); }); test('Error on Single signer not matching authAddr', async () => { @@ -200,22 +203,23 @@ describe('Txn Signing Validation errors', () => { txn.authAddr = account2.address; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining("When a single-address 'signers'"), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain("a single-address 'signers' is provided"); }); test('Error on Invalid signer address', async () => { @@ -233,22 +237,23 @@ describe('Txn Signing Validation errors', () => { txn.signers = [fakeAccount]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(`Signers array contains the invalid address "${fakeAccount}"`), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(`Signers array contains the invalid address "${fakeAccount}"`); }); // AuthAddr validations @@ -267,22 +272,23 @@ describe('Txn Signing Validation errors', () => { txn.authAddr = fakeAccount; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(`'authAddr' contains the invalid address "${fakeAccount}"`), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(`'authAddr' contains the invalid address "${fakeAccount}"`); }); // Msig validations @@ -292,22 +298,24 @@ describe('Txn Signing Validation errors', () => { txn.signers = [account1.address, invalidAccount]; unsignedTransactions = [txn]; - await expect( - dappPage.evaluate((transactions) => { - return Promise.resolve(AlgoSigner.signTxn(transactions)) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - }, unsignedTransactions) - ).resolves.toMatchObject({ - message: expect.stringContaining('There was a problem signing the transaction(s).'), + const signResponse = await dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions); + expect(signResponse).toMatchObject({ + message: expect.stringContaining('There was a problem validating the transaction(s) to be signed.'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(invalidAccount), + data: expect.anything(), }); + expect(signResponse.data).toHaveLength(1); + expect(signResponse.data[0]).toContain(invalidAccount); + expect(signResponse.data[0]).toContain('does not currently possess one of'); }); // // @TODO: Wallet Transaction Structure check tests diff --git a/packages/test-project/tests/ui-networks-e2e.test.js b/packages/test-project/tests/ui-networks-e2e.test.js index 523e87e5..75d8a1a9 100644 --- a/packages/test-project/tests/ui-networks-e2e.test.js +++ b/packages/test-project/tests/ui-networks-e2e.test.js @@ -31,9 +31,9 @@ describe('Wallet Setup', () => { describe('Create and Test Custom Networks', () => { const NetworkConfig = { name: 'E2ENet', - id: 'testnet-v1.0', - algod: 'https://algosigner.api.purestake.io/testnet/algod', - indexer: 'https://algosigner.api.purestake.io/testnet/indexer', + algod: 'https://betanet-algorand.api.purestake.io/ps2', + indexer: 'https://betanet-algorand.api.purestake.io/idx2', + headers: '{"Algod":{"x-api-key":"yiMMSnme3SE0CmZZARJW87yWHTXZrEGaNSJNx2Me"},"Indexer":{"x-api-key":"yiMMSnme3SE0CmZZARJW87yWHTXZrEGaNSJNx2Me"}}', }; const e2eNetSelector = `button#select${NetworkConfig.name}`; @@ -45,11 +45,11 @@ describe('Create and Test Custom Networks', () => { await openNetworkMenu(); await extensionPage.click('#createNetwork'); - // Fill wrong network config + // Fill erroneous network config await extensionPage.type('#networkName', NetworkConfig.name); - await extensionPage.type('#networkId', NetworkConfig.id); await extensionPage.type('#networkAlgodUrl', NetworkConfig.indexer); await extensionPage.type('#networkIndexerUrl', NetworkConfig.indexer); + await extensionPage.type('#networkHeaders', NetworkConfig.headers); // Test connection rejected await extensionPage.click('#checkNetwork'); @@ -105,7 +105,7 @@ describe('Create and Test Custom Networks', () => { await inputPassword(); // Check network was deleted - await openNetworkMenu(); + await extensionPage.waitForTimeout(1000); await expect(extensionPage.select(otherNetSelector)).rejects.toThrow(); }); }); diff --git a/packages/test-project/tests/ui-transactions-e2e.test.js b/packages/test-project/tests/ui-transactions-e2e.test.js index c38d3355..b2456315 100644 --- a/packages/test-project/tests/ui-transactions-e2e.test.js +++ b/packages/test-project/tests/ui-transactions-e2e.test.js @@ -32,13 +32,13 @@ describe('Wallet Setup', () => { describe('UI Transactions Tests', () => { const amount = Math.ceil(Math.random() * 3); // txn size, modify multiplier for bulk + const testAssetIndex = 157722252; + const testAssetDecimals = 6; let txId; // returned tx id from send txn let txTitle; // for tx verification - beforeEach(async () => { - jest.setTimeout(15000); - }); + jest.setTimeout(17000); afterAll(async () => { extensionPage.close(); @@ -141,19 +141,20 @@ describe('UI Transactions Tests', () => { await extensionPage.waitForTimeout(100); await extensionPage.click('#selectAsset'); await extensionPage.waitForTimeout(100); - await extensionPage.waitForSelector('#asset-13169404'); - await extensionPage.click('#asset-13169404'); + const assetSelector = `#asset-${testAssetIndex}`; + await extensionPage.waitForSelector(assetSelector); + await extensionPage.click(assetSelector); await extensionPage.waitForTimeout(500); // Test correct decimal handling - await extensionPage.type('#transferAmount', `0.000000000${amount}`); - const actualAmount = await extensionPage.$eval('#transferAmount', (e) => { + await extensionPage.type('#transferAmount', `${(amount / (10 ** (testAssetDecimals + 3))).toFixed(testAssetDecimals + 3)}`); + const enteredAmount = await extensionPage.$eval('#transferAmount', (e) => { const inputValue = e.value; e.value = ''; return inputValue; }); - expect(actualAmount).toMatch('0.000000'); + expect(enteredAmount).toMatch(`0.${new Array(testAssetDecimals).fill(0).join('')}`); // Test actual transfer - await extensionPage.type('#transferAmount', `0.00000${amount}`); + await extensionPage.type('#transferAmount', `${(amount / (10 ** testAssetDecimals).toFixed(testAssetDecimals))}`); // Select an Imported Account from aliases await extensionPage.type('#destinationAddress', 't'); await extensionPage.waitForSelector('.alias-selector-container'); diff --git a/packages/ui/package.json b/packages/ui/package.json index 25a7ce1b..83a50649 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-ui", - "version": "1.10.1", + "version": "1.11.0", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", @@ -16,7 +16,7 @@ "@fortawesome/fontawesome-free": "^5.15.0", "@ledgerhq/hw-app-algorand": "^6.27.9", "@ledgerhq/hw-transport-webhid": "^6.27.9", - "algosdk": "1.24.1", + "algosdk": "2.0.0", "buffer": "^6.0.3", "history": "^5.0.0", "htm": "^3.0.4", diff --git a/packages/ui/src/components/Account/AssetsList.ts b/packages/ui/src/components/Account/AssetsList.ts index 5321cba4..3bba0bc6 100644 --- a/packages/ui/src/components/Account/AssetsList.ts +++ b/packages/ui/src/components/Account/AssetsList.ts @@ -120,7 +120,7 @@ const AssetsList: FunctionalComponent = (props: any) => { diff --git a/packages/ui/src/components/Footer.ts b/packages/ui/src/components/Footer.ts index 3476bd16..e3c6bef7 100644 --- a/packages/ui/src/components/Footer.ts +++ b/packages/ui/src/components/Footer.ts @@ -5,7 +5,7 @@ const Footer: FunctionalComponent = () => { return html`
- ${'\u00A9'} 2022 PureStake + ${'\u00A9'} 2023 PureStake
`; diff --git a/packages/ui/src/components/LedgerDevice/LedgerHardwareConnector.ts b/packages/ui/src/components/LedgerDevice/LedgerHardwareConnector.ts index 726fc006..49fb0d06 100644 --- a/packages/ui/src/components/LedgerDevice/LedgerHardwareConnector.ts +++ b/packages/ui/src/components/LedgerDevice/LedgerHardwareConnector.ts @@ -36,10 +36,10 @@ const LedgerHardwareConnector: FunctionalComponent = (props: any) => { setError(''); const ddItems = new Array(); - const storeLedgerAddresses = new Array(); + const storedNetworkAddresses = new Array(); - for (let i = 0; i < store[ledger]?.length; i++) { - storeLedgerAddresses.push(store[ledger][i].address); + for (let i = 0; i < store.wallet[ledger]?.length; i++) { + storedNetworkAddresses.push(store.wallet[ledger][i].address); } ledgerActions @@ -51,7 +51,7 @@ const LedgerHardwareConnector: FunctionalComponent = (props: any) => { ); } else { for (let i = 0; i < lar.message?.length; i++) { - if (!storeLedgerAddresses?.includes(`${lar.message[i].publicAddress}`)) { + if (!storedNetworkAddresses?.includes(`${lar.message[i].publicAddress}`)) { ddItems.push(lar.message[i]); } } diff --git a/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts b/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts index 45f3f4f1..4eb85470 100644 --- a/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts +++ b/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts @@ -3,7 +3,7 @@ import { html } from 'htm/preact'; import { useContext, useEffect, useState } from 'preact/hooks'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { getBaseSupportedLedgers } from '@algosigner/common/types/ledgers'; +import { getBaseSupportedNetworks } from '@algosigner/common/types/network'; import { logging, LogLevel } from '@algosigner/common/logging'; import TxAcfg from 'components/SignTransaction/TxAcfg'; import TxPay from 'components/SignTransaction/TxPay'; @@ -28,22 +28,22 @@ const LedgerHardwareSign: FunctionalComponent = () => { const [account, setAccount] = useState(''); const [txResponseHeader, setTxResponseHeader] = useState(''); const [txResponseDetail, setTxResponseDetail] = useState(''); - const [ledger, setLedger] = useState(''); + const [network, setNetwork] = useState(''); const [sessionTxnObj, setSessionTxnObj] = useState({}); const [showTooltip, setShowTooltip] = useState(false); let currentTransaction: number = 0; let totalTxns: number = 0; - const updateAccountName = (ledger: string, from: string): void => { - if (store[ledger] && store[ledger].length) { - for (let i = 0; i < store[ledger].length; i++) { - const lookupAddress = store[ledger][i].address; - const lookupName = store[ledger][i].name; + const updateAccountName = (network: string, from: string): void => { + if (store.wallet[network] && store.wallet[network].length) { + for (let i = 0; i < store.wallet[network].length; i++) { + const lookupAddress = store.wallet[network][i].address; + const lookupName = store.wallet[network][i].name; if (lookupAddress === from) setAccount(lookupName); } } else { setTimeout(() => { - updateAccountName(ledger, from); + updateAccountName(network, from); }, 100); } }; @@ -63,12 +63,12 @@ const LedgerHardwareSign: FunctionalComponent = () => { setSessionTxnObj(returnedSessionObj); setTxn(txToSign); - getBaseSupportedLedgers().forEach((l) => { - if (txToSign.transaction?.genesisID === l['genesisId']) { - const fetchedLedger = l['name']; - setLedger(fetchedLedger); - store.setLedger(fetchedLedger); - updateAccountName(fetchedLedger, txToSign.transaction?.from); + getBaseSupportedNetworks().forEach((n) => { + if (txToSign.transaction?.genesisID === n['genesisID']) { + const matchingNetwork = n['name']; + setNetwork(matchingNetwork); + store.setActiveNetwork(matchingNetwork); + updateAccountName(matchingNetwork, txToSign.transaction?.from); } }); } @@ -113,7 +113,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { setSessionTxnObj(message.body.params); setTxn(txToSign); - updateAccountName(ledger, txToSign.transaction?.from); + updateAccountName(network, txToSign.transaction?.from); setShowTooltip(true); } else if (response) { setTxResponseHeader('Transaction(s) signed. Result sent to origin tab.'); @@ -189,7 +189,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { vo=${txn.validityObject} estFee=${txn.estimatedFee} account=${account} - ledger=${ledger} + network=${network} /> `} ${txn.transaction.type === 'keyreg' && @@ -199,7 +199,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { vo=${txn.validityObject} estFee=${txn.estimatedFee} account=${account} - ledger=${ledger} + network=${network} /> `} ${txn.transaction.type === 'acfg' && @@ -210,7 +210,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { dt=${txn.txDerivedTypeText} estFee=${txn.estimatedFee} account=${account} - ledger=${ledger} + network=${network} /> `} ${txn.transaction.type === 'axfer' && @@ -223,7 +223,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { da=${txn.displayAmount} un=${txn.unitName} account=${account} - ledger=${ledger} + network=${network} /> `} ${txn.transaction.type === 'afrz' && @@ -233,7 +233,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { vo=${txn.validityObject} estFee=${txn.estimatedFee} account=${account} - ledger=${ledger} + network=${network} /> `} ${txn.transaction.type === 'appl' && @@ -243,7 +243,7 @@ const LedgerHardwareSign: FunctionalComponent = () => { vo=${txn.validityObject} estFee=${txn.estimatedFee} account=${account} - ledger=${ledger} + network=${network} /> `} diff --git a/packages/ui/src/components/MainHeader.ts b/packages/ui/src/components/MainHeader.ts index 4d75970f..789d69ed 100644 --- a/packages/ui/src/components/MainHeader.ts +++ b/packages/ui/src/components/MainHeader.ts @@ -2,7 +2,7 @@ import { FunctionalComponent } from 'preact'; import { html } from 'htm/preact'; import HeaderComponent from './HeaderComponent'; -import LedgerSelect from './LedgerSelect'; +import NetworkSelect from './NetworkSelect'; import OpenContacts from './OpenContacts'; import SettingsMenu from './SettingsMenu'; import Logo from './Logo'; @@ -11,7 +11,7 @@ const MainHeader: FunctionalComponent = () => { return html` <${HeaderComponent}> <${Logo} /> - <${LedgerSelect} /> + <${NetworkSelect} /> <${OpenContacts} /> <${SettingsMenu} /> diff --git a/packages/ui/src/components/LedgerSelect.ts b/packages/ui/src/components/NetworkSelect.ts similarity index 69% rename from packages/ui/src/components/LedgerSelect.ts rename to packages/ui/src/components/NetworkSelect.ts index 03558b81..8aebaffc 100644 --- a/packages/ui/src/components/LedgerSelect.ts +++ b/packages/ui/src/components/NetworkSelect.ts @@ -7,14 +7,14 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { StoreContext } from 'services/StoreContext'; import { sendMessage } from 'services/Messaging'; -const LedgerSelect: FunctionalComponent = () => { +const NetworkSelect: FunctionalComponent = () => { const store: any = useContext(StoreContext); const [active, setActive] = useState(false); - let sessionLedgers; - store.getAvailableLedgers((availableLedgers) => { - if (!availableLedgers.error) { - sessionLedgers = availableLedgers; + let sessionNetworks; + store.getAvailableNetworks((availableNetworks) => { + if (!availableNetworks.error) { + sessionNetworks = availableNetworks; } }); @@ -25,12 +25,12 @@ const LedgerSelect: FunctionalComponent = () => { setActive(!active); }; - const setLedger = (ledger) => { + const setNetwork = (network) => { const params = { - ledger: ledger, + ledger: network, }; - sendMessage(JsonRpcMethod.ChangeLedger, params, function () { - store.setLedger(ledger); + sendMessage(JsonRpcMethod.ChangeNetwork, params, function () { + store.setActiveNetwork(network); flip(); route('/wallet'); }); @@ -51,22 +51,22 @@ const LedgerSelect: FunctionalComponent = () => { - ${store.ledger} + ${store.activeNetwork}