diff --git a/package.json b/package.json index 6ade33af..53a69084 100644 --- a/package.json +++ b/package.json @@ -12,17 +12,17 @@ "scripts": { "install:extension": "(cd ./packages/common && npm install); (cd ./packages/crypto && npm install); (cd ./packages/storage && npm install); (cd ./packages/ui && npm install); (cd ./packages/extension && npm install); (cd ./packages/dapp && npm install);", "install:test": "(cd ./packages/test-project && npm install);", - "removelocks": "rm -rf ./package-lock.json && find -path \"./packages/*\" -name \"package-lock.json\" -not -path \"*/node_modules/*\" -exec rm -rf {} \\;", - "update": "npm update && find -maxdepth 2 -path \"./packages/*\" -exec npm update {} \\;", + "removelocks": "rm -rf ./package-lock.json && find . -path \"./packages/*\" -name \"package-lock.json\" -not -path \"*/node_modules/*\" -exec rm -rf {} \\;", + "removemodules": "rm -rf node_modules && find . -path \"./packages/*\" -name \"node_modules*\" -exec rm -rf {} \\;", "build": "(cd ./packages/common && npm run build); (cd ./packages/crypto && npm run build); (cd ./packages/dapp && npm run build); (cd ./packages/storage && npm run build); (cd ./packages/ui && npm run build); (cd ./packages/extension && npm run build);", "build:ui": "cd ./packages/ui && npm run build && cp -r ./dist/* ../../dist/", "build:extension": "cd ./packages/extension && npm run build && cp -r ./dist/* ../../dist/", - "clean": "rm -rf ./dist/* && rm -rf ./packages/common/dist/* && rm -rf ./packages/crypto/dist/* && rm -rf ./packages/dapp/dist/* && rm -rf ./packages/extension/dist/* && rm -rf ./packages/storage/dist/* && rm -rf ./packages/ui/dist/* && rm -rf ./packages/dapp/lib/*", + "clean": "rm -rf ./dist && rm -rf ./packages/common/dist && rm -rf ./packages/crypto/dist && rm -rf ./packages/dapp/dist && rm -rf ./packages/extension/dist && rm -rf ./packages/storage/dist && rm -rf ./packages/ui/dist && rm -rf ./packages/dapp/dist", "copy": "mkdir -p ./dist && cp -r ./packages/extension/dist/* dist/ && cp -r ./packages/ui/dist/* dist/ && cp -r ./packages/dapp/dist/* dist/ && mkdir -p ./dist/docs && cp -r ./docs/* dist/docs/", "prebuild": "rm -rf ./dist/*", "postbuild": "npm run copy", "postinstall": "npm run install:extension && npm run install:test", - "rebuild": "npm run clean && npm run removelocks && npm install && npm run update && npm run build", + "rebuild": "npm run clean && npm run removelocks && npm run removemodules && npm install && npm run build", "coveragetest": "(cd ./packages/test-project && npm run coveragetest)", "test": "npm run test:unit && npm run test:e2e", "test:unit": "(cd ./packages/crypto && npm run test) && (cd ./packages/extension && npm run test) && (cd ./packages/ui && npm run test) && (cd ./packages/dapp && npm run test) && (cd ./packages/common && npm run test)", diff --git a/packages/common/src/interfaces/axfer_close.ts b/packages/common/src/interfaces/axfer_close.ts new file mode 100644 index 00000000..a4dae778 --- /dev/null +++ b/packages/common/src/interfaces/axfer_close.ts @@ -0,0 +1,13 @@ +import { IBaseTx } from './baseTx'; + +/// +// Mapping interface of allowable fields for axfer transactions. +/// + +// prettier-ignore +export interface IAssetCloseTx extends IBaseTx { + type: string, //"axfer" + assetIndex: number, //uint64 "xaid" The unique ID of the asset to be transferred. + closeRemainderTo: string, //Address "aclose" Specify this field to remove the asset holding from the sender account and reduce the account's minimum balance. + to: string, //Address +} diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index 0436d04c..3f1cdff1 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -36,6 +36,7 @@ export enum JsonRpcMethod { AssetDetails = 'asset-details', AssetsAPIList = 'assets-api-list', AssetsVerifiedList = 'assets-verified-list', + AssetOptOut = 'asset-opt-out', SignSendTransaction = 'sign-send-transaction', ChangeLedger = 'change-ledger', SaveNetwork = 'save-network', diff --git a/packages/extension/src/background/config.ts b/packages/extension/src/background/config.ts index 1f3809fc..cfb29290 100644 --- a/packages/extension/src/background/config.ts +++ b/packages/extension/src/background/config.ts @@ -92,9 +92,9 @@ export class Settings { } // Setup port splits for algod and indexer - used in sandbox installs - const parsedAlgodUrlObj = parseUrlServerAndPort(ledger.algodUrl) + const parsedAlgodUrlObj = parseUrlServerAndPort(ledger.algodUrl); const parsedIndexerUrlObj = parseUrlServerAndPort(ledger.indexerUrl); - + // Add algod links const injectedAlgod = { url: parsedAlgodUrlObj.server || `${defaultUrl}/algod`, @@ -111,15 +111,14 @@ export class Settings { headers: headersIndexer || headers, }; - if (isCheckOnly) { + if (isCheckOnly) { return { - 'algod': injectedAlgod, - 'indexer': injectedIndexer - } - } - else { + 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][API.Indexer] = injectedIndexer; this.backend_settings.InjectedNetworks[ledger.name].headers = headers; } } @@ -132,20 +131,40 @@ export class Settings { }; this.setInjectedHeaders(ledger); - logging.log(`Added Network:\n${JSON.stringify(this.backend_settings.InjectedNetworks[ledger.name],null,1)}`,2); + logging.log( + `Added Network:\n${JSON.stringify( + this.backend_settings.InjectedNetworks[ledger.name], + null, + 1 + )}`, + 2 + ); } - public static updateInjectedNetwork(updatedLedger: LedgerTemplate) { - this.backend_settings.InjectedNetworks[updatedLedger.name].genesisId = updatedLedger.genesisId; - this.backend_settings.InjectedNetworks[updatedLedger.name].symbol = updatedLedger.symbol; - this.backend_settings.InjectedNetworks[updatedLedger.name].genesisHash = + public static updateInjectedNetwork(updatedLedger: LedgerTemplate, previousName: string = '') { + const targetName = updatedLedger.uniqueName; + + if (previousName) { + this.deleteInjectedNetwork(previousName); + this.backend_settings.InjectedNetworks[targetName] = {}; + } + 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[updatedLedger.name].algodUrl = updatedLedger.algodUrl; - this.backend_settings.InjectedNetworks[updatedLedger.name].indexerUrl = + 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[updatedLedger.name],null,1)}`,2); + logging.log( + `Updated Network:\n${JSON.stringify( + this.backend_settings.InjectedNetworks[targetName], + null, + 1 + )}`, + 2 + ); } public static getBackendParams(ledger: string, api: API) { @@ -169,7 +188,7 @@ export class Settings { } public static checkNetwork(ledger: LedgerTemplate) { - const networks = this.setInjectedHeaders(ledger, true); + const networks = this.setInjectedHeaders(ledger, true); return networks; } } diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index f1198cda..94674e8e 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -334,7 +334,7 @@ export class InternalMethods { const sessTxn = session.txnWrap.body.params.transaction; // Set the fee to the estimate we showed on the screen for validation if there is one. - if(session.txnWrap.body.params.estimatedFee) { + if (session.txnWrap.body.params.estimatedFee) { sessTxn['fee'] = session.txnWrap.body.params.estimatedFee; } const sessTxnEntries = Object.entries(sessTxn).sort(); @@ -371,18 +371,18 @@ export class InternalMethods { // If v2 then it needs to return an array if (session.txnWrap?.body?.params?.transactionsOrGroups) { - message.response = [{ - blob: request.body.params.txn - }]; - } - else { + message.response = [ + { + blob: request.body.params.txn, + }, + ]; + } else { message.response = { - blob: request.body.params.txn + blob: request.body.params.txn, }; } sendResponse({ message: message }); - } // If this is a ui transaction then we need to also submit else if (session.txnWrap.source === 'ui') { @@ -678,6 +678,130 @@ export class InternalMethods { return true; } + public static [JsonRpcMethod.AssetOptOut](request: any, sendResponse: Function) { + const { ledger, address, passphrase, id } = request.body.params; + this._encryptionWrap = new encryptionWrap(passphrase); + const algod = this.getAlgod(ledger); + + this._encryptionWrap.unlock(async (unlockedValue: any) => { + if ('error' in unlockedValue) { + sendResponse(unlockedValue); + return false; + } + let account; + + // Find address to send algos from + for (var i = unlockedValue[ledger].length - 1; i >= 0; i--) { + if (unlockedValue[ledger][i].address === address) { + account = unlockedValue[ledger][i]; + break; + } + } + + const params = await algod.getTransactionParams().do(); + const txn = { + type: 'axfer', + from: address, + to: address, + closeRemainderTo: address, + assetIndex: id, + fee: params.fee, + firstRound: params.firstRound, + lastRound: params.lastRound, + genesisID: params.genesisID, + genesisHash: params.genesisHash, + }; + + let transactionWrap: BaseValidatedTxnWrap = undefined; + try { + transactionWrap = getValidatedTxnWrap(txn, txn['type']); + } catch (e) { + logging.log(`Validation failed. ${e}`); + sendResponse({ error: `Validation failed. ${e}` }); + return; + } + if (!transactionWrap) { + // We don't have a transaction wrap. We have an unknow error or extra fields, reject the transaction. + logging.log( + 'A transaction has failed because of an inability to build the specified transaction type.' + ); + sendResponse({ + error: + 'A transaction has failed because of an inability to build the specified transaction type.', + }); + return; + } else if ( + transactionWrap.validityObject && + Object.values(transactionWrap.validityObject).some( + (value) => value['status'] === ValidationStatus.Invalid + ) + ) { + // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. + const e = + 'One or more fields are not valid. Please check and try again.\n' + + Object.values(transactionWrap.validityObject) + .filter((value) => value['status'] === ValidationStatus.Invalid) + .map((vo) => vo['info']); + sendResponse({ error: e }); + return; + } else { + // We have a transaction which does not contain invalid fields, + // but may still contain fields that are dangerous + // or ones we've flagged as needing to be reviewed. + // Perform a change based on if this is a ledger device account + if (account.isHardware) { + // TODO: Temporary workaround by adding min-fee for estimate calculations since it's not in the sdk get params. + params['min-fee'] = 1000; + calculateEstimatedFee(transactionWrap, params); + + // Pass the transaction wrap we can pass to the + // central sign ledger function for consistency + this[JsonRpcMethod.LedgerSignTransaction]( + { source: 'ui', body: { params: transactionWrap } }, + (response) => { + // We only have to worry about possible errors here so we can ignore the created tab + if ('error' in response) { + sendResponse(response); + } else { + // Respond with a 0 tx id so that the page knows not to try and show it. + sendResponse({ txId: 0 }); + } + } + ); + + // Return to close connection + return true; + } else { + // We can use a modified popup to allow the normal flow, but require extra scrutiny. + const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); + let signedTxn; + try { + const builtTx = buildTransaction(txn); + signedTxn = { + txID: builtTx.txID().toString(), + blob: builtTx.signTxn(recoveredAccount.sk), + }; + } catch (e) { + sendResponse({ error: e.message }); + return false; + } + + algod + .sendRawTransaction(signedTxn.blob) + .do() + .then((resp: any) => { + sendResponse({ txId: resp.txId }); + }) + .catch((e: any) => { + sendResponse({ error: e.message }); + }); + } + } + }); + + return true; + } + public static [JsonRpcMethod.SignSendTransaction](request: any, sendResponse: Function) { const { ledger, address, passphrase, txnParams } = request.body.params; this._encryptionWrap = new encryptionWrap(passphrase); @@ -891,9 +1015,10 @@ export class InternalMethods { public static [JsonRpcMethod.SaveNetwork](request: any, sendResponse: Function) { try { + const params = request.body.params; // If we have a passphrase then we are modifying. // There may be accounts attatched, if we match on a unique name, we should update. - if (request.body.params['passphrase'] !== undefined) { + if (params['passphrase'] !== undefined) { this._encryptionWrap = new encryptionWrap(request.body.params['passphrase']); this._encryptionWrap.unlock((unlockedValue: any) => { if ('error' in unlockedValue) { @@ -902,14 +1027,17 @@ export class InternalMethods { // 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({ - name: request.body.params['name'], - genesisId: request.body.params['genesisId'], - genesisHash: request.body.params['genesisHash'], - symbol: request.body.params['symbol'], - algodUrl: request.body.params['algodUrl'], - indexerUrl: request.body.params['indexerUrl'], - headers: request.body.params['headers'], + 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. @@ -919,19 +1047,17 @@ export class InternalMethods { const comboLedgers = [...availiableLedgers]; // Add the new ledger if it isn't there. - if (!comboLedgers.some((cledg) => cledg.uniqueName === addedLedger.uniqueName)) { + if (!comboLedgers.some((cledg) => cledg.uniqueName === targetName)) { comboLedgers.push(addedLedger); // Also add the ledger to the injected ledgers in settings Settings.addInjectedNetwork(addedLedger); - } - // If the new ledger name does exist, we sould update the values as long as it is not a default ledger. - else { - const matchingLedger = comboLedgers.find( - (cledg) => cledg.uniqueName === addedLedger.uniqueName - ); + } 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); + Settings.updateInjectedNetwork(addedLedger, previousName); + matchingLedger.name = addedLedger.name; matchingLedger.genesisId = addedLedger.genesisId; matchingLedger.symbol = addedLedger.symbol; matchingLedger.genesisHash = addedLedger.genesisHash; diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 24166291..f507d45c 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -1463,6 +1463,9 @@ export class Task { [JsonRpcMethod.AssetsVerifiedList]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.AssetsVerifiedList](request, sendResponse); }, + [JsonRpcMethod.AssetOptOut]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.AssetOptOut](request, sendResponse); + }, [JsonRpcMethod.SignSendTransaction]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.SignSendTransaction](request, sendResponse); }, diff --git a/packages/extension/src/background/transaction/actions.test.ts b/packages/extension/src/background/transaction/actions.test.ts index ecfbef6d..c4a50736 100644 --- a/packages/extension/src/background/transaction/actions.test.ts +++ b/packages/extension/src/background/transaction/actions.test.ts @@ -2,6 +2,7 @@ import { getValidatedTxnWrap } from './actions'; import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; import { AssetConfigTransaction } from './acfgTransaction'; import { AssetTransferTransaction } from './axferTransaction'; +import { AssetCloseTransaction } from './axferCloseTransaction'; import { AssetFreezeTransaction } from './afrzTransaction'; test('Validate build of pay transaction', () => { @@ -96,10 +97,33 @@ test('Validate build of axfer transaction', () => { }; const result = getValidatedTxnWrap(preTransaction, 'axfer'); + console.log(result) expect(result instanceof BaseValidatedTxnWrap).toBe(true); expect(result instanceof AssetTransferTransaction).toBe(true); }); +test('Validate build of axfer close transaction', () => { + const preTransaction = { + type: 'axfer', + from: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + to: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + closeRemainderTo: 'NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ', + fee: 1000, + assetIndex: 1, + amount: 12345, + firstRound: 1, + lastRound: 1001, + genesisID: 'testnet-v1.0', + genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', + note: new Uint8Array(0), + }; + + const result = getValidatedTxnWrap(preTransaction, 'axfer'); + console.log(result); + expect(result instanceof BaseValidatedTxnWrap).toBe(true); + expect(result instanceof AssetCloseTransaction).toBe(true); +}); + test('Validate build of transaction', () => { const preTransaction = { type: 'faketype', diff --git a/packages/extension/src/background/transaction/actions.ts b/packages/extension/src/background/transaction/actions.ts index fae12007..f413739f 100644 --- a/packages/extension/src/background/transaction/actions.ts +++ b/packages/extension/src/background/transaction/actions.ts @@ -5,6 +5,7 @@ import { IAssetDestroyTx } from '@algosigner/common/interfaces/acfg_destroy'; import { IAssetFreezeTx } from '@algosigner/common/interfaces/afrz'; import { IAssetTransferTx } from '@algosigner/common/interfaces/axfer'; import { IAssetAcceptTx } from '@algosigner/common/interfaces/axfer_accept'; +import { IAssetCloseTx } from '@algosigner/common/interfaces/axfer_close'; import { IAssetClawbackTx } from '@algosigner/common/interfaces/axfer_clawback'; import { IKeyRegistrationTx } from '@algosigner/common/interfaces/keyreg'; import { IApplTx } from '@algosigner/common/interfaces/appl'; @@ -15,6 +16,7 @@ import { AssetDestroyTransaction } from './acfgDestroyTransaction'; import { AssetFreezeTransaction } from './afrzTransaction'; import { AssetTransferTransaction } from './axferTransaction'; import { AssetAcceptTransaction } from './axferAcceptTransaction'; +import { AssetCloseTransaction } from './axferCloseTransaction'; import { AssetClawbackTransaction } from './axferClawbackTransaction'; import { KeyregTransaction } from './keyregTransaction'; import { ApplTransaction } from './applTransaction'; @@ -76,13 +78,21 @@ export function getValidatedTxnWrap( validatedTxnWrap = new AssetFreezeTransaction(txn as IAssetFreezeTx, v1Validations); break; case TransactionType.Axfer: - // Validate any of the 3 types of transactions that can occur with axfer + // Validate any of the 4 types of transactions that can occur with axfer // Use the first error as the passback error. try { validatedTxnWrap = new AssetAcceptTransaction(txn as IAssetAcceptTx, v1Validations); } catch (e) { error = e; } + if (!validatedTxnWrap) { + try { + validatedTxnWrap = new AssetCloseTransaction(txn as IAssetCloseTx, v1Validations); + } catch (e) { + e.message = [error.message, e.message].join(' '); + error = e; + } + } if (!validatedTxnWrap) { try { validatedTxnWrap = new AssetTransferTransaction(txn as IAssetTransferTx, v1Validations); diff --git a/packages/extension/src/background/transaction/axferCloseTransaction.ts b/packages/extension/src/background/transaction/axferCloseTransaction.ts new file mode 100644 index 00000000..6ca40c03 --- /dev/null +++ b/packages/extension/src/background/transaction/axferCloseTransaction.ts @@ -0,0 +1,43 @@ +import { IAssetCloseTx } from '@algosigner/common/interfaces/axfer_close'; +import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; +import { InvalidTransactionStructure } from '../../errors/validation'; + +/// +// Base implementation of the transactions type interface, for use in the export wrapper class below. +/// +class AssetCloseTx implements IAssetCloseTx { + type: string = undefined; + assetIndex: number = undefined; + from: string = undefined; + fee?: number = 0; + to: any = undefined; + closeRemainderTo: string = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: string = null; + lease?: any = null; + reKeyTo?: any = null; + amount?: BigInt = null; + flatFee?: any = null; + name?: string = null; + tag?: string = null; +} + +/// +// Mapping, validation and error checking for axfer accept transactions prior to sign. +/// +export class AssetCloseTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Asset Opt-Out'; + constructor(params: IAssetCloseTx, v1Validations: boolean) { + super(params, AssetCloseTx, v1Validations); + // Additional check to verify that address from and to are the same + if (this.transaction && this.transaction['to'] !== this.transaction['from']) { + throw new InvalidTransactionStructure( + `Creation of AssetCloseTx has non identical to and from fields.` + ); + } + } +} diff --git a/packages/test-project/tests/basic-e2e-dapp.test.js b/packages/test-project/tests/basic-e2e-dapp.test.js index 863405b9..3bf8b175 100644 --- a/packages/test-project/tests/basic-e2e-dapp.test.js +++ b/packages/test-project/tests/basic-e2e-dapp.test.js @@ -438,9 +438,9 @@ describe('dApp POST Txn Tests (plus Teal compile)', () => { var popup = pages[pages.length - 1]; await appPage.waitForTimeout(500); await popup.waitForSelector('#txAlerts'); - await expect( - popup.$eval('#danger-tx-list b', (e) => e.innerText) - ).resolves.toContain('Deprecate'); + await expect(popup.$eval('#danger-tx-list', (e) => e.innerText)).resolves.toContain( + 'Deprecated' + ); await popup.waitForSelector('#approveTx'); await popup.click('#approveTx', { waitUntil: 'networkidle' }); await popup.waitForSelector('#enterPassword'); diff --git a/packages/test-project/tests/common/tests.js b/packages/test-project/tests/common/tests.js index e9476117..169781e3 100644 --- a/packages/test-project/tests/common/tests.js +++ b/packages/test-project/tests/common/tests.js @@ -65,7 +65,7 @@ function VerifyAccount(account) { await expect(extensionPage.$eval('#accountAddress', (e) => e.innerText)).resolves.toBe( account.address ); - await closeModal(); + await goBack(); await goBack(); }); } diff --git a/packages/test-project/tests/ui-networks-e2e.test.js b/packages/test-project/tests/ui-networks-e2e.test.js index 04dce1c8..70c57bb1 100644 --- a/packages/test-project/tests/ui-networks-e2e.test.js +++ b/packages/test-project/tests/ui-networks-e2e.test.js @@ -98,16 +98,6 @@ describe('Create and Test Custom Networks', () => { test('Test Deleting Networks', async () => { await openNetworkMenu(); - // Delete E2ENet - await extensionPage.waitForSelector(e2eNetSelector); - await extensionPage.click(e2eNetSelector); - await extensionPage.click('#deleteNetwork'); - await inputPassword(); - - // Check network was deleted - await openNetworkMenu(); - await expect(extensionPage.select(e2eNetSelector)).rejects.toThrow(); - // Delete OtherNet await extensionPage.waitForSelector(otherNetSelector); await extensionPage.click(otherNetSelector); diff --git a/packages/ui/src/components/Account/AccountDetails.test.ts b/packages/ui/src/components/Account/AccountDetails.test.ts deleted file mode 100644 index 8f0a3b94..00000000 --- a/packages/ui/src/components/Account/AccountDetails.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { shallow } from 'enzyme'; -import { html } from 'htm/preact'; -import AccountDetails from './AccountDetails'; - -let component; -const account = { - address: "PBZHOKKNBUCCDJB7KB2KLHUMWCGAMBXZKGBFGGBHYNNXFIBOYI7ONYBWK4" -} -const ledger = "TestNet"; - -describe('AccountDetails', () => { - beforeEach(() => { - component = shallow(html` - <${AccountDetails} account=${account} ledger=${ledger} /> - `); - }); - - it('should display account address', () => { - expect(component.contains(html`

${account.address}

`)).toBe(true); - }); - - it('should display account address QR', () => { - expect(component.find('#accountQR').exists()).toBe(true); - }); - - it('should match snapshot', () => { - expect(component).toMatchSnapshot(); - }); -}); diff --git a/packages/ui/src/components/Account/AccountDetails.ts b/packages/ui/src/components/Account/AccountDetails.ts deleted file mode 100644 index ff528858..00000000 --- a/packages/ui/src/components/Account/AccountDetails.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { FunctionalComponent } from 'preact'; -import { html } from 'htm/preact'; -import { useContext, useState } from 'preact/hooks'; -import { route } from 'preact-router'; -import qrcode from 'qrcode-generator'; -import { JsonRpcMethod } from '@algosigner/common/messaging/types'; - -import { sendMessage } from 'services/Messaging'; -import { StoreContext } from 'services/StoreContext'; - -import Authenticate from 'components/Authenticate'; -import ToClipboard from 'components/ToClipboard'; - -const AccountDetails: FunctionalComponent = (props: any) => { - const { account, ledger } = props; - const store: any = useContext(StoreContext); - const [deleting, setDeleting] = useState(false); - const [loading, setLoading] = useState(false); - const [authError, setAuthError] = useState(''); - - const deleteAccount = (pwd: string) => { - const params = { - ledger: ledger, - address: account.address, - passphrase: pwd, - }; - setLoading(true); - setAuthError(''); - sendMessage(JsonRpcMethod.DeleteAccount, params, function (response) { - if ('error' in response) { - setLoading(false); - switch (response.error) { - case 'Login Failed': - setAuthError('Wrong passphrase'); - break; - default: - setDeleting(false); - alert(`There was an unkown error: ${response.error}`); - break; - } - } else { - store.updateWallet(response, () => { - route('/wallet'); - }); - } - }); - }; - - const typeNumber: TypeNumber = 4; - const errorCorrectionLevel: ErrorCorrectionLevel = 'L'; - const qr = qrcode(typeNumber, errorCorrectionLevel); - qr.addData(account.address); - qr.make(); - const qrImg = qr.createDataURL(10, 1); - - if (!deleting) - return html` -
- Address (<${ToClipboard} data=${account.address} />) -

${account.address}

- -
- -
- - -
- `; - else - return html` - <${Authenticate} error=${authError} loading=${loading} nextStep=${deleteAccount} /> - `; -}; - -export default AccountDetails; diff --git a/packages/ui/src/components/Account/AssetDetails.ts b/packages/ui/src/components/Account/AssetDetails.ts index e85a8610..dcf61ee2 100644 --- a/packages/ui/src/components/Account/AssetDetails.ts +++ b/packages/ui/src/components/Account/AssetDetails.ts @@ -8,7 +8,7 @@ import { sendMessage } from 'services/Messaging'; import ToClipboard from 'components/ToClipboard'; const AssetDetails: FunctionalComponent = (props: any) => { - const { asset, ledger } = props; + const { asset, ledger, optOutFn } = props; const [, setResults] = useState(0); const fetchApi = async () => { @@ -74,7 +74,7 @@ const AssetDetails: FunctionalComponent = (props: any) => { ${asset.creator.slice(0, 8)}.....${asset.creator.slice(-8)}

-
+
{ > See details in GoalSeeker + ${!(asset.amount > 0) && html`Opt-out of this asset`}
`}
diff --git a/packages/ui/src/components/Account/AssetsList.ts b/packages/ui/src/components/Account/AssetsList.ts index 9d12095a..e33e8949 100644 --- a/packages/ui/src/components/Account/AssetsList.ts +++ b/packages/ui/src/components/Account/AssetsList.ts @@ -2,7 +2,11 @@ import { FunctionalComponent } from 'preact'; import { html } from 'htm/preact'; import { useState } from 'preact/hooks'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; +import { sendMessage } from 'services/Messaging'; + import AssetDetails from 'components/Account/AssetDetails'; +import Authenticate from 'components/Authenticate'; const AssetPreview: FunctionalComponent = (props: any) => { const { asset, setShowAsset } = props; @@ -27,8 +31,7 @@ const AssetPreview: FunctionalComponent = (props: any) => { ${asset.name} ${asset['asset-id']} `} - ${(!asset.name || asset.name.length === 0) && - html` ${asset['asset-id']} `} + ${(!asset.name || asset.name.length === 0) && html` ${asset['asset-id']} `} ${getAmount()} ${asset['unit-name'] && @@ -40,30 +43,55 @@ const AssetPreview: FunctionalComponent = (props: any) => { }; const AssetsList: FunctionalComponent = (props: any) => { - const { ledger, assets } = props; + const { ledger, assets, address } = props; const [showAsset, setShowAsset] = useState(null); const [fullList, setFullList] = useState(false); + const [askAuth, setAskAuth] = useState(false); + const [authError, setAuthError] = useState(''); + const [loading, setLoading] = useState(false); + + const sendOptOut = async (pwd: string) => { + setLoading(true); + setAuthError(''); + const params = { + ledger: ledger, + passphrase: pwd, + address: address, + id: showAsset['asset-id'], + }; + sendMessage(JsonRpcMethod.AssetOptOut, params, function (response) { + setLoading(false); + if ('error' in response) { + switch (response.error) { + case 'Login Failed': + setAuthError('Wrong passphrase'); + break; + default: + setAuthError(response.error); + break; + } + } else { + setAskAuth(false); + setShowAsset(null); + setFullList(false); + } + }); + }; // Only show the first 10 assets, and a link to the assets view // if needed. return html` ${assets .slice(0, 10) - .map( - (asset: any) => html` - <${AssetPreview} asset=${asset} setShowAsset=${setShowAsset} /> - ` - )} + .map((asset: any) => html`<${AssetPreview} asset=${asset} setShowAsset=${setShowAsset} />`)} ${assets.length > 10 && html` `} @@ -72,31 +100,36 @@ const AssetsList: FunctionalComponent = (props: any) => { - - -`; diff --git a/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap b/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap index 5bbeb8df..43289e96 100644 --- a/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap +++ b/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap @@ -77,7 +77,7 @@ exports[`AssetDetails should match snapshot 1`] = `

{ @@ -227,14 +228,14 @@ const ContactList: FunctionalComponent = () => { onClick=${() => openEditModal(c.name, c.address)} > - + <${ContactPreview} contact="${c}" diff --git a/packages/ui/src/components/LedgerNetworkModify.ts b/packages/ui/src/components/LedgerNetworkModify.ts index 7bd3d415..88327c9b 100644 --- a/packages/ui/src/components/LedgerNetworkModify.ts +++ b/packages/ui/src/components/LedgerNetworkModify.ts @@ -26,6 +26,8 @@ const LedgerNetworkModify: FunctionalComponent = (props: any) => { const [networkHeaders, setNetworkHeaders] = useState(props.headers || ''); const [checkStatus, setCheckStatus] = useState('gray'); + const previousName = props.name ? props.name : ''; + const deleteNetwork = (pwd: string) => { setLoading(true); setAuthError(''); @@ -94,6 +96,7 @@ const LedgerNetworkModify: FunctionalComponent = (props: any) => { setError(''); const params = { name: networkName, + previousName: previousName, genesisId: networkId, symbol: networkSymbol, algodUrl: networkAlgodUrl, diff --git a/packages/ui/src/components/SignTransaction/Common/TxTemplate.ts b/packages/ui/src/components/SignTransaction/Common/TxTemplate.ts index 1931599c..b24723e5 100644 --- a/packages/ui/src/components/SignTransaction/Common/TxTemplate.ts +++ b/packages/ui/src/components/SignTransaction/Common/TxTemplate.ts @@ -9,15 +9,12 @@ const TxTemplate: FunctionalComponent = (props: any) => { const { tx, vo, account, msig, midsection, overview } = props; const txText = JSON.stringify(tx, null, 2); - const tabsStyle = 'height: 170px; overflow: auto;'; + const tabsStyle = 'height: 160px; overflow: auto;'; return html` - ${vo && - html` - <${TxAlert} vo=${vo} /> - `} + ${vo && html`<${TxAlert} vo=${vo} />`}
-
+
diff --git a/packages/ui/src/components/SignTransaction/TxAfrz.ts b/packages/ui/src/components/SignTransaction/TxAfrz.ts index 8a79ab95..de890b46 100644 --- a/packages/ui/src/components/SignTransaction/TxAfrz.ts +++ b/packages/ui/src/components/SignTransaction/TxAfrz.ts @@ -9,18 +9,14 @@ const TxAfrz: FunctionalComponent = (props: any) => { const state = tx.freezeState ? 'Freeze' : 'Unfreeze'; - let assetIndex = html` -

- ${tx.assetIndex} -

- `; + let assetIndex = html`

${tx.assetIndex}

`; if (isLedgerBaseSupported(ledger)) { assetIndex = html` @@ -35,7 +31,7 @@ const TxAfrz: FunctionalComponent = (props: any) => { Asset ${state}
-
+
${tx.freezeAccount} diff --git a/packages/ui/src/components/SignTransaction/TxAxfer.ts b/packages/ui/src/components/SignTransaction/TxAxfer.ts index c954ce8b..6435dddd 100644 --- a/packages/ui/src/components/SignTransaction/TxAxfer.ts +++ b/packages/ui/src/components/SignTransaction/TxAxfer.ts @@ -30,11 +30,11 @@ const TxAxfer: FunctionalComponent = (props: any) => { ${dt || 'Asset Transfer'}

- + ${contact && html`<${ContactPreview} contact="${contact}" className="mt-2" />`} ${!contact && html` -
+
${tx.to} diff --git a/packages/ui/src/components/SignTransaction/TxPay.ts b/packages/ui/src/components/SignTransaction/TxPay.ts index e588b29b..187ec51f 100644 --- a/packages/ui/src/components/SignTransaction/TxPay.ts +++ b/packages/ui/src/components/SignTransaction/TxPay.ts @@ -15,16 +15,18 @@ const TxPay: FunctionalComponent = (props: any) => {

${contact && html`<${ContactPreview} contact="${contact}" className="mt-2" />`} - ${!contact && - html` -
-
-
- ${tx.to} + ${ + !contact && + html` +
+
+
+ ${tx.to} +
-
- `} + ` + } `; const overview = html` diff --git a/packages/ui/src/index.js b/packages/ui/src/index.js index 98b842c2..a71268f9 100644 --- a/packages/ui/src/index.js +++ b/packages/ui/src/index.js @@ -17,6 +17,7 @@ import CreateAccount from 'pages/CreateAccount'; import ImportAccount from 'pages/ImportAccount'; import Wallet from 'pages/Wallet'; import Account from 'pages/Account'; +import AccountDetails from 'pages/AccountDetails'; import SendAlgos from 'pages/SendAlgos'; import AddAsset from 'pages/AddAsset'; import SignTransaction from 'pages/SignTransaction'; @@ -62,6 +63,7 @@ const App = () => { <${CreateAccount} path="/:ledger/create-account" /> <${ImportAccount} path="/:ledger/import-account" /> <${Account} path="/:ledger/:address" /> + <${AccountDetails} path="/:ledger/:address/details" /> <${AddAsset} path="/:ledger/:address/add-asset" /> <${SendAlgos} path="/:ledger/:address/send" /> <${LedgerHardwareConnector} path="/:ledger/ledger-hardware-connector" /> diff --git a/packages/ui/src/pages/Account.ts b/packages/ui/src/pages/Account.ts index 81669391..8577cc6d 100644 --- a/packages/ui/src/pages/Account.ts +++ b/packages/ui/src/pages/Account.ts @@ -10,7 +10,6 @@ import { numFormat } from 'services/common'; import { StoreContext } from 'services/StoreContext'; import TransactionsList from 'components/Account/TransactionsList'; import AssetsList from 'components/Account/AssetsList'; -import AccountDetails from 'components/Account/AccountDetails'; import algo from 'assets/algo.png'; const Account: FunctionalComponent = (props: any) => { @@ -18,9 +17,15 @@ const Account: FunctionalComponent = (props: any) => { const { url, ledger, address } = props; const [account, setAccount] = useState({}); const [details, setDetails] = useState({}); - const [showDetails, setShowDetails] = useState(false); const [error, setError] = useState(null); + const rewardsTooltip = + details && + `Algos: ${numFormat( + details['amount-without-pending-rewards'] / 1e6, + 6 + )}\nRewards: ${numFormat(details['pending-rewards'] / 1e6, 6)}`; + useEffect(() => { for (let i = store[ledger].length - 1; i >= 0; i--) { if (store[ledger][i].address === address) { @@ -38,11 +43,10 @@ const Account: FunctionalComponent = (props: any) => { address: address, }; sendMessage(JsonRpcMethod.AccountDetails, params, function (response) { - if(response.error){ + if (response.error) { console.error(response.error); setError('Error: Account details not accessible.'); - } - else { + } else { setDetails(response); store.updateAccountDetails(ledger, response); } @@ -57,15 +61,15 @@ const Account: FunctionalComponent = (props: any) => { -

${account.name}

+

${account.name}

-

+
${details === null && error && html`${error}`} @@ -74,6 +78,12 @@ const Account: FunctionalComponent = (props: any) => { html` ${numFormat(details.amount / 1e6, 6)} Algos + + ` } @@ -95,28 +105,11 @@ const Account: FunctionalComponent = (props: any) => { details && details.assets && details.assets.length > 0 && - html` <${AssetsList} assets=${details.assets} ledger=${ledger} /> ` + html`<${AssetsList} assets=${details.assets} ledger=${ledger} address=${address} />` }
<${TransactionsList} address=${address} ledger=${ledger}/> - - ${ - showDetails && - html` - - ` - } `; }; diff --git a/packages/ui/src/pages/AccountDetails.ts b/packages/ui/src/pages/AccountDetails.ts new file mode 100644 index 00000000..dd400198 --- /dev/null +++ b/packages/ui/src/pages/AccountDetails.ts @@ -0,0 +1,215 @@ +import { FunctionalComponent } from 'preact'; +import { html } from 'htm/preact'; +import { useContext, useState, useEffect } from 'preact/hooks'; +import { route } from 'preact-router'; +import qrcode from 'qrcode-generator'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; + +import { sendMessage } from 'services/Messaging'; +import { StoreContext } from 'services/StoreContext'; + +import Authenticate from 'components/Authenticate'; +import ToClipboard from 'components/ToClipboard'; + +const AccountDetails: FunctionalComponent = (props: any) => { + const { ledger, address } = props; + const store: any = useContext(StoreContext); + const [details, setDetails] = useState({}); + const [modal, setModal] = useState(''); + const [loading, setLoading] = useState(false); + const [authError, setAuthError] = useState(''); + + const deleteAccount = (pwd: string) => { + const params = { + ledger: ledger, + address: address, + passphrase: pwd, + }; + setLoading(true); + setAuthError(''); + sendMessage(JsonRpcMethod.DeleteAccount, params, function (response) { + if ('error' in response) { + setLoading(false); + switch (response.error) { + case 'Login Failed': + setAuthError('Wrong passphrase'); + break; + default: + setModal(''); + alert(`There was an unkown error: ${response.error}`); + break; + } + } else { + store.updateWallet(response, () => { + route('/wallet'); + }); + } + }); + }; + + useEffect(() => { + for (let i = store[ledger].length - 1; i >= 0; i--) { + if (store[ledger][i].address === address) { + setDetails(store[ledger][i].details); + break; + } + } + }, []); + + const typeNumber: TypeNumber = 4; + const errorCorrectionLevel: ErrorCorrectionLevel = 'L'; + const qr = qrcode(typeNumber, errorCorrectionLevel); + qr.addData(address); + qr.make(); + const qrImg = qr.createDataURL(10, 1); + const ledgerName = ledger.toLowerCase(); + + return html` +
+
+ route(`/${ledger}/${address}`)} + > + + + + +

+ Address: +
+ ${address} + (<${ToClipboard} data=${address} />) +

+
+
+
+ ${details && + details['apps-local-state'] && + details['apps-local-state'].length > 0 && + html` + Opted-in Apps +
+ ${details['apps-local-state'].map( + (oia) => html` + + ${oia.id} + + + + + ` + )} +
+ `} + ${details && + details['created-assets'] && + details['created-assets'].length > 0 && + html` + Created Assets +
+ ${details['created-assets'].map( + (cas) => html` + + ${cas.params.name && + cas.params.name.length > 0 && + html` + ${cas.params.name} + ${cas.index} + `} + ${(!cas.params.name || cas.params.name.length === 0) && html`${cas.index}`} + + + + + ` + )} +
+ `} + ${details && + details['created-apps'] && + details['created-apps'].length > 0 && + html` + Created Apps +
+ ${details['created-apps'].map( + (cap) => html` + + ${cap.id} + + + + + ` + )} +
+ `} +
+
+ + +
+ ${modal && + html` + + `} + `; +}; + +export default AccountDetails; diff --git a/packages/ui/src/pages/SignWalletTransaction.ts b/packages/ui/src/pages/SignWalletTransaction.ts index 456325ca..d6390c22 100644 --- a/packages/ui/src/pages/SignWalletTransaction.ts +++ b/packages/ui/src/pages/SignWalletTransaction.ts @@ -219,9 +219,9 @@ const SignWalletTransaction: FunctionalComponent = () => { const getWrapUI = (wrap, account) => { const to = wrap.transaction.to; - const contact = to ? contacts.find((c) => (c.address === to)) : null; + const contact = to ? contacts.find((c) => c.address === to) : null; return html` -
+
${wrap.transaction.type === 'pay' && html` <${TxPay} @@ -305,10 +305,9 @@ const SignWalletTransaction: FunctionalComponent = () => { >
- ${totalGroups > 1 && html` - - Signing group ${currentGroup} out of ${totalGroups} - + ${totalGroups > 1 && + html` + Signing group ${currentGroup} out of ${totalGroups} `}
${request.body && @@ -371,7 +370,10 @@ const SignWalletTransaction: FunctionalComponent = () => {
${getWrapUI(transactionWraps[activeTx], accountNames[activeTx])} -
+
${approvedAmount} out of ${transactionWraps.length} transactions approved. { const { ledger } = store; return html`
-
+
${ store[ledger] && store[ledger].length !== 0 &&