From 70df7b9dd7435ca906bb1516971103e36faedb59 Mon Sep 17 00:00:00 2001 From: Tim Baldwin Date: Tue, 22 Sep 2020 11:07:44 -0400 Subject: [PATCH 01/45] Switched from created to released for zip workflow --- .github/workflows/zip-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zip-release.yml b/.github/workflows/zip-release.yml index 4976ba5c..aaa6946c 100644 --- a/.github/workflows/zip-release.yml +++ b/.github/workflows/zip-release.yml @@ -2,7 +2,7 @@ name: Deployment on: release: types: - - created + - released jobs: test_zip_upload: runs-on: ubuntu-latest From b2347ed2814cc2423cf5835bd6162b135da14f76 Mon Sep 17 00:00:00 2001 From: purestaketdb <45097828+purestaketdb@users.noreply.github.com> Date: Fri, 2 Oct 2020 10:20:55 -0400 Subject: [PATCH 02/45] Adding new code scanning action --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..18355e7c --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [develop, master, release] + pull_request: + # The branches below must be a subset of the branches above + branches: [develop] + schedule: + - cron: '0 16 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 83783916586c3289f2ea17a8c5169bc1c72328ba Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 5 Oct 2020 17:04:09 +0200 Subject: [PATCH 03/45] Moved Store out of index --- .../src/components/Account/AccountDetails.ts | 3 +- packages/ui/src/components/LedgerSelect.ts | 2 +- packages/ui/src/components/WalletDetails.ts | 2 +- packages/ui/src/index.js | 69 ++----------------- packages/ui/src/pages/Account.ts | 2 +- packages/ui/src/pages/Authorize.ts | 2 +- packages/ui/src/pages/CreateAccount.ts | 2 +- packages/ui/src/pages/ImportAccount.ts | 2 +- packages/ui/src/pages/Login.ts | 2 +- packages/ui/src/pages/SendAlgos.ts | 2 +- packages/ui/src/pages/SetPassword.ts | 2 +- packages/ui/src/pages/SignTransaction.ts | 2 +- packages/ui/src/pages/Wallet.ts | 2 +- packages/ui/src/services/StoreContext.ts | 63 +++++++++++++++++ 14 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 packages/ui/src/services/StoreContext.ts diff --git a/packages/ui/src/components/Account/AccountDetails.ts b/packages/ui/src/components/Account/AccountDetails.ts index 874a06ae..7354ef3f 100644 --- a/packages/ui/src/components/Account/AccountDetails.ts +++ b/packages/ui/src/components/Account/AccountDetails.ts @@ -6,7 +6,7 @@ import qrcode from 'qrcode-generator'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import Authenticate from 'components/Authenticate' @@ -59,6 +59,7 @@ const AccountDetails: FunctionalComponent = (props: any) => {
0) - // Remove # from hash - hashPath = window.location.hash.slice(2); - if ('session' in response) { - store.updateWallet(response.session.wallet); - store.setLedger(response.session.ledger); - if (hashPath.length > 0) { - route(`/${hashPath}`) - } else { - route('/wallet') - } - } else { - route('/login/'+hashPath); - } - } - }); - - return html` - <${StoreContext.Provider} value=${store}>${children} - ` -}; - +import { StoreProvider } from 'services/StoreContext' require('./styles.scss'); diff --git a/packages/ui/src/pages/Account.ts b/packages/ui/src/pages/Account.ts index 044a5784..0f49eb64 100644 --- a/packages/ui/src/pages/Account.ts +++ b/packages/ui/src/pages/Account.ts @@ -8,7 +8,7 @@ import { Link, route } from 'preact-router'; import { sendMessage } from 'services/Messaging' import { numFormat } from 'services/common'; -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import TransactionsList from 'components/Account/TransactionsList' import AssetsList from 'components/Account/AssetsList' import AccountDetails from 'components/Account/AccountDetails' diff --git a/packages/ui/src/pages/Authorize.ts b/packages/ui/src/pages/Authorize.ts index 29aba488..f8d7f587 100644 --- a/packages/ui/src/pages/Authorize.ts +++ b/packages/ui/src/pages/Authorize.ts @@ -4,7 +4,7 @@ import { useEffect, useState, useContext } from 'preact/hooks'; import { Link } from 'preact-router'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { StoreContext } from '../index' +import { StoreContext } from 'services/StoreContext' import logotype from 'assets/logotype.png' diff --git a/packages/ui/src/pages/CreateAccount.ts b/packages/ui/src/pages/CreateAccount.ts index d0a26a49..14132475 100644 --- a/packages/ui/src/pages/CreateAccount.ts +++ b/packages/ui/src/pages/CreateAccount.ts @@ -8,7 +8,7 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import SetAccountName from 'components/CreateAccount/SetAccountName' import AccountKeys from 'components/CreateAccount/AccountKeys' diff --git a/packages/ui/src/pages/ImportAccount.ts b/packages/ui/src/pages/ImportAccount.ts index 66d0b0fa..abff4d7f 100644 --- a/packages/ui/src/pages/ImportAccount.ts +++ b/packages/ui/src/pages/ImportAccount.ts @@ -4,7 +4,7 @@ import { useState, useContext } from 'preact/hooks'; import { route } from 'preact-router'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; -import { StoreContext } from '../index' +import { StoreContext } from 'services/StoreContext' import HeaderView from 'components/HeaderView' import Authenticate from 'components/Authenticate' diff --git a/packages/ui/src/pages/Login.ts b/packages/ui/src/pages/Login.ts index 27ee9719..46ebe27a 100644 --- a/packages/ui/src/pages/Login.ts +++ b/packages/ui/src/pages/Login.ts @@ -6,7 +6,7 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import background from 'assets/background.png'; import logo from 'assets/logo-inverted.svg'; diff --git a/packages/ui/src/pages/SendAlgos.ts b/packages/ui/src/pages/SendAlgos.ts index 85a262ce..78c30e6f 100644 --- a/packages/ui/src/pages/SendAlgos.ts +++ b/packages/ui/src/pages/SendAlgos.ts @@ -6,7 +6,7 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import HeaderView from 'components/HeaderView' import Authenticate from 'components/Authenticate' diff --git a/packages/ui/src/pages/SetPassword.ts b/packages/ui/src/pages/SetPassword.ts index 47f1b8a4..58f979bc 100644 --- a/packages/ui/src/pages/SetPassword.ts +++ b/packages/ui/src/pages/SetPassword.ts @@ -6,7 +6,7 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import background from 'assets/background.png'; import walletLock from 'assets/wallet-lock.png'; diff --git a/packages/ui/src/pages/SignTransaction.ts b/packages/ui/src/pages/SignTransaction.ts index 6a21c8ad..539c1a56 100644 --- a/packages/ui/src/pages/SignTransaction.ts +++ b/packages/ui/src/pages/SignTransaction.ts @@ -12,7 +12,7 @@ import TxAxfer from 'components/SignTransaction/TxAxfer' import TxAfrz from 'components/SignTransaction/TxAfrz' import Authenticate from 'components/Authenticate' import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import logotype from 'assets/logotype.png' function deny() { diff --git a/packages/ui/src/pages/Wallet.ts b/packages/ui/src/pages/Wallet.ts index 1380fc5c..49e18b37 100644 --- a/packages/ui/src/pages/Wallet.ts +++ b/packages/ui/src/pages/Wallet.ts @@ -4,7 +4,7 @@ import { useState, useContext } from 'preact/hooks'; import { useObserver } from 'mobx-react-lite'; import { Link } from 'preact-router'; -import { StoreContext } from 'index' +import { StoreContext } from 'services/StoreContext' import AccountPreview from 'components/AccountPreview' diff --git a/packages/ui/src/services/StoreContext.ts b/packages/ui/src/services/StoreContext.ts new file mode 100644 index 00000000..b668be23 --- /dev/null +++ b/packages/ui/src/services/StoreContext.ts @@ -0,0 +1,63 @@ +import { createContext } from 'preact'; +import { html } from 'htm/preact'; +import { useLocalStore } from 'mobx-react-lite'; +import { route } from 'preact-router'; +import { autorun } from 'mobx'; + +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; +import { sendMessage } from 'services/Messaging' + +export const StoreContext = createContext(undefined); + +export const StoreProvider = ({children}) => { + const existingStore = sessionStorage.getItem('wallet'); + const store = useLocalStore(() => ({ + ledger: 'MainNet', + TestNet: {}, + MainNet: {}, + savedRequest: null, + setLedger: (ledger) => { + store.ledger = ledger; + }, + updateWallet: (newWallet) => { + store.TestNet = newWallet.TestNet; + store.MainNet = newWallet.MainNet; + }, + saveRequest: (request) => { + store.savedRequest = request; + }, + clearSavedRequest: () => { + delete store.savedRequest; + }, + })); + + autorun(() => { + sessionStorage.setItem('wallet', JSON.stringify(store)) + }) + + // Try to retrieve session from background + sendMessage(JsonRpcMethod.GetSession, {}, function(response) { + if (response && response.exist){ + let hashPath = ""; + if (window.location.hash.length > 0) { + // Remove # from hash + hashPath = window.location.hash.slice(2); + } + if ('session' in response) { + store.updateWallet(response.session.wallet); + store.setLedger(response.session.ledger); + if (hashPath.length > 0) { + route(`/${hashPath}`) + } else { + route('/wallet') + } + } else { + route('/login/'+hashPath); + } + } + }); + + return html` + <${StoreContext.Provider} value=${store}>${children} + ` +}; \ No newline at end of file From a1df6f355cde38ca498afe1720cf29278798ff08 Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 5 Oct 2020 17:04:31 +0200 Subject: [PATCH 04/45] First pass at UI testing --- packages/ui/jest.config.js | 4 +- packages/ui/package.json | 4 +- .../components/Account/AccountDetails.test.ts | 44 +++++---- .../components/Account/AssetDetails.test.ts | 38 ++++++++ .../__snapshots__/AccountDetails.test.ts.snap | 39 ++++++++ .../__snapshots__/AssetDetails.test.ts.snap | 89 +++++++++++++++++++ .../TransactionDetail/TxAcfg.test.ts | 67 ++++++++++++++ .../components/TransactionDetail/TxAcfg.ts | 8 +- .../TransactionDetail/TxAfrz.test.ts | 64 +++++++++++++ .../components/TransactionDetail/TxAfrz.ts | 10 +-- .../TransactionDetail/TxAxfer.test.ts | 65 ++++++++++++++ .../components/TransactionDetail/TxAxfer.ts | 10 +-- .../TransactionDetail/TxPay.test.ts | 64 +++++++++++++ .../src/components/TransactionDetail/TxPay.ts | 10 +-- .../__snapshots__/TxAcfg.test.ts.snap | 70 +++++++++++++++ .../__snapshots__/TxAfrz.test.ts.snap | 78 ++++++++++++++++ .../__snapshots__/TxAxfer.test.ts.snap | 70 +++++++++++++++ .../__snapshots__/TxPay.test.ts.snap | 71 +++++++++++++++ packages/ui/src/pages/Login.test.ts | 36 ++++++++ .../pages/__snapshots__/Login.test.ts.snap | 56 ++++++++++++ 20 files changed, 859 insertions(+), 38 deletions(-) create mode 100644 packages/ui/src/components/Account/AssetDetails.test.ts create mode 100644 packages/ui/src/components/Account/__snapshots__/AccountDetails.test.ts.snap create mode 100644 packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap create mode 100644 packages/ui/src/components/TransactionDetail/TxAcfg.test.ts create mode 100644 packages/ui/src/components/TransactionDetail/TxAfrz.test.ts create mode 100644 packages/ui/src/components/TransactionDetail/TxAxfer.test.ts create mode 100644 packages/ui/src/components/TransactionDetail/TxPay.test.ts create mode 100644 packages/ui/src/components/TransactionDetail/__snapshots__/TxAcfg.test.ts.snap create mode 100644 packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap create mode 100644 packages/ui/src/components/TransactionDetail/__snapshots__/TxAxfer.test.ts.snap create mode 100644 packages/ui/src/components/TransactionDetail/__snapshots__/TxPay.test.ts.snap create mode 100644 packages/ui/src/pages/Login.test.ts create mode 100644 packages/ui/src/pages/__snapshots__/Login.test.ts.snap diff --git a/packages/ui/jest.config.js b/packages/ui/jest.config.js index 432cc1c5..c001e11e 100644 --- a/packages/ui/jest.config.js +++ b/packages/ui/jest.config.js @@ -16,9 +16,6 @@ module.exports = { "**/__tests__/**/*.[jt]s?(x)", "**/?(*.)(spec|test).[jt]s?(x)" ], - setupFiles: [ - "jest-webextension-mock" - ], moduleNameMapper: { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", "\\.(css|less|scss)$": "identity-obj-proxy", @@ -34,4 +31,5 @@ module.exports = { "roots": [ "/src" ], + "snapshotSerializers": ["enzyme-to-json/serializer"] } diff --git a/packages/ui/package.json b/packages/ui/package.json index 44ab46b7..3c710ac4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,7 +7,8 @@ "scripts": { "start": "webpack-dev-server", "build": "webpack --progress", - "test": "" + "test": "jest", + "test-watch": "jest --watch" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.14.0", @@ -29,6 +30,7 @@ "enzyme-adapter-preact-pure": "^2.2.3", "file-loader": "^6.1.0", "html-webpack-plugin": "^4.4.1", + "enzyme-to-json": "^3.6.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.4.2", "jest-webextension-mock": "^3.6.1", diff --git a/packages/ui/src/components/Account/AccountDetails.test.ts b/packages/ui/src/components/Account/AccountDetails.test.ts index 72d68eca..ce867684 100644 --- a/packages/ui/src/components/Account/AccountDetails.test.ts +++ b/packages/ui/src/components/Account/AccountDetails.test.ts @@ -1,15 +1,29 @@ -// import { shallow } from 'enzyme'; -// import { html } from 'htm/preact'; -// import AccountDetails from './AccountDetails'; - -// describe('AccountDetails', () => { -// it('should display account information', () => { -// const account = { -// address: "PBZHOKKNBUCCDJB7KB2KLHUMWCGAMBXZKGBFGGBHYNNXFIBOYI7ONYBWK4" -// } -// const ledger = "TestNet"; -// const component = shallow(html`<${AccountDetails} account=${account} ledger=${ledger} />`); - -// expect(component).toMatchSnapshot(); -// }); -// }); \ No newline at end of file +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/AssetDetails.test.ts b/packages/ui/src/components/Account/AssetDetails.test.ts new file mode 100644 index 00000000..f88448d1 --- /dev/null +++ b/packages/ui/src/components/Account/AssetDetails.test.ts @@ -0,0 +1,38 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import AssetDetails from './AssetDetails'; + +let component; +const asset = { + "amount": 1000, + "asset-id": 12007988, + "creator": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "is-frozen": false, + "unit-name": "DsG0I1", + "total": 1000, + "reserve": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "name": "Dm-1", + "manager": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "freeze": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "default-frozen": false, + "decimals": 0, + "clawback": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A" +}; + +const ledger = "TestNet"; + +describe('AssetDetails', () => { + beforeEach(() => { + component = shallow(html` + <${AssetDetails} asset=${asset} ledger=${ledger} /> + `); + }); + + it('should display asset name', () => { + expect(component.contains(html`${asset.name}`)).toBe(true); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/components/Account/__snapshots__/AccountDetails.test.ts.snap b/packages/ui/src/components/Account/__snapshots__/AccountDetails.test.ts.snap new file mode 100644 index 00000000..35112a9e --- /dev/null +++ b/packages/ui/src/components/Account/__snapshots__/AccountDetails.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountDetails should match snapshot 1`] = ` +
+ + Address + +

+ PBZHOKKNBUCCDJB7KB2KLHUMWCGAMBXZKGBFGGBHYNNXFIBOYI7ONYBWK4 +

+
+ +
+ +
+`; diff --git a/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap b/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap new file mode 100644 index 00000000..62f74c87 --- /dev/null +++ b/packages/ui/src/components/Account/__snapshots__/AssetDetails.test.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssetDetails should match snapshot 1`] = ` +
+
+ + Dm-1 + +
+ + 12007988 + +
+

+ + Your balance + + + + 1,000 + + + + DsG0I1 + + +

+
+

+ + Total units + + + + 1,000 + + + + DsG0I1 + + +

+

+ + Creator + + ( + + ) + + RCWKH27Q + ..... + CJGRNH7A + +

+ +
+`; diff --git a/packages/ui/src/components/TransactionDetail/TxAcfg.test.ts b/packages/ui/src/components/TransactionDetail/TxAcfg.test.ts new file mode 100644 index 00000000..555ea9aa --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/TxAcfg.test.ts @@ -0,0 +1,67 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import TxAcfg from './TxAcfg'; + +let component; +const tx = { + "asset-config-transaction": { + "asset-id": 0, + "params": { + "creator": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "decimals": 1, + "default-frozen": false, + "name": "non", + "total": 12999, + "unit-name": "non" + } + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 9265758, + "created-asset-index": 12249848, + "fee": 241000, + "first-valid": 9265746, + "genesis-hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=", + "genesis-id": "testnet-v1.0", + "id": "B2EMD3434ROUEZESQXI3C5TNUBUL6AZJXYEEE54BBRGBJ743SKLQ", + "intra-round-offset": 0, + "last-valid": 9266746, + "note": "bm90ZQ==", + "receiver-rewards": 0, + "round-time": 1600194561, + "sender": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "sender-rewards": 0, + "signature": { + "sig": "lO/DZnJHH2iS/gReqIHzyJS2A36oPGJqx6hvHu0s5Ie/Didp/OMfRFHP4eUHJmzjMLZ0kf0S+OShxSoiimVRDQ==" + }, + "tx-type": "acfg" +}; +const ledger = "TestNet"; + +describe('TxAcfg', () => { + beforeEach(() => { + component = shallow(html` + <${TxAcfg} tx=${tx} ledger=${ledger} /> + `); + }); + + it('should display tx id', () => { + expect(component.contains(html`${tx.id}`)).toBe(true); + }); + + it('should display tx from', () => { + expect(component.contains(html`${tx.sender}`)).toBe(true); + }); + + it('should display asset name', () => { + expect(component.contains(html`${tx['asset-config-transaction']['params']['name']}`)).toBe(true); + }); + + it('should display confirmed round', () => { + expect(component.contains(html`${tx['confirmed-round'].toString()}`)).toBe(true); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/components/TransactionDetail/TxAcfg.ts b/packages/ui/src/components/TransactionDetail/TxAcfg.ts index 8e98b0af..2c74c364 100644 --- a/packages/ui/src/components/TransactionDetail/TxAcfg.ts +++ b/packages/ui/src/components/TransactionDetail/TxAcfg.ts @@ -7,13 +7,13 @@ const TxAcfg: FunctionalComponent = (props: any) => { return html`

Asset configuration

-

TxID: ${tx.id}

-

From: ${tx.sender}

+

TxID: ${tx.id}

+

From: ${tx.sender}

${tx['asset-config-transaction']['params']['name'] && html` -

Asset name: ${tx['asset-config-transaction']['params']['name']}

+

Asset name: ${tx['asset-config-transaction']['params']['name']}

`}

Total: ${tx['asset-config-transaction']['params']['total']} ${tx['asset-config-transaction']['params']['unit-name']}

-

Block: ${tx['confirmed-round']}

+

Block: ${tx['confirmed-round']}

See details in GoalSeeker diff --git a/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts b/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts new file mode 100644 index 00000000..c072ae86 --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts @@ -0,0 +1,64 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import TxAfrz from './TxAfrz'; + +let component; +const tx = { + "asset-freeze-transaction": { + "address": "ZYQX7BZ6LGTD7UCS7J5RVEAKHUJPK3FNJFZV2GPUYS2TFIADVFHDBKTN7I", + "asset-id": 185, + "new-freeze-status": true + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 3276956, + "fee": 1000, + "first-valid": 3276927, + "genesis-hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=", + "id": "VH4LN4PICQPJNAZE6KVCNOYLE3K5XWWZH2OM3HWCSF2TOQU4MGNA", + "intra-round-offset": 0, + "last-valid": 3277927, + "note": "AnwdDiVabq0=", + "receiver-rewards": 0, + "round-time": 1574696199, + "sender": "WLH5LELVSEVQL45LBRQYCLJAX6KQPGWUY5WHJXVRV2NPYZUBQAFPH22Q7A", + "sender-rewards": 0, + "signature": { + "sig": "TtL+XMfgNcQ1H/Tgdc11yFjLKm+dZ8PskkfTkxwgUih8FS4LW+ADQnmoVxDaolfwaX5km6XHBXY2Fx91e3o/DQ==" + }, + "tx-type": "afrz" +}; + +const ledger = "TestNet"; + +describe('TxAfrz', () => { + beforeEach(() => { + component = shallow(html` + <${TxAfrz} tx=${tx} ledger=${ledger} /> + `); + }); + + it('should display tx id', () => { + expect(component.contains(html`${tx.id}`)).toBe(true); + }); + + it('should display sender', () => { + expect(component.contains(html`${tx.sender}`)).toBe(true); + }); + + it('should display frozen address', () => { + expect(component.contains(html`${tx['asset-freeze-transaction']['address']}`)).toBe(true); + }); + + it('should display asset ID', () => { + expect(component.contains(html`${tx['asset-freeze-transaction']['asset-id'].toString()}`)).toBe(true); + }); + + it('should display confirmed round', () => { + expect(component.contains(html`${tx['confirmed-round'].toString()}`)).toBe(true); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/components/TransactionDetail/TxAfrz.ts b/packages/ui/src/components/TransactionDetail/TxAfrz.ts index 0e203d8d..0b0ae87b 100644 --- a/packages/ui/src/components/TransactionDetail/TxAfrz.ts +++ b/packages/ui/src/components/TransactionDetail/TxAfrz.ts @@ -9,12 +9,12 @@ const TxAfrz: FunctionalComponent = (props: any) => { return html`

Asset ${freezed}

-

TxID: ${tx.id}

-

Origin: ${tx.sender}

-

Freeze address: ${tx['asset-freeze-transaction']['address']}

-

Asset: ${tx['asset-freeze-transaction']['asset-id']}

+

TxID: ${tx.id}

+

Origin: ${tx.sender}

+

Freeze address: ${tx['asset-freeze-transaction']['address']}

+

Asset: ${tx['asset-freeze-transaction']['asset-id']}

Action: ${freezed}

-

Block: ${tx['confirmed-round']}

+

Block: ${tx['confirmed-round']}

See details in GoalSeeker diff --git a/packages/ui/src/components/TransactionDetail/TxAxfer.test.ts b/packages/ui/src/components/TransactionDetail/TxAxfer.test.ts new file mode 100644 index 00000000..d4103162 --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/TxAxfer.test.ts @@ -0,0 +1,65 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import TxAxfer from './TxAxfer'; + +let component; +const tx = { + "asset-transfer-transaction": { + "amount": 0, + "asset-id": 10984, + "close-amount": 0, + "receiver": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A" + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 9548799, + "fee": 249000, + "first-valid": 9548788, + "genesis-hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=", + "genesis-id": "testnet-v1.0", + "id": "5EYWUOBWEEOM4QRE6JNVP3XHEVFCZ7CFNNI32Q5T2SWWUC7QPGIQ", + "intra-round-offset": 0, + "last-valid": 9549788, + "receiver-rewards": 0, + "round-time": 1601394079, + "sender": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "sender-rewards": 0, + "signature": { + "sig": "SP9PaE/7EkOrhBhieble5kcfHbbUGaDl2EXtPcL4OSZy5bPO/ruxECWJPECCH3TJBd9Z8nDMAnv0bZYp/DL8Bg==" + }, + "tx-type": "axfer" +}; + +const ledger = "TestNet"; + +describe('TxAxfer', () => { + beforeEach(() => { + component = shallow(html` + <${TxAxfer} tx=${tx} ledger=${ledger} /> + `); + }); + + it('should display tx id', () => { + expect(component.contains(html`${tx.id}`)).toBe(true); + }); + + it('should display sender', () => { + expect(component.contains(html`${tx.sender}`)).toBe(true); + }); + + it('should display receiver', () => { + expect(component.contains(html`${tx['asset-transfer-transaction'].receiver}`)).toBe(true); + }); + + it('should display amount', () => { + expect(component.contains(html`${(tx['asset-transfer-transaction']['amount']).toString()}`)).toBe(true); + }); + + it('should display confirmed round', () => { + expect(component.contains(html`${tx['confirmed-round'].toString()}`)).toBe(true); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/components/TransactionDetail/TxAxfer.ts b/packages/ui/src/components/TransactionDetail/TxAxfer.ts index dfabe7a2..3ef53903 100644 --- a/packages/ui/src/components/TransactionDetail/TxAxfer.ts +++ b/packages/ui/src/components/TransactionDetail/TxAxfer.ts @@ -9,11 +9,11 @@ const TxAxfer: FunctionalComponent = (props: any) => { return html`

Asset transfer

-

TxID: ${tx.id}

-

From: ${tx.sender}

-

To: ${tx['asset-transfer-transaction'].receiver}

-

Amount: ${tx['asset-transfer-transaction']['amount']}

-

Block: ${tx['confirmed-round']}

+

TxID: ${tx.id}

+

From: ${tx.sender}

+

To: ${tx['asset-transfer-transaction'].receiver}

+

Amount: ${tx['asset-transfer-transaction']['amount']}

+

Block: ${tx['confirmed-round']}

See details in GoalSeeker diff --git a/packages/ui/src/components/TransactionDetail/TxPay.test.ts b/packages/ui/src/components/TransactionDetail/TxPay.test.ts new file mode 100644 index 00000000..eb588fed --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/TxPay.test.ts @@ -0,0 +1,64 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import TxPay from './TxPay'; + +let component; +const tx = { + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 9548884, + "fee": 245000, + "first-valid": 9548877, + "genesis-hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=", + "genesis-id": "testnet-v1.0", + "id": "FTENCQN3ZPSK7CZP2WVJ2FGZG66K6ZT65NEECKWGJ753KAXQSOZQ", + "intra-round-offset": 0, + "last-valid": 9549877, + "payment-transaction": { + "amount": 4232, + "close-amount": 0, + "receiver": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A" + }, + "receiver-rewards": 0, + "round-time": 1601394438, + "sender": "RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A", + "sender-rewards": 0, + "signature": { + "sig": "ZNzEA9U60EQ9w7kZct4t/PyhoE1NYWar6kLJskCIAgfDP4tePFmYdADjNpDcThsRAwSWu0rkRkl+j9WS5br/Aw==" + }, + "tx-type": "pay" +}; + +const ledger = "TestNet"; + +describe('TxPay', () => { + beforeEach(() => { + component = shallow(html` + <${TxPay} tx=${tx} ledger=${ledger} /> + `); + }); + + it('should display tx id', () => { + expect(component.contains(html`${tx.id}`)).toBe(true); + }); + + it('should display sender', () => { + expect(component.contains(html`${tx.sender}`)).toBe(true); + }); + + it('should display receiver', () => { + expect(component.contains(html`${tx['payment-transaction'].receiver}`)).toBe(true); + }); + + it('should display amount', () => { + expect(component.contains(html`${(tx['payment-transaction']['amount']/1e6).toString()} Algos`)).toBe(true); + }); + + it('should display confirmed round', () => { + expect(component.contains(html`${tx['confirmed-round'].toString()}`)).toBe(true); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/components/TransactionDetail/TxPay.ts b/packages/ui/src/components/TransactionDetail/TxPay.ts index 39bd57f0..529d432e 100644 --- a/packages/ui/src/components/TransactionDetail/TxPay.ts +++ b/packages/ui/src/components/TransactionDetail/TxPay.ts @@ -11,11 +11,11 @@ const TxPay: FunctionalComponent = (props: any) => { return html`

Payment

-

TxID: ${tx.id}

-

From: ${tx.sender}

-

To: ${tx['payment-transaction'].receiver}

-

Amount: ${tx['payment-transaction']['amount']/1e6} Algos

-

Block: ${tx['confirmed-round']}

+

TxID: ${tx.id}

+

From: ${tx.sender}

+

To: ${tx['payment-transaction'].receiver}

+

Amount: ${tx['payment-transaction']['amount']/1e6} Algos

+

Block: ${tx['confirmed-round']}

See details in GoalSeeker diff --git a/packages/ui/src/components/TransactionDetail/__snapshots__/TxAcfg.test.ts.snap b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAcfg.test.ts.snap new file mode 100644 index 00000000..fe8ba281 --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAcfg.test.ts.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TxAcfg should match snapshot 1`] = ` + +`; diff --git a/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap new file mode 100644 index 00000000..e390a765 --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TxAfrz should match snapshot 1`] = ` +
+

+ Asset + Freeze +

+

+ + TxID: + + + + VH4LN4PICQPJNAZE6KVCNOYLE3K5XWWZH2OM3HWCSF2TOQU4MGNA + +

+

+ + Origin: + + + + WLH5LELVSEVQL45LBRQYCLJAX6KQPGWUY5WHJXVRV2NPYZUBQAFPH22Q7A + +

+

+ + Freeze address: + + + + ZYQX7BZ6LGTD7UCS7J5RVEAKHUJPK3FNJFZV2GPUYS2TFIADVFHDBKTN7I + +

+

+ + Asset: + + + + 185 + +

+

+ + Action: + + + Freeze +

+

+ + Block: + + + + 3276956 + +

+ +
+`; diff --git a/packages/ui/src/components/TransactionDetail/__snapshots__/TxAxfer.test.ts.snap b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAxfer.test.ts.snap new file mode 100644 index 00000000..dfefaf9e --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAxfer.test.ts.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TxAxfer should match snapshot 1`] = ` +
+

+ Asset transfer +

+

+ + TxID: + + + + 5EYWUOBWEEOM4QRE6JNVP3XHEVFCZ7CFNNI32Q5T2SWWUC7QPGIQ + +

+

+ + From: + + + + RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A + +

+

+ + To: + + + + RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A + +

+

+ + Amount: + + + + 0 + +

+

+ + Block: + + + + 9548799 + +

+ +
+`; diff --git a/packages/ui/src/components/TransactionDetail/__snapshots__/TxPay.test.ts.snap b/packages/ui/src/components/TransactionDetail/__snapshots__/TxPay.test.ts.snap new file mode 100644 index 00000000..b51a6220 --- /dev/null +++ b/packages/ui/src/components/TransactionDetail/__snapshots__/TxPay.test.ts.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TxPay should match snapshot 1`] = ` +
+

+ Payment +

+

+ + TxID: + + + + FTENCQN3ZPSK7CZP2WVJ2FGZG66K6ZT65NEECKWGJ753KAXQSOZQ + +

+

+ + From: + + + + RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A + +

+

+ + To: + + + + RCWKH27QBUZSE5B5BRR4KY6J4FHZB6GMY3ZYHFL2ER43ETTGBQCJGRNH7A + +

+

+ + Amount: + + + + 0.004232 + Algos + +

+

+ + Block: + + + + 9548884 + +

+ +
+`; diff --git a/packages/ui/src/pages/Login.test.ts b/packages/ui/src/pages/Login.test.ts new file mode 100644 index 00000000..f8f49e2b --- /dev/null +++ b/packages/ui/src/pages/Login.test.ts @@ -0,0 +1,36 @@ +import { shallow } from 'enzyme'; +import { html } from 'htm/preact'; +import { sendMessage } from 'services/Messaging' +import Login from './Login'; + +jest.mock('services/Messaging'); + +describe('Login', () => { + let component = shallow(html` + <${Login} /> + `); + + it('should display empty input field', () => { + expect(component.find('input#enterPassword').exists()).toBe(true); + expect(component.find('input#enterPassword').props().value).toBe(''); + }); + + it('should display submit button', () => { + expect(component.find('button#login').exists()).toBe(true); + expect(component.find('button#login').props()).toEqual({ + className: "button is-link is-fullwidth ", + disabled: true, + id: "login", + onClick: expect.any(Function), + }); + }); + + it('should sendMessage on submit', () => { + component.find('button#login').simulate('click'); + expect(sendMessage).toHaveBeenCalledTimes(1); + }); + + it('should match snapshot', () => { + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/pages/__snapshots__/Login.test.ts.snap b/packages/ui/src/pages/__snapshots__/Login.test.ts.snap new file mode 100644 index 00000000..58e44b28 --- /dev/null +++ b/packages/ui/src/pages/__snapshots__/Login.test.ts.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Login should match snapshot 1`] = ` +
+
+
+
+ +
+
+
+ +

+ AlgoSigner does not store your password. If you’ve forgotten your password, you’ll need to create a new wallet and re-link your accounts. +

+
+
+
+ +
+
+`; From 7e620a1fd21def52ce854897cdc2088673c959df Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 5 Oct 2020 18:21:28 +0200 Subject: [PATCH 05/45] Added freeze status check on TxAfrz test --- packages/ui/src/components/TransactionDetail/TxAfrz.test.ts | 5 +++++ packages/ui/src/components/TransactionDetail/TxAfrz.ts | 2 +- .../TransactionDetail/__snapshots__/TxAfrz.test.ts.snap | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts b/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts index c072ae86..2c20084b 100644 --- a/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts +++ b/packages/ui/src/components/TransactionDetail/TxAfrz.test.ts @@ -54,6 +54,11 @@ describe('TxAfrz', () => { expect(component.contains(html`${tx['asset-freeze-transaction']['asset-id'].toString()}`)).toBe(true); }); + it('should display freeze status', () => { + const freezed = tx['asset-freeze-transaction']['new-freeze-status'] ? 'Freeze' : 'Unfreeze'; + expect(component.contains(html`${freezed}`)).toBe(true); + }); + it('should display confirmed round', () => { expect(component.contains(html`${tx['confirmed-round'].toString()}`)).toBe(true); }); diff --git a/packages/ui/src/components/TransactionDetail/TxAfrz.ts b/packages/ui/src/components/TransactionDetail/TxAfrz.ts index 0b0ae87b..8a360359 100644 --- a/packages/ui/src/components/TransactionDetail/TxAfrz.ts +++ b/packages/ui/src/components/TransactionDetail/TxAfrz.ts @@ -13,7 +13,7 @@ const TxAfrz: FunctionalComponent = (props: any) => {

Origin: ${tx.sender}

Freeze address: ${tx['asset-freeze-transaction']['address']}

Asset: ${tx['asset-freeze-transaction']['asset-id']}

-

Action: ${freezed}

+

Action: ${freezed}

Block: ${tx['confirmed-round']}

diff --git a/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap index e390a765..93b257bd 100644 --- a/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap +++ b/packages/ui/src/components/TransactionDetail/__snapshots__/TxAfrz.test.ts.snap @@ -52,7 +52,9 @@ exports[`TxAfrz should match snapshot 1`] = ` Action: - Freeze + + Freeze +

From 04f19c9d7c1143ea094868a961043391a516849f Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Thu, 8 Oct 2020 11:09:23 +0200 Subject: [PATCH 06/45] Implemented temp fix for macOS Chrome running on extended screen --- packages/extension/manifest.json | 1 + packages/ui/index.html | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 8b9e7727..29720b89 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -23,6 +23,7 @@ "default_icon": "icon.png", "default_popup":"index.html" }, + "content_security_policy": "script-src 'self' 'sha256-r3HgVicK23eFlaBe+glyMCfP98WwT3i3bmq8FxshKMY='; object-src 'self'", "web_accessible_resources": ["AlgoSigner.min.js"], "permissions": [ "storage" diff --git a/packages/ui/index.html b/packages/ui/index.html index 07930cd1..82dc99a6 100644 --- a/packages/ui/index.html +++ b/packages/ui/index.html @@ -11,5 +11,39 @@

+ \ No newline at end of file From a1de8a1532618a361677d2b0a4efd4f73614b187 Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Tue, 20 Oct 2020 14:34:37 +0200 Subject: [PATCH 07/45] Implemented internal methods for handling assets search This includes querying Algorand's Verified assets API and querying the indexer for all assets --- packages/common/src/messaging/types.ts | 2 + .../background/messaging/internalMethods.ts | 85 ++++++++++++++++--- .../src/background/messaging/task.ts | 6 ++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index 649d6ffa..34592fb9 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -24,6 +24,8 @@ export enum JsonRpcMethod { AccountDetails = "account-details", Transactions = "transactions", AssetDetails = "asset-details", + AssetsAPIList = "assets-api-list", + AssetsVerifiedList = "assets-verified-list", SignSendTransaction = "sign-send-transaction", ChangeLedger = "change-ledger", diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 5af0eba1..dba238f7 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -325,8 +325,70 @@ export class InternalMethods { return true; } + public static [JsonRpcMethod.AssetsAPIList](request: any, sendResponse: Function) { + function searchAssets(assets, indexer, nextToken, filter) { + const req = indexer.searchForAssets().limit(30).name(filter); + if (nextToken) + req.nextToken(nextToken); + req.do().then((res: any) => { + let newAssets = assets.concat(res.assets); + for (var i = newAssets.length - 1; i >= 0; i--) { + newAssets[i] = { + "asset_id": newAssets[i].index, + "name": newAssets[i]['params']['name'], + "unit_name": newAssets[i]['params']['unit-name'], + }; + } + res.assets = newAssets; + + sendResponse(res); + }).catch((e: any) => { + sendResponse({error: e.message}); + }); + } + + const { ledger, filter, nextToken } = request.body.params; + let indexer = this.getIndexer(ledger); + // Do the search for asset id (if filter value is integer) + // and asset name and concat them. + if (filter.length > 0 && !isNaN(filter) && (!nextToken || nextToken.length === 0)) { + indexer.searchForAssets().index(filter).do().then((res: any) => { + searchAssets(res.assets, indexer, nextToken, filter); + }).catch((e: any) => { + sendResponse({error: e.message}); + }); + } else { + searchAssets([], indexer, nextToken, filter); + } + return true; + } + + public static [JsonRpcMethod.AssetsVerifiedList](request: any, sendResponse: Function) { + const { ledger } = request.body.params; + + if (ledger === Ledger.MainNet){ + fetch("https://mobile-api.algorand.com/api/assets/?status=verified") + .then((response) => { + return response.json().then((json) =>{ + if (response.ok) { + sendResponse(json); + } else { + sendResponse({error: json}); + } + }) + }).catch((e) => { + sendResponse({error: e.message}); + }); + } else { + sendResponse({ + results: [] + }); + } + return true; + } + public static [JsonRpcMethod.SignSendTransaction](request: any, sendResponse: Function) { - const { ledger, address, to, amount, note, passphrase } = request.body.params; + const { ledger, address, passphrase, txnParams } = request.body.params; this._encryptionWrap = new encryptionWrap(request.body.params.passphrase); var algod = this.getAlgod(ledger); @@ -349,21 +411,20 @@ export class InternalMethods { let params = await algod.getTransactionParams().do(); let txn = { - "type": "pay", - "from": address, - "to": to, - "fee": params.fee, - "amount": +amount, - "firstRound": params.firstRound, - "lastRound": params.lastRound, - "genesisID": params.genesisID, - "genesisHash": params.genesisHash, - "note": new Uint8Array(Buffer.from(note)) + ...txnParams, + fee: params.fee, + firstRound: params.firstRound, + lastRound: params.lastRound, + genesisID: params.genesisID, + genesisHash: params.genesisHash, }; + if ('note' in txn) + txn.note = new Uint8Array(Buffer.from(txn.note)); + const txHeaders = { 'Content-Type' : 'application/x-binary' - } + }; var transactionWrap = undefined; try { diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index f0d0c579..36221bbc 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -495,6 +495,12 @@ export class Task { [JsonRpcMethod.AssetDetails]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.AssetDetails](request, sendResponse) }, + [JsonRpcMethod.AssetsAPIList]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.AssetsAPIList](request, sendResponse) + }, + [JsonRpcMethod.AssetsVerifiedList]: (request: any, sendResponse: Function) => { + return InternalMethods[JsonRpcMethod.AssetsVerifiedList](request, sendResponse) + }, [JsonRpcMethod.SignSendTransaction]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.SignSendTransaction](request, sendResponse) }, From cb8a0a1796134c8fb84af526e174dfdf923749ea Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Tue, 20 Oct 2020 14:35:35 +0200 Subject: [PATCH 08/45] Implemented debounce hook --- packages/ui/src/services/customHooks.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/ui/src/services/customHooks.ts diff --git a/packages/ui/src/services/customHooks.ts b/packages/ui/src/services/customHooks.ts new file mode 100644 index 00000000..a4115320 --- /dev/null +++ b/packages/ui/src/services/customHooks.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'preact/hooks'; + +export function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, + [value] + ); + + return debouncedValue; +} \ No newline at end of file From 31b20b28a4cf5d99727faaf7a928df8a95da192c Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Tue, 20 Oct 2020 14:37:19 +0200 Subject: [PATCH 09/45] Assets can now be added to an account from the UI --- .../src/components/Account/AddAssetConfirm.ts | 140 ++++++++++++++ .../ui/src/components/Account/AssetsList.ts | 26 ++- packages/ui/src/index.js | 2 + packages/ui/src/pages/Account.ts | 14 +- packages/ui/src/pages/AddAsset.ts | 181 ++++++++++++++++++ packages/ui/src/pages/SendAlgos.ts | 17 +- 6 files changed, 356 insertions(+), 24 deletions(-) create mode 100644 packages/ui/src/components/Account/AddAssetConfirm.ts create mode 100644 packages/ui/src/pages/AddAsset.ts diff --git a/packages/ui/src/components/Account/AddAssetConfirm.ts b/packages/ui/src/components/Account/AddAssetConfirm.ts new file mode 100644 index 00000000..470c65d8 --- /dev/null +++ b/packages/ui/src/components/Account/AddAssetConfirm.ts @@ -0,0 +1,140 @@ +import { FunctionalComponent } from "preact"; +import { html } from 'htm/preact'; +import { useContext, useState } from 'preact/hooks'; +import { route } from 'preact-router'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; + +import { sendMessage } from 'services/Messaging' +import { StoreContext } from 'services/StoreContext' + +import Authenticate from 'components/Authenticate' + +const AddAssetConfirm: FunctionalComponent = (props: any) => { + const { asset, ledger, address } = props; + const store:any = useContext(StoreContext); + const [askAuth, setAskAuth] = useState(false); + const [loading, setLoading] = useState(false); + const [authError, setAuthError] = useState(''); + const [error, setError] = useState(''); + const [txId, setTxId] = useState(''); + + const addAsset = (pwd: string) => { + const params = { + ledger: ledger, + passphrase: pwd, + address: address, + txnParams: { + type: "axfer", + assetIndex: asset['asset_id'], + from: address, + to: address, + amount: 0 + } + }; + + setLoading(true); + setAuthError(''); + sendMessage(JsonRpcMethod.SignSendTransaction, params, function(response) { + if ('error' in response) { + setLoading(false); + switch (response.error) { + case "Login Failed": + setAuthError('Wrong passphrase'); + break; + default: + setError(response.error); + setAskAuth(false); + break; + } + } else { + setAskAuth(false); + setTxId(response.txId); + } + }); + }; + + return html` + ${ !askAuth && txId.length === 0 && !error && html` + + `} + + ${txId.length > 0 && html` +
+ Transaction sent! + + +
+ `} + + ${ error !== undefined && error.length > 0 && html` +
+

+ Transaction failed with the following error: +

+

${error}

+
+ `} + + ${askAuth && html` + <${Authenticate} + error=${authError} + loading=${loading} + nextStep=${addAsset} /> + `} + ` +}; + +export default AddAssetConfirm; \ No newline at end of file diff --git a/packages/ui/src/components/Account/AssetsList.ts b/packages/ui/src/components/Account/AssetsList.ts index d1a5963c..626a0936 100644 --- a/packages/ui/src/components/Account/AssetsList.ts +++ b/packages/ui/src/components/Account/AssetsList.ts @@ -43,21 +43,17 @@ const AssetsList: FunctionalComponent = (props: any) => { // Only show the first 10 assets, and a link to the assets view // if needed. return html` -
- Assets - ${ assets.slice(0, 10).map((asset: any) => html` - <${AssetPreview} asset=${asset} setShowAsset=${setShowAsset} /> - `)} - ${ assets.length > 10 && html` - - `} -
- + ${ assets.slice(0, 10).map((asset: any) => html` + <${AssetPreview} asset=${asset} setShowAsset=${setShowAsset} /> + `)} + ${ assets.length > 10 && html` + + `}
diff --git a/packages/ui/src/index.js b/packages/ui/src/index.js index 5165dca0..06dccfeb 100644 --- a/packages/ui/src/index.js +++ b/packages/ui/src/index.js @@ -19,6 +19,7 @@ import ImportAccount from 'pages/ImportAccount' import Wallet from 'pages/Wallet' import Account from 'pages/Account' import SendAlgos from 'pages/SendAlgos' +import AddAsset from 'pages/AddAsset' import SignTransaction from 'pages/SignTransaction' import { StoreProvider } from 'services/StoreContext' @@ -51,6 +52,7 @@ const App = () => { <${CreateAccount} path="/:ledger/create-account" /> <${ImportAccount} path="/:ledger/import-account" /> <${Account} path="/:ledger/:address" /> + <${AddAsset} path="/:ledger/:address/add-asset" /> <${SendAlgos} path="/:ledger/:address/send" />
diff --git a/packages/ui/src/pages/Account.ts b/packages/ui/src/pages/Account.ts index 0f49eb64..7acb20a9 100644 --- a/packages/ui/src/pages/Account.ts +++ b/packages/ui/src/pages/Account.ts @@ -73,9 +73,17 @@ const Account: FunctionalComponent = (props: any) => {
- ${ results && results.assets.length > 0 && html` - <${AssetsList} assets=${results.assets} ledger=${ledger}/> - `} +
+
+ Assets + <${Link} id="sendAlgos" style="font-size: 0.9em" class="is-pulled-right" href=${`${url}/add-asset`}> + Add new asset + +
+ ${ results && results.assets.length > 0 && html` + <${AssetsList} assets=${results.assets} ledger=${ledger}/> + `} +
<${TransactionsList} address=${address} ledger=${ledger}/> diff --git a/packages/ui/src/pages/AddAsset.ts b/packages/ui/src/pages/AddAsset.ts new file mode 100644 index 00000000..c90d77b9 --- /dev/null +++ b/packages/ui/src/pages/AddAsset.ts @@ -0,0 +1,181 @@ +import { FunctionalComponent } from "preact"; +import { html } from 'htm/preact'; +import { useState, useEffect } from 'preact/hooks'; +import { route } from 'preact-router'; +import { JsonRpcMethod } from '@algosigner/common/messaging/types'; + +import { sendMessage } from 'services/Messaging' +import { useDebounce } from 'services/customHooks' + +import HeaderView from 'components/HeaderView' +import AddAssetConfirm from 'components/Account/AddAssetConfirm' + +const AddAsset: FunctionalComponent = (props: any) => { + const { matches, path, url, ledger, address } = props; + const [tab, setTab] = useState(0); + const [filter, setFilter] = useState(''); + const [selectedAsset, setSelectedAsset] = useState(null); + const [nextToken, setNextToken] = useState(null); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + // Verified results coming from Inc's API + const [verifiedResults, setVerifiedResults] = useState({}); + const [verifiedIDs, setVerifiedIDs] = useState([]); + + const debouncedFilter = useDebounce(filter, 500); + + const filterChange = (e) => { + setNextToken(null); + setFilter(e.target.value); + }; + + const changeTab = (selectedTab) => { + setNextToken(null); + setTab(selectedTab); + }; + + const fetchApi = () => { + setLoading(true); + const params = { + ledger: ledger, + filter: filter, + nextToken: nextToken + }; + sendMessage(JsonRpcMethod.AssetsAPIList, params, function(response) { + // If there is a nextToken set, it means that we are loading additional + // results. + if (nextToken && nextToken.length > 0) + setResults(results.concat(response.assets)); + else + setResults(response.assets); + + setNextToken(response['next-token']); + setLoading(false); + }); + } + + useEffect(() => { + switch (tab) { + case 0: + if (verifiedResults.results) { + const value = debouncedFilter.toLowerCase(); + setResults(verifiedResults.results.filter((elem) => { + return (elem['asset_id'] === +value) || + (elem.name && elem.name.toLowerCase().includes(value)) || + (elem['unit_name'] && elem['unit_name'].toLowerCase().includes(value)); + })); + } + break; + case 1: + fetchApi(); + break; + }; + }, [debouncedFilter, tab]); + + useEffect(() => { + setLoading(true); + const params = { + ledger: ledger + }; + sendMessage(JsonRpcMethod.AssetsVerifiedList, params, function(response) { + setVerifiedResults(response); + setResults(response.results); + let ids: number[] = []; + for (var i = response.results.length - 1; i >= 0; i--) { + ids.push(response.results[i]['asset_id']); + } + setVerifiedIDs(ids); + setLoading(false); + }); + }, []); + + return html` +
+ <${HeaderView} + action="${() => route(`/${matches.ledger}/${matches.address}`)}" + title="Add asset" /> + +
+ +
+ +
+ +
+ +
+ ${ results.length == 0 && !loading && html` +
+ No results! +
+ `} + ${results.map((asset: any) => html` +
setSelectedAsset(asset)} + style="border-bottom: 1px solid rgba(138, 159, 168, 0.2); cursor: pointer; justify-content: space-between; align-items: center;"> +
+ ${ asset.name && asset.name.length > 0 && html` + ${ asset.name } + `} + ${ verifiedIDs.includes(asset['asset_id']) && html` + + + + `} +
+ ${ asset['unit_name'] && asset['unit_name'].length > 0 && html` + ${asset['unit_name']} + `} +
+ + ${asset['asset_id']} +
+ `)} + ${ nextToken && nextToken.length > 0 && ! loading && html` + + `} + ${ loading && html` +
+ + + +
+ `} +
+
+ + ${ selectedAsset && html` + + `} + ` +}; + +export default AddAsset; \ No newline at end of file diff --git a/packages/ui/src/pages/SendAlgos.ts b/packages/ui/src/pages/SendAlgos.ts index 78c30e6f..39712a0e 100644 --- a/packages/ui/src/pages/SendAlgos.ts +++ b/packages/ui/src/pages/SendAlgos.ts @@ -14,9 +14,9 @@ import Authenticate from 'components/Authenticate' const SendAlgos: FunctionalComponent = (props: any) => { const store:any = useContext(StoreContext); - const { matches, path, url, ledger, address } = props; + const { matches, ledger, address } = props; - const [askAuth, setAskAuth] = useState(false); + const [askAuth, setAskAuth] = useState(false); const [to, setTo] = useState(''); const [amount, setAmount] = useState(''); const [note, setNote] = useState(''); @@ -41,12 +41,17 @@ const SendAlgos: FunctionalComponent = (props: any) => { const params = { ledger: ledger, + passphrase: pwd, address: account.address, - amount: +amount*1e6, - note: note, - to: to, - passphrase: pwd + txnParams: { + type: "pay", + from: account.address, + to: to, + note: note, + amount: +amount*1e6, + } }; + sendMessage(JsonRpcMethod.SignSendTransaction, params, function(response) { if ('error' in response) { setLoading(false); From e8c1365ad26ca8515053882b72926c03bc41df3a Mon Sep 17 00:00:00 2001 From: Brent Date: Mon, 26 Oct 2020 10:06:57 -0400 Subject: [PATCH 10/45] Tx breakouts, validation changes, error highlighting --- packages/common/src/interfaces/acfg.ts | 4 +- packages/common/src/interfaces/acfg_create.ts | 19 +++ .../common/src/interfaces/acfg_destroy.ts | 9 ++ packages/common/src/interfaces/appl.ts | 14 +- packages/common/src/interfaces/axfer.ts | 4 +- .../common/src/interfaces/axfer_accept.ts | 11 ++ .../common/src/interfaces/axfer_clawback.ts | 11 ++ packages/common/src/interfaces/baseTx.ts | 4 +- packages/common/src/interfaces/keyreg.ts | 2 +- packages/common/src/interfaces/pay.ts | 4 +- packages/common/src/types/ledgers.ts | 5 + packages/common/src/types/transaction.ts | 3 +- packages/extension/package.json | 2 +- .../background/messaging/internalMethods.ts | 9 +- .../src/background/messaging/task.ts | 81 +++++------ .../transaction/acfgCreateTransaction.ts | 40 ++++++ .../transaction/acfgDestroyTransaction.ts | 31 +++++ .../background/transaction/acfgTransaction.ts | 52 ++++--- .../background/transaction/actions.test.ts | 131 ++++++++++++++++++ .../src/background/transaction/actions.ts | 88 ++++++++++-- .../background/transaction/afrzTransaction.ts | 16 ++- .../background/transaction/applTransaction.ts | 30 +++- .../transaction/axferAcceptTransaction.ts | 40 ++++++ .../transaction/axferClawbackTransaction.ts | 33 +++++ .../transaction/axferTransaction.ts | 21 +-- .../transaction/baseValidatedTxnWrap.ts | 61 ++++++-- .../transaction/keyregTransaction.ts | 14 +- .../background/transaction/payTransaction.ts | 15 +- .../src/background/utils/validator.test.ts | 34 ++--- .../src/background/utils/validator.ts | 101 ++++++++++---- .../extension/src/errors/validation.test.ts | 8 ++ packages/extension/src/errors/validation.ts | 7 + packages/ui/src/components/AccountPreview.ts | 4 +- .../src/components/SignTransaction/TxAcfg.ts | 6 +- .../src/components/SignTransaction/TxAfrz.ts | 4 +- .../src/components/SignTransaction/TxAlert.ts | 41 ++++++ .../src/components/SignTransaction/TxAxfer.ts | 6 +- .../src/components/SignTransaction/TxPay.ts | 6 +- packages/ui/src/pages/SignTransaction.ts | 29 ++-- packages/ui/src/styles.scss | 24 ++++ 40 files changed, 821 insertions(+), 203 deletions(-) create mode 100644 packages/common/src/interfaces/acfg_create.ts create mode 100644 packages/common/src/interfaces/acfg_destroy.ts create mode 100644 packages/common/src/interfaces/axfer_accept.ts create mode 100644 packages/common/src/interfaces/axfer_clawback.ts create mode 100644 packages/common/src/types/ledgers.ts create mode 100644 packages/extension/src/background/transaction/acfgCreateTransaction.ts create mode 100644 packages/extension/src/background/transaction/acfgDestroyTransaction.ts create mode 100644 packages/extension/src/background/transaction/axferAcceptTransaction.ts create mode 100644 packages/extension/src/background/transaction/axferClawbackTransaction.ts create mode 100644 packages/extension/src/errors/validation.test.ts create mode 100644 packages/extension/src/errors/validation.ts create mode 100644 packages/ui/src/components/SignTransaction/TxAlert.ts diff --git a/packages/common/src/interfaces/acfg.ts b/packages/common/src/interfaces/acfg.ts index 9d15b531..4e180c22 100644 --- a/packages/common/src/interfaces/acfg.ts +++ b/packages/common/src/interfaces/acfg.ts @@ -5,7 +5,7 @@ import { IBaseTx } from "./baseTx"; /// export interface IAssetConfigTx extends IBaseTx { type: string, //"acfg" - assetIndex?: number, //uint64 "caid" For re-configure or destroy transactions, this is the unique asset ID. On asset creation, the ID is set to zero. + assetIndex: number, //uint64 "caid" For re-configure or destroy transactions, this is the unique asset ID. On asset creation, the ID is set to zero. assetTotal?: number, //uint64 "t" The total number of base units of the asset to create. This number cannot be changed. assetDecimals?: number, //uint32 "dc" The number of digits to use after the decimal point when displaying the asset. If 0, the asset is not divisible. If 1, the base unit of the asset is in tenths. If 2, the base unit of the asset is in hundredths. assetDefaultFrozen?: boolean, //bool "df" True to freeze holdings for this asset by default. @@ -16,5 +16,5 @@ export interface IAssetConfigTx extends IBaseTx { assetManager?: string, //Address "m" The address of the account that can manage the configuration of the asset and destroy it. assetReserve?: string, //Address "r" The address of the account that holds the reserve (non-minted) units of the asset. This address has no specific authority in the protocol itself. It is used in the case where you want to signal to holders of your asset that the non-minted units of the asset reside in an account that is different from the default creator account (the sender). assetFreeze?: string, //Address "f" The address of the account used to freeze holdings of this asset. If empty, freezing is not permitted. - assetClawback?: string //Address "c" The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted. + assetClawback?: string, //Address "c" The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted. } diff --git a/packages/common/src/interfaces/acfg_create.ts b/packages/common/src/interfaces/acfg_create.ts new file mode 100644 index 00000000..5a4f6b53 --- /dev/null +++ b/packages/common/src/interfaces/acfg_create.ts @@ -0,0 +1,19 @@ +import { IBaseTx } from "./baseTx"; + +/// +// Mapping interface of allowable fields for acfg create transactions. +/// +export interface IAssetCreateTx extends IBaseTx { + type: string, //"acfg" + assetTotal: number, //uint64 "t" The total number of base units of the asset to create. This number cannot be changed. + assetDecimals: number, //uint32 "dc" The number of digits to use after the decimal point when displaying the asset. If 0, the asset is not divisible. If 1, the base unit of the asset is in tenths. If 2, the base unit of the asset is in hundredths. + assetDefaultFrozen?: boolean, //bool "df" True to freeze holdings for this asset by default. + assetUnitName?: string, //string "un" The name of a unit of this asset. Supplied on creation. Example: USDT + assetName?: string, //string "an" The name of the asset. Supplied on creation. Example: Tether + assetURL?: string, //string "au" Specifies a URL where more information about the asset can be retrieved. Max size is 32 bytes. + assetMetadataHash?: any, //[]byte "am" This field is intended to be a 32-byte hash of some metadata that is relevant to your asset and/or asset holders. The format of this metadata is up to the application. This field can only be specified upon creation. An example might be the hash of some certificate that acknowledges the digitized asset as the official representation of a particular real-world asset. + assetManager?: string, //Address "m" The address of the account that can manage the configuration of the asset and destroy it. + assetReserve?: string, //Address "r" The address of the account that holds the reserve (non-minted) units of the asset. This address has no specific authority in the protocol itself. It is used in the case where you want to signal to holders of your asset that the non-minted units of the asset reside in an account that is different from the default creator account (the sender). + assetFreeze?: string, //Address "f" The address of the account used to freeze holdings of this asset. If empty, freezing is not permitted. + assetClawback?: string, //Address "c" The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted. +} \ No newline at end of file diff --git a/packages/common/src/interfaces/acfg_destroy.ts b/packages/common/src/interfaces/acfg_destroy.ts new file mode 100644 index 00000000..e197e263 --- /dev/null +++ b/packages/common/src/interfaces/acfg_destroy.ts @@ -0,0 +1,9 @@ +import { IBaseTx } from "./baseTx"; + +/// +// Mapping interface of allowable fields for acfg destroy transactions. +/// +export interface IAssetDestroyTx extends IBaseTx { + type: string, //"acfg" + assetIndex: number, //uint64 "caid" For re-configure or destroy transactions, this is the unique asset ID. On asset creation, the ID is set to zero. +} \ No newline at end of file diff --git a/packages/common/src/interfaces/appl.ts b/packages/common/src/interfaces/appl.ts index 8c0574c9..a934096d 100644 --- a/packages/common/src/interfaces/appl.ts +++ b/packages/common/src/interfaces/appl.ts @@ -1,4 +1,16 @@ /// // Mapping interface of allowable fields for appl transactions. /// -export interface IAppl {} \ No newline at end of file +export interface IApplTx { + type: string, //"appl" + apid: number, //uint64 "apid" ID of the application being configured or empty if creating. + apan: number, //uint64 "apan" Defines what additional actions occur with the transaction. See the OnComplete section of the TEAL spec for details. + apat?: string, //Address "apat" List of accounts in addition to the sender that may be accessed from the application's approval-program and clear-state-program. + apap?: string, //Address "apap" Logic executed for every application transaction, except when on-completion is set to "clear". It can read and write global state for the application, as well as account-specific local state. Approval programs may reject the transaction. + apaa?: any, //byte[] "apaa" Transaction specific arguments accessed from the application's approval-program and clear-state-program. + apsu?: string, //Address "apsu" Logic executed for application transactions with on-completion set to "clear". It can read and write global state for the application, as well as account-specific local state. Clear state programs cannot reject the transaction. + apfa?: string, //Address "apfa" Lists the applications in addition to the application-id whose global states may be accessed by this application's approval-program and clear-state-program. The access is read-only. + apas?: string, //Address "apas" Lists the assets whose AssetParams may be accessed by this application's approval-program and clear-state-program. The access is read-only. + apgs?: any, //StateSchema "apgs" Holds the maximum number of global state values defined within a StateSchema object. + apls?: any, //StateSchema "apls" Holds the maximum number of local state values defined within a StateSchema object. +} \ No newline at end of file diff --git a/packages/common/src/interfaces/axfer.ts b/packages/common/src/interfaces/axfer.ts index 4e325d93..30695912 100644 --- a/packages/common/src/interfaces/axfer.ts +++ b/packages/common/src/interfaces/axfer.ts @@ -6,9 +6,7 @@ import { IBaseTx } from "./baseTx"; export interface IAssetTransferTx extends IBaseTx { type: string, //"axfer" assetIndex: number, //uint64 "xaid" The unique ID of the asset to be transferred. - amount?: number, //uint64 "aamt" The amount of the asset to be transferred. A zero amount transferred to self allocates that asset in the account's Asset map. + amount: number, //uint64 "aamt" The amount of the asset to be transferred. A zero amount transferred to self allocates that asset in the account's Asset map. to: string, //Address "arcv" The recipient of the asset transfer. assetCloseTo?: string, //Address "aclose" Specify this field to remove the asset holding from the sender account and reduce the account's minimum balance. - - //For clawback the sender of this transaction must be the clawback account specified in the asset configuration } \ No newline at end of file diff --git a/packages/common/src/interfaces/axfer_accept.ts b/packages/common/src/interfaces/axfer_accept.ts new file mode 100644 index 00000000..ee06839d --- /dev/null +++ b/packages/common/src/interfaces/axfer_accept.ts @@ -0,0 +1,11 @@ +import { IBaseTx } from "./baseTx"; + +/// +// Mapping interface of allowable fields for axfer accept transactions. +/// +export interface IAssetAcceptTx extends IBaseTx { + type: string, //"axfer" + assetIndex: number, //uint64 "xaid" The unique ID of the asset to be transferred. + amount?: number, //uint64 "aamt" The amount of the asset to be transferred. A zero amount transferred to self allocates that asset in the account's Asset map. + to: string, //Address "arcv" The recipient of the asset transfer. Must be self for Asset Accept. +} \ No newline at end of file diff --git a/packages/common/src/interfaces/axfer_clawback.ts b/packages/common/src/interfaces/axfer_clawback.ts new file mode 100644 index 00000000..4a05c190 --- /dev/null +++ b/packages/common/src/interfaces/axfer_clawback.ts @@ -0,0 +1,11 @@ +import { IBaseTx } from "./baseTx"; + +/// +// Mapping interface of allowable fields for axfer transactions. +/// +export interface IAssetClawbackTx extends IBaseTx { + type: string, //"axfer" + assetIndex: number, //uint64 "xaid" The unique ID of the asset to be transferred. + amount: number, //uint64 "aamt" The amount of the asset to be transferred. A zero amount transferred to self allocates that asset in the account's Asset map. assetCloseTo?: string, //Address "aclose" Specify this field to remove the asset holding from the sender account and reduce the account's minimum balance. + assetRevocationTarget: string, //Address "asnd" The address from which the funds will be withdrawn. +} \ No newline at end of file diff --git a/packages/common/src/interfaces/baseTx.ts b/packages/common/src/interfaces/baseTx.ts index d502c6c2..80434041 100644 --- a/packages/common/src/interfaces/baseTx.ts +++ b/packages/common/src/interfaces/baseTx.ts @@ -2,8 +2,7 @@ // Mapping interface of allowable base fields for transactions. /// export interface IBaseTx { - //ledger?: string, - from?: string, //Address "snd" The address of the account that pays the fee and amount. (Auto set by algosdk as the from.publicKey) + from: string, //Address "snd" The address of the account that pays the fee and amount. (Auto set by algosdk as the from.publicKey) fee: number, //uint64 "fee" Paid by the sender to the FeeSink to prevent denial-of-service. The minimum fee on Algorand is currently 1000 microAlgos. firstRound: number, //uint64 "fv" The first round for when the transaction is valid. If the transaction is sent prior to this round it will be rejected by the network. lastRound: number, //uint64 "lv" The ending round for which the transaction is valid. After this round, the transaction will be rejected by the network. @@ -13,5 +12,6 @@ export interface IBaseTx { group?: any, //[32]byte "grp" The group specifies that the transaction is part of a group and, if so, specifies the hash of the transaction group. Assign a group ID to a transaction through the workflow described in the Atomic Transfers Guide. lease?: any, //[32]byte "lx" A lease enforces mutual exclusion of transactions. If this field is nonzero, then once the transaction is confirmed, it acquires the lease identified by the (Sender, Lease) pair of the transaction until the LastValid round passes. While this transaction possesses the lease, no other transaction specifying this lease can be confirmed. A lease is often used in the context of Algorand Smart Contracts to prevent replay attacks. (Buffer is created from the provided value) //txType //string "type" Specifies the type of transaction. This value is automatically generated using any of the developer tools. + rekey?: any, } diff --git a/packages/common/src/interfaces/keyreg.ts b/packages/common/src/interfaces/keyreg.ts index 5c215d0b..baf797a6 100644 --- a/packages/common/src/interfaces/keyreg.ts +++ b/packages/common/src/interfaces/keyreg.ts @@ -9,6 +9,6 @@ export interface IKeyRegistrationTx extends IBaseTx { selectionKey: string, //VrfPubkey "selkey" The VRF public key. 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. + voteKeyDilution?: number, //uint64 "votekd" This is the dilution for the 2-level participation key. //nonparticipation //bool "nonpart" All new Algorand accounts are participating by default. } diff --git a/packages/common/src/interfaces/pay.ts b/packages/common/src/interfaces/pay.ts index dcc2b3c8..b2b3080d 100644 --- a/packages/common/src/interfaces/pay.ts +++ b/packages/common/src/interfaces/pay.ts @@ -7,6 +7,6 @@ export interface IPaymentTx extends IBaseTx { type: string, //"pay" to: string, //Address "rcv" The address of the account that receives the amount. amount: number, //uint64 "amt" The total amount to be sent in microAlgos. - closeRemainderTo?: string, //Address "close" When set, it indicates that the transaction is requesting that the Sender account should be closed, and all remaining funds, after the fee and amount are paid, be transferred to this address. - rekey?: any + closeRemainderTo?: string, //Address "close" When set, it indicates that the transaction is requesting that the Sender account should be closed, and all remaining funds, after the fee and amount are paid, be transferred to this address. + rekey?: any, } diff --git a/packages/common/src/types/ledgers.ts b/packages/common/src/types/ledgers.ts new file mode 100644 index 00000000..2eb83d0f --- /dev/null +++ b/packages/common/src/types/ledgers.ts @@ -0,0 +1,5 @@ +export function getSupportedLedgers(): Array{ + // Need to add access to additional ledger types from import + return [{"name": "mainnet", "genesisId": 'mainnet-v1.0', "genesisHash": ""}, + {"name": "testnet", "genesisId": 'testnet-v1.0', "genesisHash": ""}]; +} \ No newline at end of file diff --git a/packages/common/src/types/transaction.ts b/packages/common/src/types/transaction.ts index 30009343..81a564ca 100644 --- a/packages/common/src/types/transaction.ts +++ b/packages/common/src/types/transaction.ts @@ -7,6 +7,7 @@ export enum TransactionType { Keyreg = "keyreg", Axfer = "axfer", Afrz = "afrz", - Acfg = "acfg" + Acfg = "acfg", + Appl = "appl", }; diff --git a/packages/extension/package.json b/packages/extension/package.json index c49282d0..d63d22d4 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -18,7 +18,7 @@ "webpack-cli": "^3.3.12" }, "dependencies": { - "algosdk": "1.6.2" + "algosdk": "^1.7.2" }, "scripts": { "build": "npm run clean && npm run bundle && npm run copy", diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index dba238f7..96bc1e53 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -7,7 +7,7 @@ import { Settings } from '../config'; import encryptionWrap from "../encryptionWrap"; import Session from '../utils/session'; import AssetsDetailsHelper from '../utils/assetsDetailsHelper'; -import { ValidationResponse } from '../utils/validator'; +import { ValidationStatus } from '../utils/validator'; import { getValidatedTxnWrap } from "../transaction/actions"; const algosdk = require("algosdk"); @@ -409,7 +409,6 @@ export class InternalMethods { var recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); let params = await algod.getTransactionParams().do(); - let txn = { ...txnParams, fee: params.fee, @@ -441,13 +440,13 @@ export class InternalMethods { 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 === ValidationResponse.Invalid)) { + 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. sendResponse({error: 'One or more fields are not valid. Please check and try again.'}); return; } - else if(transactionWrap.validityObject && (Object.values(transactionWrap.validityObject).some(value => value === ValidationResponse.Warning )) - || (Object.values(transactionWrap.validityObject).some(value => value === ValidationResponse.Dangerous))) { + else if(transactionWrap.validityObject && (Object.values(transactionWrap.validityObject).some(value => value['status'] === ValidationStatus.Warning )) + || (Object.values(transactionWrap.validityObject).some(value => value['status'] === ValidationStatus.Dangerous))) { // We have a transaction which does not contain invalid fields, but does contain fields that are dangerous // or ones we've flagged as needing to be reviewed. We can use a modified popup to allow the normal flow, but require extra scrutiny. let signedTxn; diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 36221bbc..b012cad2 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -4,14 +4,14 @@ import {RequestErrors} from '@algosigner/common/types'; import {JsonRpcMethod} from '@algosigner/common/messaging/types'; import { Ledger, API } from './types'; import { getValidatedTxnWrap } from "../transaction/actions"; -import { ValidationResponse } from '../utils/validator'; +import { 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} from '@algosigner/common/logging'; - +import { InvalidTransactionStructure } from "../../errors/validation"; export class Task { @@ -118,67 +118,52 @@ export class Task { resolve: Function, reject: Function ) => { var transactionWrap = undefined; + var validationError = undefined; try { transactionWrap = getValidatedTxnWrap(d.body.params, d.body.params["type"]); } catch(e) { logging.log(`Validation failed. ${e}`); + validationError = e; } - - if(!transactionWrap) { + if(!transactionWrap && validationError && validationError instanceof InvalidTransactionStructure) { + // We don't have a transaction wrap, but we have a validation error. + d.error = { + message: validationError.message + }; + reject(d); + return; + } + else 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.'); d.error = { - message: 'Validation failed for transaction. Please verify the properties are valid.' + message: (validationError || 'Validation failed for transaction. Please verify the properties are valid.') }; reject(d); } - else if(transactionWrap.validityObject && Object.values(transactionWrap.validityObject).some(value => value === ValidationResponse.Invalid)) { + 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. // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. + let invalidKeys = []; + for (const [key, value] of Object.entries(transactionWrap.validityObject)) { + if(value===ValidationStatus.Invalid){ + invalidKeys.push(`${key}`); + } + } d.error = { - message: 'Validation failed for transaction because of invalid properties.' + message: `Validation failed for transaction because of invalid properties [${invalidKeys.join(',')}].` }; reject(d); } else { - // Check for the validity of the from field. - let tx = d.body.params; - if (tx.from === undefined) { - d.error = { - message: 'from property is required.' - } - reject(d); - return; - } - if (tx.genesisID === undefined) { - d.error = { - message: 'genesisID property is required.' - } - reject(d); - return; - } - - let txLedger; - if (tx.genesisID === "mainnet-v1.0") - txLedger = Ledger.MainNet; - else if (tx.genesisID === "testnet-v1.0") - txLedger = Ledger.TestNet; - - if (!InternalMethods.checkValidAccount(tx.from, txLedger)){ - d.error = { - message: 'from is not a valid user account.' - } - reject(d); - return; - } + if(transactionWrap.validityObject && (Object.values(transactionWrap.validityObject).some(value => value['status'] === ValidationStatus.Warning )) + || (Object.values(transactionWrap.validityObject).some(value => value['status'] === ValidationStatus.Dangerous))) { + // Check for the validity of the from field. + d.body.params = transactionWrap; - if(transactionWrap.validityObject && (Object.values(transactionWrap.validityObject).some(value => value === ValidationResponse.Warning )) - || (Object.values(transactionWrap.validityObject).some(value => value === ValidationResponse.Dangerous))) { - d.body.params = transactionWrap.transaction; // We have a transaction which does not contain invalid fields, but does contain fields that are dangerous // or ones we've flagged as needing to be reviewed. We can use a modified popup to allow the normal flow, but require extra scrutiny. - extensionBrowser.windows.create({ url: extensionBrowser.runtime.getURL("index.html#/sign-transaction"), type: "popup", @@ -199,9 +184,8 @@ export class Task { }); } else { - d.body.params = transactionWrap.transaction; + d.body.params = transactionWrap; // We have a transaction which appears to be valid. Show the popup as normal. - extensionBrowser.windows.create({ url: extensionBrowser.runtime.getURL("index.html#/sign-transaction"), type: "popup", @@ -387,7 +371,7 @@ export class Task { lastRound, genesisID, genesisHash, - note } = message.body.params; + note } = message.body.params.transaction; let ledger switch (genesisID) { @@ -423,7 +407,14 @@ export class Task { var recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - let txn = {...message.body.params}; + let txn = {...message.body.params.transaction}; + + Object.keys({...message.body.params.transaction}).forEach(key => { + console.log(`Key: ${key}, Value:: ${txn[key]}`); + if(txn[key] === undefined || txn[key] === null){ + delete txn[key]; + } + }); if ('note' in txn && txn.note !== undefined) { txn.note = new Uint8Array(Buffer.from(txn.note)); diff --git a/packages/extension/src/background/transaction/acfgCreateTransaction.ts b/packages/extension/src/background/transaction/acfgCreateTransaction.ts new file mode 100644 index 00000000..47b5b958 --- /dev/null +++ b/packages/extension/src/background/transaction/acfgCreateTransaction.ts @@ -0,0 +1,40 @@ +import { IAssetCreateTx } from "@algosigner/common/interfaces/acfg_create"; +import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; + +/// +// Mapping, validation and error checking for acfg create transactions prior to sign. +/// +class AssetCreateTx implements IAssetCreateTx { + assetTotal: number = undefined; + assetDecimals: number = undefined; + type: string = undefined; + from: string = undefined; + fee: number = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + 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; + group?: any = null; + lease?: any = null; + rekey?: any = null; +}; + +/// +// Mapping, validation and error checking for acfg create transactions prior to sign. +/// +export class AssetCreateTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Create Asset'; + constructor(params: IAssetCreateTx){ + super(params, AssetCreateTx); + } +} \ No newline at end of file diff --git a/packages/extension/src/background/transaction/acfgDestroyTransaction.ts b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts new file mode 100644 index 00000000..23a69ee5 --- /dev/null +++ b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts @@ -0,0 +1,31 @@ +import { IAssetDestroyTx } from "@algosigner/common/interfaces/acfg_destroy"; +import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; + +/// +// Mapping, validation and error checking for acfg destroy transactions prior to sign. +/// +class AssetDestroyTx implements IAssetDestroyTx { + type: string = undefined; + assetIndex: number = undefined; + from: string = undefined; + fee: number = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; +}; + +/// +// Mapping, validation and error checking for acfg destroy transactions prior to sign. +/// +export class AssetDestroyTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Destroy Asset'; + constructor(params: IAssetDestroyTx){ + super(params, AssetDestroyTx); + + } +} \ No newline at end of file diff --git a/packages/extension/src/background/transaction/acfgTransaction.ts b/packages/extension/src/background/transaction/acfgTransaction.ts index 771f2fc4..61429ede 100644 --- a/packages/extension/src/background/transaction/acfgTransaction.ts +++ b/packages/extension/src/background/transaction/acfgTransaction.ts @@ -1,39 +1,49 @@ import { IAssetConfigTx } from "@algosigner/common/interfaces/acfg"; import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; +import { InvalidTransactionStructure } from "../../errors/validation" /// -// Mapping, validation and error checking for transaction acfg transactions prior to sign. +// Mapping, validation and error checking for acfg transactions prior to sign. /// class AssetConfigTx implements IAssetConfigTx { type: string = undefined; - assetIndex?: number = undefined; - assetTotal?: number = undefined; - assetDecimals?: number = undefined; - assetDefaultFrozen?: boolean = undefined; - assetUnitName?: string = undefined; - assetName?: string = undefined; - assetURL?: string = undefined; - assetMetadataHash?: any = undefined; - assetManager?: string = undefined; - assetReserve?: string = undefined; - assetFreeze?: string = undefined; - assetClawback?: string = undefined; - from?: string = undefined; + assetIndex: number = undefined; + from: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; - note?: string = undefined; + note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - group?: any = undefined; - lease?: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; + + // Modifications must include one of these + assetTotal?: number = 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 transaction pay transactions prior to sign. +// Mapping, validation and error checking for asset config transactions prior to sign. /// -export class AcfgTransaction extends BaseValidatedTxnWrap { - constructor(params: IAssetConfigTx){ - super(params, AssetConfigTx); +export class AssetConfigTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Modify Asset'; + constructor(params: IAssetConfigTx){ + super(params, AssetConfigTx, requiredAcfgParams); } } \ No newline at end of file diff --git a/packages/extension/src/background/transaction/actions.test.ts b/packages/extension/src/background/transaction/actions.test.ts index 34c0eedf..a2cb2326 100644 --- a/packages/extension/src/background/transaction/actions.test.ts +++ b/packages/extension/src/background/transaction/actions.test.ts @@ -1,5 +1,9 @@ import { getValidatedTxnWrap } from "./actions"; import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; +import { AssetConfigTransaction } from "./acfgTransaction"; +import { AssetDestroyTransaction } from "./acfgDestroyTransaction"; +import { AssetTransferTransaction } from "./axferTransaction"; +import { AssetFreezeTransaction } from "./afrzTransaction"; test('Valitdate build of pay transaction', () => { let preTransaction = { @@ -23,6 +27,7 @@ test('Valitdate build of keygreg transaction', () => { let preTransaction = { "type": "keyreg", "fee": 1000, + "from": "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", voteKey: "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", selectionKey: "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", voteFirst: 1, @@ -42,6 +47,8 @@ test('Valitdate build of acfg transaction', () => { let preTransaction = { "type": "acfg", "fee": 1000, + "assetIndex": 1, + "from": "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", "firstRound": 1, "lastRound": 1001, "genesisID": "testnet-v1.0", @@ -51,11 +58,13 @@ test('Valitdate build of acfg transaction', () => { let result = getValidatedTxnWrap(preTransaction, "acfg"); expect(result instanceof BaseValidatedTxnWrap).toBe(true); + expect(result instanceof AssetDestroyTransaction).toBe(true); }); test('Valitdate build of afrz transaction', () => { let preTransaction = { "type": "afrz", + "from": "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", "freezeAccount": "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ", "freezeState": true, "fee": 1000, @@ -69,6 +78,7 @@ test('Valitdate build of afrz transaction', () => { let result = getValidatedTxnWrap(preTransaction, "afrz"); expect(result instanceof BaseValidatedTxnWrap).toBe(true); + expect(result instanceof AssetFreezeTransaction).toBe(true); }); test('Valitdate build of axfer transaction', () => { @@ -88,6 +98,7 @@ test('Valitdate build of axfer transaction', () => { let result = getValidatedTxnWrap(preTransaction, "axfer"); expect(result instanceof BaseValidatedTxnWrap).toBe(true); + expect(result instanceof AssetTransferTransaction).toBe(true); }); test('Valitdate build of transaction', () => { @@ -96,3 +107,123 @@ test('Valitdate build of transaction', () => { } expect(() => getValidatedTxnWrap(preTransaction, "faketype")).toThrow(); }); +// Check missing fields from transactions in all types +test('Valitdate pay transaction required fields', () => { + let preTransaction = { + "type": "pay" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "pay"); + } + catch(e){ + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("to"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("amount"); +}); +test('Valitdate clawback transaction required fields', () => { + let preTransaction = { + "type": "axfer" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "axfer"); + } + catch(e){ + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("amount"); + expect(errorMessage).toContain("assetIndex"); + expect(errorMessage).toContain("assetRevocationTarget"); +}); +test('Valitdate accept transaction required fields', () => { + let preTransaction = { + "type": "axfer" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "axfer"); + } + catch(e){ + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("assetIndex"); +}); +test('Valitdate create transaction required fields', () => { + let preTransaction = { + "type": "acfg" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "acfg"); + } + catch(e){ + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("assetTotal"); + expect(errorMessage).toContain("assetDecimals"); +}); +test('Valitdate destroy transaction required fields', () => { + let preTransaction = { + "type": "acfg" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "acfg"); + } + catch(e){ + console.log(e.message); + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("assetIndex"); +}); +test('Valitdate modify asset transaction required fields', () => { + let preTransaction = { + "type": "acfg" + } + let errorMessage:string = undefined; + try { + getValidatedTxnWrap(preTransaction, "acfg"); + } + catch(e){ + errorMessage = e.message; + } + expect(errorMessage).toContain("fee"); + expect(errorMessage).toContain("firstRound"); + expect(errorMessage).toContain("lastRound"); + expect(errorMessage).toContain("genesisID"); + expect(errorMessage).toContain("genesisHash"); + expect(errorMessage).toContain("from"); + expect(errorMessage).toContain("assetIndex"); +}); diff --git a/packages/extension/src/background/transaction/actions.ts b/packages/extension/src/background/transaction/actions.ts index 65e2385b..0ccef00a 100644 --- a/packages/extension/src/background/transaction/actions.ts +++ b/packages/extension/src/background/transaction/actions.ts @@ -1,37 +1,109 @@ import { IPaymentTx } from "@algosigner/common/interfaces/pay"; import { IAssetConfigTx } from "@algosigner/common/interfaces/acfg"; +import { IAssetCreateTx } from "@algosigner/common/interfaces/acfg_create"; +import { IAssetDestroyTx } from "@algosigner/common/interfaces/acfg_destroy"; import { IAssetFreezeTx } from "@algosigner/common/interfaces/afrz"; import { IAssetTransferTx } from "@algosigner/common/interfaces/axfer"; +import { IAssetAcceptTx } from "@algosigner/common/interfaces/axfer_accept"; +import { IAssetClawbackTx } from "@algosigner/common/interfaces/axfer_clawback"; import { IKeyRegistrationTx } from "@algosigner/common/interfaces/keyreg"; +import { IApplTx } from "@algosigner/common/interfaces/appl"; import { PayTransaction } from "./payTransaction"; -import { AcfgTransaction } from "./acfgTransaction"; -import { AfrzTransaction } from "./afrzTransaction"; -import { AxferTransaction } from "./axferTransaction"; +import { AssetConfigTransaction } from "./acfgTransaction"; +import { AssetCreateTransaction } from "./acfgCreateTransaction"; +import { AssetDestroyTransaction } from "./acfgDestroyTransaction"; +import { AssetFreezeTransaction } from "./afrzTransaction"; +import { AssetTransferTransaction } from "./axferTransaction"; +import { AssetAcceptTransaction } from "./axferAcceptTransaction"; +import { AssetClawbackTransaction } from "./axferClawbackTransaction"; import { KeyregTransaction } from "./keyregTransaction"; +import { ApplTransaction } from "./applTransaction"; import { TransactionType } from "@algosigner/common/types/transaction"; import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; +import logging from "@algosigner/common/logging"; /// // Sign transaction and return. /// export function getValidatedTxnWrap(txn: object, type: string) { - let validatedTxnWrap: BaseValidatedTxnWrap; + let validatedTxnWrap: BaseValidatedTxnWrap = undefined; + let error:Error = undefined; switch(type.toLowerCase()){ case TransactionType.Pay: validatedTxnWrap = new PayTransaction(txn as IPaymentTx); break; case TransactionType.Acfg: - validatedTxnWrap = new AcfgTransaction(txn as IAssetConfigTx); + // Validate any of the 3 types of transactions that can occur with acfg + // Use the first error as the passback error. + try { + validatedTxnWrap = new AssetConfigTransaction(txn as IAssetConfigTx); + } + catch(e) { + error = e; + } + if(!validatedTxnWrap){ + try { + validatedTxnWrap = new AssetCreateTransaction(txn as IAssetCreateTx); + } + catch(e) { + e.message = [error.message, e.message].join(' '); + error = e; + } + } + if(!validatedTxnWrap){ + try { + validatedTxnWrap = new AssetDestroyTransaction(txn as IAssetDestroyTx); + } + catch(e) { + e.message = [error.message, e.message].join(' '); + error = e; + } + } + if(!validatedTxnWrap && error){ + throw error; + } break; case TransactionType.Afrz: - validatedTxnWrap = new AfrzTransaction(txn as IAssetFreezeTx); + validatedTxnWrap = new AssetFreezeTransaction(txn as IAssetFreezeTx); break; case TransactionType.Axfer: - validatedTxnWrap = new AxferTransaction(txn as IAssetTransferTx); + // Validate any of the 3 types of transactions that can occur with axfer + // Use the first error as the passback error. + try { + validatedTxnWrap = new AssetAcceptTransaction(txn as IAssetAcceptTx); + } + catch(e) { + error = e; + } + if(!validatedTxnWrap){ + try { + validatedTxnWrap = new AssetTransferTransaction(txn as IAssetTransferTx); + + } + catch(e) { + e.message = [error.message, e.message].join(' '); + error = e; + } + } + if(!validatedTxnWrap){ + try { + validatedTxnWrap = new AssetClawbackTransaction(txn as IAssetClawbackTx); + } + catch(e) { + e.message = [error.message, e.message].join(' '); + error = e; + } + } + if(!validatedTxnWrap && error){ + throw error; + } break; case TransactionType.Keyreg: - validatedTxnWrap = new KeyregTransaction(txn as IKeyRegistrationTx); + validatedTxnWrap = new KeyregTransaction(txn as IKeyRegistrationTx); + break; + case TransactionType.Appl: + validatedTxnWrap = new ApplTransaction(txn as IApplTx); break; default: throw new Error("Type of transaction not specified or known."); diff --git a/packages/extension/src/background/transaction/afrzTransaction.ts b/packages/extension/src/background/transaction/afrzTransaction.ts index aec9189f..42f9ea7b 100644 --- a/packages/extension/src/background/transaction/afrzTransaction.ts +++ b/packages/extension/src/background/transaction/afrzTransaction.ts @@ -2,28 +2,30 @@ import { IAssetFreezeTx } from "@algosigner/common/interfaces/afrz"; import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; /// -// Mapping, validation and error checking for transaction afrz transactions prior to sign. +// Mapping, validation and error checking for afrz transactions prior to sign. /// class AssetFreezeTx implements IAssetFreezeTx{ type: string = undefined; assetIndex: number = undefined; freezeAccount: string = undefined; freezeState: boolean = undefined; - from?: string = undefined; + from: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; - note?: string = undefined; + note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - group?: any = undefined; - lease?: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; }; /// -// Mapping, validation and error checking for transaction pay transactions prior to sign. +// Mapping, validation and error checking for afrz transactions prior to sign. /// -export class AfrzTransaction extends BaseValidatedTxnWrap { +export class AssetFreezeTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Freeze Asset'; constructor(params: IAssetFreezeTx){ super(params, AssetFreezeTx); } diff --git a/packages/extension/src/background/transaction/applTransaction.ts b/packages/extension/src/background/transaction/applTransaction.ts index 3bf91923..cbf453b9 100644 --- a/packages/extension/src/background/transaction/applTransaction.ts +++ b/packages/extension/src/background/transaction/applTransaction.ts @@ -1,9 +1,29 @@ -import { IAppl } from "@algosigner/common/interfaces/appl"; -import { ValidationResponse, Validate } from "../utils/validator"; +import { IApplTx } from "@algosigner/common/interfaces/appl"; +import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; /// -// Mapping, validation and error checking for transaction appl transactions prior to sign. +// Mapping, validation and error checking for appl transactions prior to sign. /// -export class ApplTransaction implements IAppl { - constructor(params: IAppl){} +export class ApplTx implements IApplTx { + type: string = undefined;; + apid: number = undefined;; + apan: number = undefined;; + apat?: string = null; + apap?: string = null; + apaa?: any = null; + apsu?: string = null; + apfa?: string = null; + apas?: string = null; + apgs?: any = null; + apls?: any = null; +} + +/// +// Mapping, validation and error checking for appl transactions prior to sign. +/// +export class ApplTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Application'; + constructor(params: IApplTx){ + super(params, ApplTx); + } } \ No newline at end of file diff --git a/packages/extension/src/background/transaction/axferAcceptTransaction.ts b/packages/extension/src/background/transaction/axferAcceptTransaction.ts new file mode 100644 index 00000000..2ae893c6 --- /dev/null +++ b/packages/extension/src/background/transaction/axferAcceptTransaction.ts @@ -0,0 +1,40 @@ +import { IAssetAcceptTx } from "@algosigner/common/interfaces/axfer_accept"; +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 AssetAcceptTx implements IAssetAcceptTx { + type: string = undefined; + assetIndex: number = undefined; + from: string = undefined; + fee: number = undefined; + to: any = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; + amount?: number = null; +} + +/// +// Mapping, validation and error checking for axfer accept transactions prior to sign. +/// +export class AssetAcceptTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Asset Opt-In'; + constructor(params: IAssetAcceptTx){ + super(params, AssetAcceptTx); + // Additional check to verify that amount is 0 and address from and to are the same + if(this.transaction && this.transaction['amount'] && this.transaction['amount'] != 0) { + throw new InvalidTransactionStructure(`Creation of AssetAcceptTx has an invalid amount.`); + } + if(this.transaction && this.transaction['to'] !== this.transaction['from']) { + throw new InvalidTransactionStructure(`Creation of AssetAcceptTx has non identical to and from fields.`); + } + } +} \ No newline at end of file diff --git a/packages/extension/src/background/transaction/axferClawbackTransaction.ts b/packages/extension/src/background/transaction/axferClawbackTransaction.ts new file mode 100644 index 00000000..3d4f28f8 --- /dev/null +++ b/packages/extension/src/background/transaction/axferClawbackTransaction.ts @@ -0,0 +1,33 @@ +import { IAssetClawbackTx } from "@algosigner/common/interfaces/axfer_clawback"; +import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; + +/// +// Base implementation of the transactions type interface, for use in the export wrapper class below. +/// +class AssetClawbackTx implements IAssetClawbackTx { + type: string = undefined; + assetIndex: number = undefined; + amount: number = undefined; + assetCloseTo?: string = undefined; + from: string = undefined; + fee: number = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; + assetRevocationTarget: string = undefined; +} + +/// +// Mapping, validation and error checking for axfer clawback transactions prior to sign. +/// +export class AssetClawbackTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Asset Clawback'; + constructor(params: IAssetClawbackTx){ + super(params, AssetClawbackTx); + } +} \ No newline at end of file diff --git a/packages/extension/src/background/transaction/axferTransaction.ts b/packages/extension/src/background/transaction/axferTransaction.ts index c19257fe..e75cc223 100644 --- a/packages/extension/src/background/transaction/axferTransaction.ts +++ b/packages/extension/src/background/transaction/axferTransaction.ts @@ -7,26 +7,27 @@ import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; class AssetTransferTx implements IAssetTransferTx { type: string = undefined; assetIndex: number = undefined; - amount?: number = undefined; + amount: number = undefined; to: string = undefined; - assetCloseTo?: string = undefined; - from?: string = undefined; + assetCloseTo?: string = null; + from: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; - note?: string = undefined; + note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - group?: any = undefined; - lease?: any = undefined; - + group?: any = null; + lease?: any = null; + rekey?: any = null; } /// -// Mapping, validation and error checking for transaction axfer transactions prior to sign. +// Mapping, validation and error checking for axfer transactions prior to sign. /// -export class AxferTransaction extends BaseValidatedTxnWrap { - constructor(params: IAssetTransferTx){ +export class AssetTransferTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Asset Transfer'; + constructor(params: IAssetTransferTx){ super(params, AssetTransferTx); } } \ No newline at end of file diff --git a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts index 566ff2f3..fd3ac6fa 100644 --- a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts +++ b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts @@ -1,5 +1,6 @@ import { Validate, ValidationResponse } from "../utils/validator"; import { logging } from "@algosigner/common/logging"; +import { InvalidTransactionStructure } from "../../errors/validation" // // Base validated transaction wrap. @@ -7,21 +8,61 @@ import { logging } from "@algosigner/common/logging"; export class BaseValidatedTxnWrap { transaction: any = undefined; validityObject: object = {}; - - constructor(params: any, txnType: any) { + txDerivedTypeText: string; + + constructor(params: any, txnType: any, requiredParamsSet: Array = undefined) { this.transaction = new txnType(); + var missingFields = []; + var extraFields = []; + + // Cycle base transaction fields for this type of transaction to verify require fields are present. + // Nullable type fields are being initialized to null instead of undefined. + Object.entries(this.transaction).forEach(([key, value]) => { + if(value === undefined && (params[key] === undefined || params[key] === null)){ + missingFields.push(key) + } + }); + + // Check required values in the case where one of a set is required. + if(requiredParamsSet && requiredParamsSet.length > 0){ + var foundValue = false; + for(let key in requiredParamsSet){ + if(params[key] !== undefined && params[key] !== null){ + foundValue = true; + break; + } + } + if(!foundValue){ + missingFields.push('params'); + } + } + + // Throwing error here so that missing fields can be combined. + if(missingFields.length > 0){ + throw new InvalidTransactionStructure(`Creation of ${txnType.name} has missing or invalid required properties: ${missingFields.toString()}.`) + } + + // Check the properties included versus the interface. Reject transactions with unknown fields. + for (let prop in params) { if(!(Object.keys(this.transaction).includes(prop))){ - throw new Error(`Transaction has additional unknown fields.`); - } - try { - this.transaction[prop] = params[prop]; - this.validityObject[prop] = Validate(prop, params[prop]) as ValidationResponse; + extraFields.push(prop); } - catch(e){ - logging.log(e); - throw new Error(`Transaction has encountered an unknown error while processing.`); + else { + try { + this.transaction[prop] = params[prop]; + this.validityObject[prop] = Validate(prop, params[prop]) as ValidationResponse; + } + catch(e){ + logging.log(e); + throw new Error(`Transaction has encountered an unknown error while processing.`); + } } } + + // Throwing error here so that extra fields can be combined. + if(extraFields.length > 0){ + throw new InvalidTransactionStructure(`Creation of ${txnType.name} has extra or invalid fields: ${extraFields.toString()}.`) + } } } diff --git a/packages/extension/src/background/transaction/keyregTransaction.ts b/packages/extension/src/background/transaction/keyregTransaction.ts index 45b3fed5..2f665c9b 100644 --- a/packages/extension/src/background/transaction/keyregTransaction.ts +++ b/packages/extension/src/background/transaction/keyregTransaction.ts @@ -10,22 +10,24 @@ class KeyRegistrationTx implements IKeyRegistrationTx { selectionKey: string = undefined; voteFirst: number = undefined; voteLast: number = undefined; - voteKeyDilution: number = undefined; - from?: string = undefined; + voteKeyDilution?: number = null; + from: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; - note?: string = undefined; + note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - group?: any = undefined; - lease?: any = undefined; + group?: any = null; + lease?: any = null; + rekey?: any = null; } /// -// Mapping, validation and error checking for transaction keyreg transactions prior to sign. +// Mapping, validation and error checking for keyreg transactions prior to sign. /// export class KeyregTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Key Registration'; constructor(params: IKeyRegistrationTx){ super(params, KeyRegistrationTx); } diff --git a/packages/extension/src/background/transaction/payTransaction.ts b/packages/extension/src/background/transaction/payTransaction.ts index fb1b1bc1..d97ca2a5 100644 --- a/packages/extension/src/background/transaction/payTransaction.ts +++ b/packages/extension/src/background/transaction/payTransaction.ts @@ -8,23 +8,24 @@ class PaymentTx implements IPaymentTx{ type: string = undefined; to: string = undefined; amount: number = undefined; - closeRemainderTo?: string = undefined; - rekey?: any = undefined; - from?: string = undefined; + closeRemainderTo?: string = null; + rekey?: any = null; + from: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; - note?: string = undefined; + note?: string = null; genesisID: string = undefined; genesisHash: any = undefined; - group?: any = undefined; - lease?: any = undefined; + group?: any = null; + lease?: any = null; }; /// -// Mapping, validation and error checking for transaction pay transactions prior to sign. +// Mapping, validation and error checking for pay transactions prior to sign. /// export class PayTransaction extends BaseValidatedTxnWrap { + txDerivedTypeText: string = 'Pay Algos'; constructor(params: IPaymentTx){ super(params, PaymentTx); } diff --git a/packages/extension/src/background/utils/validator.test.ts b/packages/extension/src/background/utils/validator.test.ts index 06c7f6d2..d626f019 100644 --- a/packages/extension/src/background/utils/validator.test.ts +++ b/packages/extension/src/background/utils/validator.test.ts @@ -1,75 +1,75 @@ -import { Validate, ValidationResponse } from "./validator"; +import { Validate, ValidationStatus } from "./validator"; test('Valitdate correct to address', () => { let result = Validate("to", "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ"); - expect(result).toBe(ValidationResponse.Valid); + expect(result.status).toBe(ValidationStatus.Valid); }); test('Valitdate invalid to address', () => { let result = Validate("to", "12345"); - expect(result).toBe(ValidationResponse.Invalid); + expect(result.status).toBe(ValidationStatus.Invalid); }); test('Valitdate correct amount', () => { let result = Validate("amount", 1); - expect(result).toBe(ValidationResponse.Valid); + expect(result.status).toBe(ValidationStatus.Valid); }); test('Valitdate invalid amount', () => { let result = Validate("amount", 9999999999999999999999999); - expect(result).toBe(ValidationResponse.Invalid); + expect(result.status).toBe(ValidationStatus.Invalid); }); test('Valitdate normal fee', () => { let result = Validate("fee", 1000); - expect(result).toBe(ValidationResponse.Valid); + expect(result.status).toBe(ValidationStatus.Valid); }); test('Valitdate elevated fee', () => { let result = Validate("fee", 100000); - expect(result).toBe(ValidationResponse.Warning); + expect(result.status).toBe(ValidationStatus.Warning); }); test('Valitdate very high fee', () => { let result = Validate("fee", 10000000); - expect(result).toBe(ValidationResponse.Dangerous); + expect(result.status).toBe(ValidationStatus.Dangerous); }); test('Valitdate closeRemainderTo empty', () => { let result = Validate("closeRemainderTo", ""); - expect(result).toBe(ValidationResponse.Valid); + expect(result.status).toBe(ValidationStatus.Valid); }); test('Valitdate closeRemainderTo', () => { let result = Validate("closeRemainderTo", "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ"); - expect(result).toBe(ValidationResponse.Dangerous); + expect(result.status).toBe(ValidationStatus.Dangerous); }); test('Valitdate assetCloseTo', () => { let result = Validate("assetCloseTo", "NM2MBC673SL7TQIKUXD4JOBR3XQITDCHIMIEODQBUGFMAN54QV2VUYWZNQ"); - expect(result).toBe(ValidationResponse.Dangerous); + expect(result.status).toBe(ValidationStatus.Dangerous); }); test('Valitdate correct assetIndex', () => { let result = Validate("assetIndex", 1); - expect(result).toBe(ValidationResponse.Valid); + expect(result.status).toBe(ValidationStatus.Valid); }); test('Valitdate invalid assetIndex', () => { let result = Validate("assetIndex", 9999999999999999999999999); - expect(result).toBe(ValidationResponse.Invalid); + expect(result.status).toBe(ValidationStatus.Invalid); }); test('Valitdate correct rounds', () => { let resultFirst = Validate("firstRound", 1); let resultLast = Validate("lastRound", 1); - expect(resultFirst).toBe(ValidationResponse.Valid); - expect(resultLast).toBe(ValidationResponse.Valid); + expect(resultFirst.status).toBe(ValidationStatus.Valid); + expect(resultLast.status).toBe(ValidationStatus.Valid); }); test('Valitdate invalid rounds', () => { let resultFirst = Validate("firstRound", 9999999999999999999999999); let resultLast = Validate("lastRound", 9999999999999999999999999); - expect(resultFirst).toBe(ValidationResponse.Invalid); - expect(resultLast).toBe(ValidationResponse.Invalid); + expect(resultFirst.status).toBe(ValidationStatus.Invalid); + expect(resultLast.status).toBe(ValidationStatus.Invalid); }); \ No newline at end of file diff --git a/packages/extension/src/background/utils/validator.ts b/packages/extension/src/background/utils/validator.ts index d78d1246..9e1a7d33 100644 --- a/packages/extension/src/background/utils/validator.ts +++ b/packages/extension/src/background/utils/validator.ts @@ -1,29 +1,63 @@ const algosdk = require("algosdk"); +import { getSupportedLedgers } from "@algosigner/common/types/ledgers"; /// -// Validation responses. +// Validation Status /// -export enum ValidationResponse { +export enum ValidationStatus { Valid = 0, // Field is valid or not one of the validated fields Invalid = 1, // Field value is invalid and should not be used Warning = 2, // Field is out of normal parameters and should be inspected closely Dangerous = 3 // Field has risky or costly fields with values and should be inspected very closely } +/// +// 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. +/// +export class ValidationResponse { + status: ValidationStatus; + info: string; + className: string; + constructor(props) { + this.status = props.status; + this.info = props.info; + this.className = _convertFieldResponseToClassname(this.status); + } +} /// // Return field if valid based on type. /// export function Validate(field: any, value: any): ValidationResponse { switch(field) { - // Validate the address is accurate + // Validate the addresses are accurate case "to": if(!algosdk.isValidAddress(value)) { - return ValidationResponse.Invalid; + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Address does not adhear to a valid structure.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } - + case "from": + if(!algosdk.isValidAddress(value)) { + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Address does not adhear to a valid structure.'}); + } + else { + return new ValidationResponse({status:ValidationStatus.Valid}); + } case "amount": case "assetIndex": case "firstRound": @@ -32,63 +66,82 @@ export function Validate(field: any, value: any): ValidationResponse { case "voteLast": case "voteKeyDilution": if (value && (!Number.isSafeInteger(value) || parseInt(value) < 0)){ - return ValidationResponse.Invalid; + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } // Warn on fee amounts above minimum, send dangerous response on those above 1 Algo. case "fee": try { - if(value && (!Number.isSafeInteger(value) || parseInt(value) < 0)) { - return ValidationResponse.Invalid; + console.log(`Getting ready to test fee: ${value}, type:${typeof value}`); + if(!Number.isSafeInteger(value) || parseInt(value) < 0) { + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); } - else if(value && parseInt(value) > 1000000) { - return ValidationResponse.Dangerous; + else if(parseInt(value) > 1000000) { + return new ValidationResponse({status:ValidationStatus.Dangerous, info:'The associated fee is very high compared to the minimum value.'}); } - else if(value && parseInt(value) > 1000) { - return ValidationResponse.Warning; + else if(parseInt(value) > 1000) { + return new ValidationResponse({status:ValidationStatus.Warning, info:'The fee is higher than the minimum value.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } } catch { // For any case where the parse int may fail. - return ValidationResponse.Invalid; + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Value unable to be cast correctly to a numeric value.'}); } // Close to types should issue a Dangerous validation warning if they contain values. case "closeRemainderTo": if(value) { - return ValidationResponse.Dangerous; + return new ValidationResponse({status:ValidationStatus.Dangerous, info:'A close to address is associated to this transaction.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } case "assetCloseTo": if(value) { - return ValidationResponse.Dangerous; + return new ValidationResponse({status:ValidationStatus.Dangerous, info:'A close to address is associated to this transaction.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } case "rekey": if(value) { - return ValidationResponse.Invalid; + return new ValidationResponse({status:ValidationStatus.Invalid, info:'Rekey transactions are not currently accepted in AlgoSigner.'}); } else { - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } + + // Genesis ID must be present and one of the approved values + case "genesisID": + if(getSupportedLedgers().some(l => value === l["genesisId"])){ + return new ValidationResponse({status:ValidationStatus.Valid}); + } + else { + return new ValidationResponse({status:ValidationStatus.Invalid, info:'The associated genesis id does not match a supported ledger.'}); + } + // Genesis hash must be present and one of the approved values + case "genesisHash": + if(value) { + return new ValidationResponse({status:ValidationStatus.Valid}); + } + else { + return new ValidationResponse({status:ValidationStatus.Invalid, info:'The genesis hash value is invalid or does not exist.'}); + } + default: // Our field isn't one of the listed ones, so we can mark it as valid - return ValidationResponse.Valid; + return new ValidationResponse({status:ValidationStatus.Valid}); } // If for some reason the case falls through mark the field invalid. - return ValidationResponse.Invalid; + return new ValidationResponse({status:ValidationStatus.Invalid, info:'An unknown error has occurred during validation.'}); } \ No newline at end of file diff --git a/packages/extension/src/errors/validation.test.ts b/packages/extension/src/errors/validation.test.ts new file mode 100644 index 00000000..9c3f0491 --- /dev/null +++ b/packages/extension/src/errors/validation.test.ts @@ -0,0 +1,8 @@ +import { InvalidTransactionStructure } from './validation'; + +test('Invalid transaction', () => { + function throwError () { + throw new InvalidTransactionStructure("Test Message"); + } + expect(() => throwError()).toThrow(InvalidTransactionStructure); +}); \ No newline at end of file diff --git a/packages/extension/src/errors/validation.ts b/packages/extension/src/errors/validation.ts new file mode 100644 index 00000000..dc3ad5ab --- /dev/null +++ b/packages/extension/src/errors/validation.ts @@ -0,0 +1,7 @@ +export class InvalidTransactionStructure extends Error { + constructor(message?: any) { + message ? super(message) : super(); + this.name = 'InvalidTransactionStructure'; + Error.captureStackTrace(this, InvalidTransactionStructure); + } +} \ No newline at end of file diff --git a/packages/ui/src/components/AccountPreview.ts b/packages/ui/src/components/AccountPreview.ts index d1a2196f..94e581ea 100644 --- a/packages/ui/src/components/AccountPreview.ts +++ b/packages/ui/src/components/AccountPreview.ts @@ -33,8 +33,10 @@ const AccountPreview: FunctionalComponent = (props: any) => { ${ account.name }
- ${ results && html` + ${ results && results.assets && html` ${results.assets.length} ASAs
+ `} + ${ results && html` ${numFormat(results.amount/1e6, 6)} Algos `} ${ results===null && html` diff --git a/packages/ui/src/components/SignTransaction/TxAcfg.ts b/packages/ui/src/components/SignTransaction/TxAcfg.ts index 90df2fe5..7f96bd53 100644 --- a/packages/ui/src/components/SignTransaction/TxAcfg.ts +++ b/packages/ui/src/components/SignTransaction/TxAcfg.ts @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks'; const TxAcfg: FunctionalComponent = (props: any) => { const [tab, setTab] = useState('overview'); - const { tx, account } = props; + const { tx, account, vo, dt } = props; const txText = JSON.stringify(tx, null, 2); @@ -20,7 +20,7 @@ const TxAcfg: FunctionalComponent = (props: any) => {
-

Asset configuration

+

${dt || 'Asset Configuration'}

    @@ -67,7 +67,7 @@ const TxAcfg: FunctionalComponent = (props: any) => {

    ${tx.assetTotal}

`} -

Fee: ${tx.fee/1e6} Algos

+

Fee: ${tx.fee/1e6} Algos

`} ${ tab==="details" && html` diff --git a/packages/ui/src/components/SignTransaction/TxAfrz.ts b/packages/ui/src/components/SignTransaction/TxAfrz.ts index a2f6e318..cd10f72c 100644 --- a/packages/ui/src/components/SignTransaction/TxAfrz.ts +++ b/packages/ui/src/components/SignTransaction/TxAfrz.ts @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks'; const TxAfrz: FunctionalComponent = (props: any) => { const [tab, setTab] = useState('overview'); - const { tx, account, ledger } = props; + const { tx, account, ledger, vo } = props; const txText = JSON.stringify(tx, null, 2); @@ -58,7 +58,7 @@ const TxAfrz: FunctionalComponent = (props: any) => { ${tx.assetIndex} -
+

Fee:

${tx.fee/1e6} Algos

diff --git a/packages/ui/src/components/SignTransaction/TxAlert.ts b/packages/ui/src/components/SignTransaction/TxAlert.ts new file mode 100644 index 00000000..3268e897 --- /dev/null +++ b/packages/ui/src/components/SignTransaction/TxAlert.ts @@ -0,0 +1,41 @@ +import { html } from 'htm/preact'; +import { FunctionalComponent, VNode } from "preact"; +import logging from "@algosigner/common/logging"; + + +const TxAlert: FunctionalComponent = (props: any) => { + const { vo } = props; + + // Using this structure so that any additional modifications for displaying + // warning and dangerous alerts separately can be handled more easily + var dangerList:Array> = [] + var warningList:Array> = [] + if(vo){ + Object.keys(vo).forEach(key => + { + if(vo[key]['status'] === 3) { + dangerList.push(html`${key}: ${vo[key]['info']}\n`) + } + else if(vo[key]['status'] === 2){ + warningList.push(html`${key}: ${vo[key]['info']}\n`) + } + }); + } + return html` +
+ ${(dangerList.length > 0) && html` +
+
Dangerous Fields Detected:
+

${dangerList}

+
` + } + ${(warningList.length > 0) && html` +
+
Risky Fields Detected:
+

${warningList}

+
+ `} +
`; +} + +export default TxAlert \ No newline at end of file diff --git a/packages/ui/src/components/SignTransaction/TxAxfer.ts b/packages/ui/src/components/SignTransaction/TxAxfer.ts index 7e28a892..0f286f84 100644 --- a/packages/ui/src/components/SignTransaction/TxAxfer.ts +++ b/packages/ui/src/components/SignTransaction/TxAxfer.ts @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks'; const TxAxfer: FunctionalComponent = (props: any) => { const [tab, setTab] = useState('overview'); - const { tx, account, ledger } = props; + const { tx, account, ledger, vo, dt } = props; const txText = JSON.stringify(tx, null, 2); @@ -20,7 +20,7 @@ const TxAxfer: FunctionalComponent = (props: any) => {
-

Asset Transfer

+

${dt || "Asset Transfer"}

@@ -53,7 +53,7 @@ const TxAxfer: FunctionalComponent = (props: any) => { ${tx.assetIndex}
-
+

Fee:

${tx.fee/1e6} Algos

diff --git a/packages/ui/src/components/SignTransaction/TxPay.ts b/packages/ui/src/components/SignTransaction/TxPay.ts index 43741a7f..c2ed965b 100644 --- a/packages/ui/src/components/SignTransaction/TxPay.ts +++ b/packages/ui/src/components/SignTransaction/TxPay.ts @@ -4,7 +4,7 @@ import { useState } from 'preact/hooks'; const TxPay: FunctionalComponent = (props: any) => { const [tab, setTab] = useState('overview'); - const { tx, account } = props; + const { tx, account, vo } = props; const txText = JSON.stringify(tx, null, 2); @@ -20,7 +20,7 @@ const TxPay: FunctionalComponent = (props: any) => {
-

Payment

+

Payment

@@ -49,7 +49,7 @@ const TxPay: FunctionalComponent = (props: any) => {

Sending:

${tx.amount/1e6} Algos

-
+

Fee:

${tx.fee/1e6} Algos

diff --git a/packages/ui/src/pages/SignTransaction.ts b/packages/ui/src/pages/SignTransaction.ts index 539c1a56..c7ae27a5 100644 --- a/packages/ui/src/pages/SignTransaction.ts +++ b/packages/ui/src/pages/SignTransaction.ts @@ -7,6 +7,7 @@ import { isFromExtension } from '@algosigner/common/utils'; import TxAcfg from 'components/SignTransaction/TxAcfg' import TxPay from 'components/SignTransaction/TxPay' +import TxAlert from 'components/SignTransaction/TxAlert' import TxKeyreg from 'components/SignTransaction/TxKeyreg' import TxAxfer from 'components/SignTransaction/TxAxfer' import TxAfrz from 'components/SignTransaction/TxAfrz' @@ -83,7 +84,7 @@ const SignTransaction: FunctionalComponent = (props) => { } if (request.body) { - let tx = request.body.params; + let tx = request.body.params.transaction; // Search for account let txLedger; if (tx.genesisID === "mainnet-v1.0") @@ -105,7 +106,7 @@ const SignTransaction: FunctionalComponent = (props) => {
-
+
${ request.body && html`
@@ -117,22 +118,24 @@ const SignTransaction: FunctionalComponent = (props) => {
- +
+ ${ request.body.params.validityObject && html`<${TxAlert} vo=${request.body.params.validityObject} />` } +
- ${ request.body.params.type==="pay" && html` - <${TxPay} tx=${request.body.params} account=${account} ledger=${ledger} /> + ${ request.body.params.transaction.type==="pay" && html` + <${TxPay} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} account=${account} ledger=${ledger} /> `} - ${ request.body.params.type==="keyreg" && html` - <${TxKeyreg} tx=${request.body.params} account=${account} ledger=${ledger} /> + ${ request.body.params.transaction.type==="keyreg" && html` + <${TxKeyreg} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} account=${account} ledger=${ledger} /> `} - ${ request.body.params.type==="acfg" && html` - <${TxAcfg} tx=${request.body.params} account=${account} ledger=${ledger} /> + ${ request.body.params.transaction.type==="acfg" && html` + <${TxAcfg} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} dt=${request.body.params.txDerivedTypeText} account=${account} ledger=${ledger} /> `} - ${ request.body.params.type==="axfer" && html` - <${TxAxfer} tx=${request.body.params} account=${account} ledger=${ledger} /> + ${ request.body.params.transaction.type==="axfer" && html` + <${TxAxfer} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} dt=${request.body.params.txDerivedTypeText} account=${account} ledger=${ledger} /> `} - ${ request.body.params.type==="afrz" && html` - <${TxAfrz} tx=${request.body.params} account=${account} ledger=${ledger} /> + ${ request.body.params.transaction.type==="afrz" && html` + <${TxAfrz} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} account=${account} ledger=${ledger} /> `}
`} diff --git a/packages/ui/src/styles.scss b/packages/ui/src/styles.scss index 641246cf..b2063031 100644 --- a/packages/ui/src/styles.scss +++ b/packages/ui/src/styles.scss @@ -81,3 +81,27 @@ $modal-close-top: 8px; border-bottom-color: #94b1c3; border-left-color: #94b1c3; } + +#warning-tx-list { + border-left: 5px solid rgb(255,215,0); + background-color: rgb(255,215,0,.1); + padding: 5px; + margin: 5px 0px 5px 0px; +} + +#danger-tx-list { + border-left: 5px solid rgb(255,0,0); + background-color: rgb(255,0,0,.1); + padding: 5px; + margin: 5px 0px 5px 0px; +} + +.tx-field-warning { + border: solid 1px rgb(255,215,0); + background-color: rgb(255,215,0,.1); +} + +.tx-field-danger { + border: solid 1px rgb(255,215,0); + background-color: rgb(255,0,0,.1); +} \ No newline at end of file From d93067957e8b6a9836a45f290f21cf722c198ab0 Mon Sep 17 00:00:00 2001 From: Tim Baldwin Date: Mon, 26 Oct 2020 13:38:00 -0400 Subject: [PATCH 11/45] Broke tests up, added asset create, opt-in --- .../test-project/tests/basic-e2e-dapp.test.js | 424 +++++++++++++++--- 1 file changed, 362 insertions(+), 62 deletions(-) diff --git a/packages/test-project/tests/basic-e2e-dapp.test.js b/packages/test-project/tests/basic-e2e-dapp.test.js index 7d8d15c5..446684ba 100644 --- a/packages/test-project/tests/basic-e2e-dapp.test.js +++ b/packages/test-project/tests/basic-e2e-dapp.test.js @@ -5,16 +5,23 @@ */ const testNetAccount = "E2E-Tests" // for now, also hardcoding in the regex match for account info, cannot interpolate variables in toMatch +const optInAccount = "Opt-In" const sendAlgoToAddress = "AEC4WDHXCDF4B5LBNXXRTB3IJTVJSWUZ4VJ4THPU2QGRJGTA3MIDFN3CQA" const testAccountAddress = "MTHFSNXBMBD4U46Z2HAYAOLGD2EV6GQBPXVTL727RR3G44AJ3WVFMZGSBE" +const optInAddress = "RMY2R35P7PHRBONNKNGDMP75R5DFLRILGESHCNESM22EZ77TQL5GBF75GI" const unsafePassword = 'c5brJp5f' +const samplePage = 'https://google.com/' // Prefer about:blank, bug in puppeteer +let getParams // holds the parameters for all txn +let appPage // re-using one window + describe('Wallet Setup', () => { const extensionName = 'AlgoSigner' const extensionPopupHtml = 'index.html' const unsafeMenmonic = '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' + const unsafeOptInMenmonic = 'space merge shoot airport soup stairs shuffle select absurd remain december neglect unfold portion forward december genuine tonight choice volume gospel save corn about few' const amount = Math.floor((Math.random() * 10)+.1); // txn size, modify multiplier for bulk const secondTestNetAccount = "Created-Account" @@ -26,7 +33,7 @@ describe('Wallet Setup', () => { beforeAll( async () => { const dummyPage = await browser.newPage(); - await dummyPage.waitFor(2000); // arbitrary wait time. + await dummyPage.waitForTimeout(2000); // arbitrary wait time. const targets = await browser.targets(); const extensionTarget = targets.find(({ _targetInfo }) => { @@ -65,60 +72,76 @@ describe('Wallet Setup', () => { await extensionPage.waitForSelector('#createWallet') await extensionPage.type('#setPassword',unsafePassword); await extensionPage.type('#confirmPassword',unsafePassword); - await extensionPage.waitFor(2000) + await extensionPage.waitForTimeout(2000) await extensionPage.waitForSelector('#createWallet') await extensionPage.click('#createWallet') }) test('Switch Ledger', async () => { - await extensionPage.waitFor(2000) + await extensionPage.waitForTimeout(2000) await extensionPage.screenshot({path: 'screenshots/test_waiting_for_page.png'}) await extensionPage.click('#selectLedger') - await extensionPage.waitFor(500) + await extensionPage.waitForTimeout(500) await extensionPage.click('#selectTestNet') }) - test('Import Account', async () => { + test('Import Base Account', async () => { await extensionPage.waitForSelector('#addAccount') await extensionPage.click('#addAccount') await extensionPage.waitForSelector('#importAccount') await extensionPage.click('#importAccount') await extensionPage.waitForSelector('#accountName') await extensionPage.type('#accountName',testNetAccount); - await extensionPage.waitFor(100) + await extensionPage.waitForTimeout(100) await extensionPage.type('#enterMnemonic', unsafeMenmonic) - await extensionPage.waitFor(100) + await extensionPage.waitForTimeout(100) await extensionPage.click('#nextStep') await extensionPage.waitForSelector('#enterPassword') await extensionPage.type('#enterPassword',unsafePassword); - await extensionPage.waitFor(200) + await extensionPage.waitForTimeout(200) await extensionPage.click('#authButton') - await extensionPage.waitFor(1000) + await extensionPage.waitForTimeout(1000) + }) + + test('Load Home after Account Import', async () => { + await extensionPage.waitForTimeout(2000) // loading the account takes time }) + test('Import OptIn Account', async () => { + await extensionPage.click('a.mr-2') + await extensionPage.waitForSelector('#addAccount') + await extensionPage.click('#addAccount') + await extensionPage.waitForSelector('#importAccount') + await extensionPage.click('#importAccount') + await extensionPage.waitForSelector('#accountName') + await extensionPage.type('#accountName',optInAccount); + await extensionPage.waitForTimeout(100) + await extensionPage.type('#enterMnemonic', unsafeOptInMenmonic) + await extensionPage.waitForTimeout(100) + await extensionPage.click('#nextStep') + await extensionPage.waitForSelector('#enterPassword') + await extensionPage.type('#enterPassword',unsafePassword); + await extensionPage.waitForTimeout(200) + await extensionPage.click('#authButton') + await extensionPage.waitForTimeout(1000) + }) }) -describe('Basic dApp Tests', () => { +describe('Basic dApp Setup and GET Tests', () => { - const sampleDapp = 'https://purestake.github.io/algosigner-dapp-example/legacy/index.html' - const samplePage = 'https://purestake.com' - let getParams - let appPage let connected let getStatus - let getSignedBlob - let getTxId - jest.setTimeout(10000); + jest.setTimeout(30000); beforeAll( async () => { appPage = await browser.newPage(); - await appPage.goto(sampleDapp); - await appPage.waitFor(1000) + await appPage.goto(samplePage); + await appPage.waitForTimeout(1000); }) afterAll(async () => { - appPage.close() + // appPage.close() }) test('Connect Dapp through content.js', async () => { @@ -126,10 +149,12 @@ describe('Basic dApp Tests', () => { connected = await appPage.evaluate( () => { AlgoSigner.connect() }); - - await appPage.waitFor(2000) + + await appPage.waitForTimeout(2000); const pages = await browser.pages(); const popup = pages[pages.length-1]; + + await popup.waitForSelector("#grantAccess"); await popup.click("#grantAccess"); @@ -141,15 +166,15 @@ describe('Basic dApp Tests', () => { return Promise.resolve( AlgoSigner.accounts({ledger: 'TestNet'}) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`AlgoSigner Testnet Account reached: ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error connecting ${e}`); })) }) + console.log(`AlgoSigner Testnet Account reached: ${getAccounts[0].address}`) expect(getAccounts[0].address).toMatch(testAccountAddress) }) @@ -163,15 +188,15 @@ describe('Basic dApp Tests', () => { path: '/v2/transactions/params' }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`TestNet transaction params: ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error fetching params ${e}`); })); }) + console.log(`TestNet transaction params: ${getParams["consensus-version"]}`) expect(getParams).toHaveProperty('consensus-version') expect(getParams).toHaveProperty('fee') expect(getParams.fee).toEqual(0) @@ -191,15 +216,15 @@ describe('Basic dApp Tests', () => { path: '/v2/status' }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`TestNet status: ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error fetching status ${e}`); })); }) + console.log(`TestNet status: ${getStatus["last-round"]}`) expect(getStatus).toHaveProperty('time-since-last-round') expect(getStatus).toHaveProperty('last-round') expect(getStatus).toHaveProperty('last-version') @@ -219,15 +244,15 @@ describe('Basic dApp Tests', () => { path: '/v2/ledger/supply' }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`TestNet supply ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error fetching supply ${e}`); })); }) + console.log(`TestNet supply: ${getLedgerSupply.current_round}`) expect(getLedgerSupply).toHaveProperty('current_round') expect(getLedgerSupply).toHaveProperty('online-money') expect(getLedgerSupply).toHaveProperty('total-money') @@ -246,16 +271,16 @@ describe('Basic dApp Tests', () => { path: '/v2/assets/150821' }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`TestNet asset info: ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error fetching asset: ${e}`); })); }) - await appPage.waitFor(2000) + await appPage.waitForTimeout(2000) + console.log(`TestNet asset info: ${getAnAsset['asset']['params']['unit-name']}`) expect(getAnAsset['asset']['params']['unit-name']).toMatch('dectest') expect(getAnAsset.asset.params.name).toMatch('decimal Test') expect(getAnAsset['asset']['params']['default-frozen']).toBe(false) @@ -267,11 +292,42 @@ describe('Basic dApp Tests', () => { expect(getAnAsset.asset.params.manager).toMatch(ownerAccount) expect(getAnAsset.asset.params.reserve).toMatch(ownerAccount) expect(getAnAsset.asset.index).toEqual(assetIndex) - - console.log(getAnAsset) }) + test('GET: Asset list limited', async () =>{ + const shortAssetList = await appPage.evaluate( () => { + + return Promise.resolve( + AlgoSigner.indexer({ + ledger: 'TestNet', + path: '/v2/assets?limit=2' + }) + .then((d) => { + console.log(`TestNet asset list: ${d}`) + return d; + }) + .catch((e) => { + console.error(`Error fetching asset list: ${e}`); + })); + + }) + + await appPage.waitForTimeout(200) + console.log(`TestNet asset list: ${shortAssetList.assets[0].index}`) + expect(shortAssetList.assets.length).toEqual(2) + expect(shortAssetList.assets[0].index).toEqual(185) + }) + +}) + +describe('dApp POST Txn Tests (plus Teal compile)', () => { + let getSignedBlob + let getTxId + let matchTxId + let assetTxId + let assetIndex + test('POST: Compile Teal', async () => { const tealCompiled = await appPage.evaluate( () => { @@ -285,49 +341,293 @@ describe('Basic dApp Tests', () => { contentType: 'text/plain' }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; + console.log(`TestNet Teal compile: ${d}`) return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; + console.error(`Error compiling Teal: ${e}`); })); }) - await appPage.waitFor(200) - console.log(tealCompiled) + await appPage.waitForTimeout(200) + console.log(`TestNet Teal compile: ${tealCompiled.result}`) expect(tealCompiled.result).toMatch("ASABACI=") }) + test('Send Tx', async () => { + // Create a function to map to the puppeteer object + async function autosign () { + await appPage.waitForTimeout(1000); + const pages = await browser.pages(); + var popup = pages[pages.length-1]; + await popup.waitForSelector("#approveTx"); + await popup.click('#approveTx', {waitUntil: 'networkidle2'}); + await popup.waitForSelector("#enterPassword"); + await popup.type('#enterPassword',unsafePassword); + await popup.waitForSelector("#authButton"); + await popup.click('#authButton', {waitUntil: 'networkidle2'}); + } + + // Attach the function to the dapp page for use after popup + await appPage.exposeFunction("autosign", autosign); + + getSignedBlob = await appPage.evaluate(async (testAccountAddress, getParams, sendAlgoToAddress) => { + const amount = Math.floor(Math.random() * 10); + let txn = { + "from": testAccountAddress, + "to": sendAlgoToAddress, + "fee": getParams['fee'], + "amount": amount, + "type": "pay", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "test string note" + }; + + // Get only the promise first + var signPromise = AlgoSigner.sign(txn); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + }, testAccountAddress, getParams, sendAlgoToAddress); + console.log(typeof(getSignedBlob)) + console.log(`Confirmation: ${JSON.stringify(getSignedBlob)}`) + expect(getSignedBlob).toHaveProperty("txID"); + expect(getSignedBlob).toHaveProperty("blob"); + matchTxId = getSignedBlob.txID; + }) - test('GET: Asset list limited', async () =>{ - const shortAssetList = await appPage.evaluate( () => { - - return Promise.resolve( - AlgoSigner.indexer({ + test('Post Signed Blob', async () => { + getTxId = await appPage.evaluate( (getSignedBlob) => { + return AlgoSigner.send({ ledger: 'TestNet', - path: '/v2/assets?limit=2' + tx: getSignedBlob.blob }) .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; return d; }) .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; - })); + return(e) + }); + }, getSignedBlob); - }) - - await appPage.waitFor(200) - console.log(shortAssetList) - expect(shortAssetList.assets.length).toEqual(2) - expect(shortAssetList.assets[0].index).toEqual(185) + console.log(`Confirmation: ${JSON.stringify(getTxId)}`) + expect(getTxId.txId).toEqual(matchTxId) }) + test('Overspend Tx', async () => { + await appPage.waitForTimeout(1000); + getSignedBlob = await appPage.evaluate(async (testAccountAddress, getParams, sendAlgoToAddress) => { + const amount = 900000000 + Math.floor(Math.random() * 10); + let txn = { + "from": testAccountAddress, + "to": sendAlgoToAddress, + "fee": getParams['fee'], + "amount": amount, + "type": "pay", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Very large tx" + }; + + // Get only the promise first + var signPromise = AlgoSigner.sign(txn); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + + }, testAccountAddress, getParams, sendAlgoToAddress); + + console.log(`Blob 2: ${getSignedBlob.blob}`); + expect(getSignedBlob).toHaveProperty("txID"); + expect(getSignedBlob).toHaveProperty("blob"); + }) + + test('Post Overspend Blob', async () => { + getTxId = await appPage.evaluate( (getSignedBlob) => { + return AlgoSigner.send({ + ledger: 'TestNet', + tx: getSignedBlob.blob + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + }, getSignedBlob); + + expect(getTxId.message).toMatch("overspend") + }) }) +describe('dApp Asset Txn Tests', () => { + let getSignedBlob + let getTxId + let assetTxId + let assetIndex + + test('Asset Create Tx', async () => { + await appPage.waitForTimeout(1000); + getSignedBlob = await appPage.evaluate(async (testAccountAddress, getParams, sendAlgoToAddress) => { + let txn = { + "from": testAccountAddress, + "fee": getParams['fee'], + "assetName": 'AutoTest', + "assetUnitName": 'AT', + "assetTotal": 1000 + Math.round(Math.floor(Math.random() * 10)), + "assetDecimals": 2, + "type": "acfg", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset create" + }; + + // Get only the promise first + var signPromise = AlgoSigner.sign(txn); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + + }, testAccountAddress, getParams, sendAlgoToAddress); - + console.log(`Blob: ${getSignedBlob.blob}`); + expect(getSignedBlob).toHaveProperty("txID"); + expect(getSignedBlob).toHaveProperty("blob"); + assetTxId = getSignedBlob.txID + }) + + test('Post Asset Create Blob', async () => { + getTxId = await appPage.evaluate( (getSignedBlob) => { + return AlgoSigner.send({ + ledger: 'TestNet', + tx: getSignedBlob.blob + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + }, getSignedBlob); + + expect(getTxId.txId).toMatch(assetTxId) + }) + test('Verify Asset Pending Tx', async () => { + + let lastRound = getParams['last-round'] + let pendingTx = await appPage.evaluate(async (assetTxId, lastRound) => { + + let validateCheck = true; + let pendingCheck; + + while(validateCheck) { + + pendingCheck = await AlgoSigner.algod({ + ledger: 'TestNet', + path: '/v2/transactions/pending/' + assetTxId + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + + if("asset-index" in pendingCheck) { + validateCheck = false; + } + else { + lastRound++; + + await AlgoSigner.algod({ + ledger: 'TestNet', + path: '/v2/status/wait-for-block-after/' + lastRound + }) + .catch((e) => { + return(e) + }); + } + } + + return pendingCheck; + }, assetTxId, lastRound); + + assetIndex = pendingTx["asset-index"] + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + }) + + test('Asset OptIn Tx', async () => { + await appPage.waitForTimeout(1000); + getSignedBlob = await appPage.evaluate(async (optInAddress, getParams, assetIndex) => { + let txn = { + "from": optInAddress, + "to": optInAddress, + "amount": 0, + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "axfer", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Optin" + }; + + // Get only the promise first + var signPromise = AlgoSigner.sign(txn); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + + }, optInAddress, getParams, assetIndex); + + expect(getSignedBlob).toHaveProperty("txID"); + expect(getSignedBlob).toHaveProperty("blob"); + console.log(JSON.stringify(getSignedBlob)) + assetTxId = getSignedBlob.txID + }) + + test('Post Asset Opt-in Blob', async () => { + getTxId = await appPage.evaluate( (getSignedBlob) => { + return AlgoSigner.send({ + ledger: 'TestNet', + tx: getSignedBlob.blob + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + }, getSignedBlob); + + if("message" in getTxId) { + console.log('Likely OVERSPEND - ADD FUNDS TO optInAddress') + console.log(JSON.stringify(getTxId)) + } + else { + expect(getTxId.txId).toMatch(assetTxId) } + + }) +}) \ No newline at end of file From 0c30f00178ab856af9fb2655b2d3fb53b5a01c67 Mon Sep 17 00:00:00 2001 From: Tim Baldwin Date: Mon, 26 Oct 2020 13:55:01 -0400 Subject: [PATCH 12/45] Added Asset transfer --- .../test-project/tests/basic-e2e-dapp.test.js | 56 +++++ .../tests/manual-e2e-dapp.test.js | 227 ------------------ 2 files changed, 56 insertions(+), 227 deletions(-) delete mode 100644 packages/test-project/tests/manual-e2e-dapp.test.js diff --git a/packages/test-project/tests/basic-e2e-dapp.test.js b/packages/test-project/tests/basic-e2e-dapp.test.js index 446684ba..13bf56d8 100644 --- a/packages/test-project/tests/basic-e2e-dapp.test.js +++ b/packages/test-project/tests/basic-e2e-dapp.test.js @@ -627,7 +627,63 @@ describe('dApp Asset Txn Tests', () => { } else { expect(getTxId.txId).toMatch(assetTxId) } + }) + + test('Asset Transfer Tx', async () => { + await appPage.waitForTimeout(1000); + getSignedBlob = await appPage.evaluate(async (optInAddress, getParams, assetIndex, testAccountAddress) => { + let txn = { + "from": testAccountAddress, + "to": optInAddress, + "amount": Math.round(Math.floor(Math.random() * 10)), + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "axfer", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Tx" + }; + + // Get only the promise first + var signPromise = AlgoSigner.sign(txn); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + + }, optInAddress, getParams, assetIndex, testAccountAddress); + + expect(getSignedBlob).toHaveProperty("txID"); + expect(getSignedBlob).toHaveProperty("blob"); + console.log(JSON.stringify(getSignedBlob)) + assetTxId = getSignedBlob.txID + }) + test('Post Asset Opt-in Blob', async () => { + getTxId = await appPage.evaluate( (getSignedBlob) => { + return AlgoSigner.send({ + ledger: 'TestNet', + tx: getSignedBlob.blob + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + }, getSignedBlob); + + if("message" in getTxId) { + console.log(JSON.stringify(getTxId)) + } + else { + expect(getTxId.txId).toMatch(assetTxId) } }) + // tbd Asset Destroy! + }) \ No newline at end of file diff --git a/packages/test-project/tests/manual-e2e-dapp.test.js b/packages/test-project/tests/manual-e2e-dapp.test.js deleted file mode 100644 index 5bbf3c30..00000000 --- a/packages/test-project/tests/manual-e2e-dapp.test.js +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Exercises dApp - originally required manual intervention - * - * @group manual-dapp - */ - -const testNetAccount = "E2E-Tests" // for now, also hardcoding in the regex match for account info, cannot interpolate variables in toMatch -const sendAlgoToAddress = "AEC4WDHXCDF4B5LBNXXRTB3IJTVJSWUZ4VJ4THPU2QGRJGTA3MIDFN3CQA" -const testAccountAddress = "MTHFSNXBMBD4U46Z2HAYAOLGD2EV6GQBPXVTL727RR3G44AJ3WVFMZGSBE" -const unsafePassword = 'c5brJp5f' - -describe('Wallet Setup', () => { - - const extensionName = 'AlgoSigner'; - const extensionPopupHtml = 'index.html'; - const unsafeMenmonic = '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'; - const amount = Math.floor((Math.random() * 10) + .1); // txn size, modify multiplier for bulk - const secondTestNetAccount = "Created-Account"; - let baseUrl // set in beforeAll - let txId // returned tx id from send txn - - jest.setTimeout(10000); - - beforeAll(async () => { - const targets = await browser.targets(); - const extensionTarget = await targets.find(({ _targetInfo }) => { - return _targetInfo.title === extensionName && _targetInfo.type === 'background_page'; - }); - - const extensionUrl = extensionTarget._targetInfo.url || ''; - const extensionID = extensionUrl.split('/')[2]; - - baseUrl = `chrome-extension://${extensionID}/${extensionPopupHtml}`; - await page.goto(baseUrl); - }); - - test('Welcome Page Title', async () => { - await expect(page.title()).resolves.toMatch(extensionName); - }) - - test('Create New Wallet', async () => { - await page.waitForSelector('#setPassword'); - await page.click('#setPassword'); - }) - - test('Set new wallet password', async () => { - await expect(page.$eval('.mt-2', e => e.innerText)).resolves.toMatch('my_1st_game_was_GALAGA!'); - await page.waitForSelector('#createWallet'); - await page.type('#setPassword',unsafePassword); - await page.type('#confirmPassword',unsafePassword); - await page.waitForSelector('#createWallet'); - await page.click('#createWallet'); - }) - - test('Switch Ledger', async () => { - await page.screenshot({path: 'screenshots/test_waiting_for_page.png'}); - await page.waitForSelector('#selectLedger'); - await page.click('#selectLedger'); - await page.waitForSelector('#selectTestNet'); - await page.click('#selectTestNet'); - }) - - test('Import Account', async () => { - await page.waitForSelector('#addAccount'); - await page.click('#addAccount'); - await page.waitForSelector('#importAccount'); - await page.click('#importAccount'); - await page.waitForSelector('#accountName'); - await page.type('#accountName',testNetAccount); - await page.waitForSelector('#enterMnemonic'); - await page.type('#enterMnemonic', unsafeMenmonic); - await page.waitForSelector('#nextStep'); - await page.click('#nextStep'); - await page.waitForSelector('#enterPassword'); - await page.type('#enterPassword',unsafePassword); - await page.waitForSelector('#authButton'); - await page.click('#authButton'); - await page.waitForSelector('#addAccount'); - }) - -}) - -describe('Basic dApp Tests', () => { - - const sampleDapp = 'https://purestake.github.io/algosigner-dapp-example/legacy/index.html' - const samplePage = 'https://purestake.com' - let getParams - let connected - let getStatus - let getSignedBlob - let getTxId - var dappPage - - jest.setTimeout(30000); - - test('Connect Dapp through content.js', async () => { - dappPage = await browser.newPage(); - - await dappPage.goto(sampleDapp); - await dappPage.waitForSelector("#connect"); - - connected = await dappPage.evaluate(() => { - AlgoSigner.connect() - }); - - await dappPage.waitFor(1000); - const pages = await browser.pages(); - var popup = pages[pages.length-1]; - - await popup.waitForSelector("#grantAccess"); - await popup.click('#grantAccess', {waitUntil: 'networkidle2'}); - }) - - test('Get TestNet accounts', async () => { - console.log('Get TestNet accounts starting'); - var getAccounts = await dappPage.evaluate(() => { - console.log('Get TestNet accounts appPage evaluate'); - return AlgoSigner.accounts({ledger: 'TestNet'}) - .then((d) => { - console.log('AlgoSigner Tesnet Account reached.') - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; - return d; - }) - .catch((e) => { - console.log('AlgoSigner failed to get Tesnet Account.') - console.error(e); - return e; - }) - }) - - expect(getAccounts[0].address).toMatch(testAccountAddress); - }) - - test('Get params', async () => { - getParams = await dappPage.evaluate(() => { - return AlgoSigner.algod({ - ledger: 'TestNet', - path: '/v2/transactions/params' - }) - .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; - return d; - }) - .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; - }); - }) - - expect(getParams).toHaveProperty('consensus-version') - expect(getParams).toHaveProperty('fee') - expect(getParams.fee).toEqual(0) - expect(getParams).toHaveProperty('min-fee') - expect(getParams).toHaveProperty('genesis-hash') - expect(getParams).toHaveProperty('genesis-id') - expect(getParams).toHaveProperty('last-round') - - }) - - - - test('Send Tx', async () => { - // Create a function to map to the puppeteer object - async function autosign () { - await dappPage.waitFor(1000); - const pages = await browser.pages(); - var popup = pages[pages.length-1]; - await popup.waitForSelector("#approveTx"); - await popup.click('#approveTx', {waitUntil: 'networkidle2'}); - await popup.waitForSelector("#enterPassword"); - await popup.type('#enterPassword',unsafePassword); - await popup.waitForSelector("#authButton"); - await popup.click('#authButton', {waitUntil: 'networkidle2'}); - } - - // Attach the function to the dapp page for use after popup - await dappPage.exposeFunction("autosign", autosign); - - getSignedBlob = await dappPage.evaluate(async (testAccountAddress, getParams, sendAlgoToAddress) => { - const amount = Math.floor(Math.random() * 10); - let txn = { - "from": testAccountAddress, - "to": sendAlgoToAddress, - "fee": getParams['fee'], - "amount": amount, - "type": "pay", - "firstRound": getParams['last-round'], - "lastRound": getParams['last-round'] + 1000, - "genesisID": getParams['genesis-id'], - "genesisHash": getParams['genesis-hash'], - "note": "test string note" - }; - - // Get only the promise first - var signPromise = AlgoSigner.sign(txn); - - // Initialize the sign process - await window.autosign(); - - // Return the final result of promise - return await signPromise; - }, testAccountAddress, getParams, sendAlgoToAddress); - }) - - test('Post Signed Blob', async () => { - getTxId = await dappPage.evaluate( (getSignedBlob) => { - return AlgoSigner.send({ - ledger: 'TestNet', - tx: getSignedBlob.blob - }) - .then((d) => { - document.getElementById("log").value += JSON.stringify(d) + "\n\n"; - return d; - }) - .catch((e) => { - console.error(e); - document.getElementById("log").value += JSON.stringify(e) + "\n\n"; - }); - }, getSignedBlob); - - console.log(`Confirmation: ${JSON.stringify(getTxId)}`) - }) -}) - - - - From 18e298bb83de9db8c2d5989e8bb1613abaf28e9f Mon Sep 17 00:00:00 2001 From: Brent Date: Tue, 27 Oct 2020 08:45:12 -0400 Subject: [PATCH 13/45] Clawback and asset modify parameter setup --- packages/common/src/interfaces/axfer_clawback.ts | 1 + packages/extension/src/background/messaging/task.ts | 1 - .../src/background/transaction/axferClawbackTransaction.ts | 3 ++- .../src/background/transaction/baseValidatedTxnWrap.ts | 7 +++---- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/common/src/interfaces/axfer_clawback.ts b/packages/common/src/interfaces/axfer_clawback.ts index 4a05c190..b877d214 100644 --- a/packages/common/src/interfaces/axfer_clawback.ts +++ b/packages/common/src/interfaces/axfer_clawback.ts @@ -8,4 +8,5 @@ export interface IAssetClawbackTx extends IBaseTx { assetIndex: number, //uint64 "xaid" The unique ID of the asset to be transferred. amount: number, //uint64 "aamt" The amount of the asset to be transferred. A zero amount transferred to self allocates that asset in the account's Asset map. assetCloseTo?: string, //Address "aclose" Specify this field to remove the asset holding from the sender account and reduce the account's minimum balance. assetRevocationTarget: string, //Address "asnd" The address from which the funds will be withdrawn. + to: string, //Address } \ No newline at end of file diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index b012cad2..9cb1c11d 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -410,7 +410,6 @@ export class Task { let txn = {...message.body.params.transaction}; Object.keys({...message.body.params.transaction}).forEach(key => { - console.log(`Key: ${key}, Value:: ${txn[key]}`); if(txn[key] === undefined || txn[key] === null){ delete txn[key]; } diff --git a/packages/extension/src/background/transaction/axferClawbackTransaction.ts b/packages/extension/src/background/transaction/axferClawbackTransaction.ts index 3d4f28f8..4d0ac3aa 100644 --- a/packages/extension/src/background/transaction/axferClawbackTransaction.ts +++ b/packages/extension/src/background/transaction/axferClawbackTransaction.ts @@ -8,8 +8,9 @@ class AssetClawbackTx implements IAssetClawbackTx { type: string = undefined; assetIndex: number = undefined; amount: number = undefined; - assetCloseTo?: string = undefined; + assetCloseTo?: string = null; from: string = undefined; + to: string = undefined; fee: number = undefined; firstRound: number = undefined; lastRound: number = undefined; diff --git a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts index fd3ac6fa..434f9c52 100644 --- a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts +++ b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts @@ -26,14 +26,13 @@ export class BaseValidatedTxnWrap { // Check required values in the case where one of a set is required. if(requiredParamsSet && requiredParamsSet.length > 0){ var foundValue = false; - for(let key in requiredParamsSet){ + requiredParamsSet.forEach(key => { if(params[key] !== undefined && params[key] !== null){ foundValue = true; - break; } - } + }); if(!foundValue){ - missingFields.push('params'); + missingFields.push(`Transaction requires one parameter from:[${requiredParamsSet}]`); } } From 4b2f5cdbcb058a601fbe93a832718b0778ee5548 Mon Sep 17 00:00:00 2001 From: Tim Baldwin Date: Wed, 28 Oct 2020 15:51:32 -0400 Subject: [PATCH 14/45] Updated dapp tests with Freeze, clawback, destroy. Added id to UI asset list --- .../test-project/tests/basic-e2e-dapp.test.js | 616 +++++++++++++----- .../ui/src/components/Account/AssetsList.ts | 2 +- 2 files changed, 437 insertions(+), 181 deletions(-) diff --git a/packages/test-project/tests/basic-e2e-dapp.test.js b/packages/test-project/tests/basic-e2e-dapp.test.js index 13bf56d8..7eba3264 100644 --- a/packages/test-project/tests/basic-e2e-dapp.test.js +++ b/packages/test-project/tests/basic-e2e-dapp.test.js @@ -4,17 +4,117 @@ * @group basic-dapp */ -const testNetAccount = "E2E-Tests" // for now, also hardcoding in the regex match for account info, cannot interpolate variables in toMatch -const optInAccount = "Opt-In" +// TODO Move pending function to window and re-use +// TODO Move post function to window and re-use + + // Globals +const testNetAccount = "E2E-Tests" // for now, also hardcoding in the regex match for account info, cannot interpolate variables in toMatch +const optInAccount = "Opt-In" // for now, also hardcoding in the regex match for account info, cannot interpolate variables in toMatch const sendAlgoToAddress = "AEC4WDHXCDF4B5LBNXXRTB3IJTVJSWUZ4VJ4THPU2QGRJGTA3MIDFN3CQA" const testAccountAddress = "MTHFSNXBMBD4U46Z2HAYAOLGD2EV6GQBPXVTL727RR3G44AJ3WVFMZGSBE" const optInAddress = "RMY2R35P7PHRBONNKNGDMP75R5DFLRILGESHCNESM22EZ77TQL5GBF75GI" const unsafePassword = 'c5brJp5f' const samplePage = 'https://google.com/' // Prefer about:blank, bug in puppeteer - +// Set in one test and re-used in later tests let getParams // holds the parameters for all txn -let appPage // re-using one window +let appPage // re-using one window to run exercise dApp +let baseUrl // holds the extension url for a local window +let transferAmount // amount of asset transfered from test user to opt-in user + +async function verifyTransaction(txIdVerify, lastRoundVerify, browserPage) { + console.log(`Using Verify Tx Function ${txIdVerify} ${lastRoundVerify}`) + + let pendingTx = await browserPage.evaluate(async (txIdVerify, lastRoundVerify) => { + + let validateCheck = true; + let pendingCheck; + + while(validateCheck) { + + pendingCheck = await AlgoSigner.algod({ + ledger: 'TestNet', + path: '/v2/transactions/pending/' + txIdVerify + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + + if("confirmed-round" in pendingCheck) { + validateCheck = false; + } + else { + lastRoundVerify += 2; + + await AlgoSigner.algod({ + ledger: 'TestNet', + path: '/v2/status/wait-for-block-after/' + lastRoundVerify + }) + .catch((e) => { + return(e) + }); + } + } + + return pendingCheck; + }, txIdVerify, lastRoundVerify); + + console.log(`Exiting Verify Tx Function ${JSON.stringify(pendingTx)}`) + + return pendingTx +} + +async function signTransaction(txnParams, browserPage) { + console.log(`Starting Sign Tx Function: ${txnParams.type}`) + signedTx = await browserPage.evaluate(async (txnParams) => { + + // Get only the promise first + var signPromise = AlgoSigner.sign(txnParams); + + // Initialize the sign process + await window.autosign(); + + // Return the final result of promise + return await signPromise; + + }, txnParams) + .catch((e) => { + return e + }) + + if("message" in signedTx) { + console.log(`Error: ${JSON.stringify(signedTx)}`); + } + else { + console.log(`Exiting Sign Tx Function: ${signedTx.txID}`) + } + + return signedTx +} + +async function postTransaction(localSignedBlob, browserPage){ + console.log(`Entering Post Tx Function: ${localSignedBlob.txID}`) + + getTxId = await browserPage.evaluate( (localSignedBlob) => { + return AlgoSigner.send({ + ledger: 'TestNet', + tx: localSignedBlob.blob + }) + .then((d) => { + return d; + }) + .catch((e) => { + return(e) + }); + }, localSignedBlob); + + console.log(`Exiting Post Tx Function`); + + return getTxId; +} describe('Wallet Setup', () => { @@ -25,9 +125,8 @@ describe('Wallet Setup', () => { const amount = Math.floor((Math.random() * 10)+.1); // txn size, modify multiplier for bulk const secondTestNetAccount = "Created-Account" - let baseUrl // set in beforeAll let extensionPage // set in beforeAll - let txId // returned tx id from send txn + jest.setTimeout(10000); @@ -153,8 +252,6 @@ describe('Basic dApp Setup and GET Tests', () => { await appPage.waitForTimeout(2000); const pages = await browser.pages(); const popup = pages[pages.length-1]; - - await popup.waitForSelector("#grantAccess"); await popup.click("#grantAccess"); @@ -325,8 +422,6 @@ describe('dApp POST Txn Tests (plus Teal compile)', () => { let getSignedBlob let getTxId let matchTxId - let assetTxId - let assetIndex test('POST: Compile Teal', async () => { @@ -477,213 +572,374 @@ describe('dApp Asset Txn Tests', () => { let getTxId let assetTxId let assetIndex + let transferAmount + + test('Asset Create Tx', async () => { + console.log(`Starting Asset Create for ${testAccountAddress} using ${JSON.stringify(getParams)}`); + + let txn = { + "from": testAccountAddress, + "fee": getParams['fee'], + "assetName": 'AutoTest', + "assetUnitName": 'AT', + "assetTotal": 1000 + Math.round(Math.floor(Math.random() * 30)), + "assetDecimals": 1, + "type": "acfg", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "assetManager": testAccountAddress, + "assetReserve": testAccountAddress, + "assetFreeze": testAccountAddress, + "assetClawback": testAccountAddress, + "note": "Asset create" + } + + localSignedBlob = await signTransaction(txn, appPage); + + if("message" in localSignedBlob) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`) + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID + getSignedBlob = localSignedBlob + console.log(`Exiting Asset Create ${localSignedBlob.txID}`) + } + + }) - test('Asset Create Tx', async () => { - await appPage.waitForTimeout(1000); - getSignedBlob = await appPage.evaluate(async (testAccountAddress, getParams, sendAlgoToAddress) => { - let txn = { - "from": testAccountAddress, - "fee": getParams['fee'], - "assetName": 'AutoTest', - "assetUnitName": 'AT', - "assetTotal": 1000 + Math.round(Math.floor(Math.random() * 10)), - "assetDecimals": 2, - "type": "acfg", - "firstRound": getParams['last-round'], - "lastRound": getParams['last-round'] + 1000, - "genesisID": getParams['genesis-id'], - "genesisHash": getParams['genesis-hash'], - "note": "Asset create" - }; + test('Post Asset Create Blob', async () => { + console.log(`Posting Asset Create ${getSignedBlob.txID}`); - // Get only the promise first - var signPromise = AlgoSigner.sign(txn); + getTxId = await postTransaction(getSignedBlob, appPage); + if("message" in getTxId) { + console.log(`Error: ${JSON.stringify(getTxId)}`); + } + else { + expect(getTxId.txId).toMatch(getSignedBlob.txID); + console.log(`Exiting Asset Create ${getTxId.txId}`); + } + }) - // Initialize the sign process - await window.autosign(); - - // Return the final result of promise - return await signPromise; - - }, testAccountAddress, getParams, sendAlgoToAddress); + test('Verify Asset Pending Tx', async () => { + console.log(`Verifying Asset Create`) - console.log(`Blob: ${getSignedBlob.blob}`); - expect(getSignedBlob).toHaveProperty("txID"); - expect(getSignedBlob).toHaveProperty("blob"); - assetTxId = getSignedBlob.txID + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) + + assetIndex = pendingTx["asset-index"] + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + + console.log(`Exiting Verify Asset Create ${pendingTx["asset-index"]}`) }) - test('Post Asset Create Blob', async () => { - getTxId = await appPage.evaluate( (getSignedBlob) => { - return AlgoSigner.send({ - ledger: 'TestNet', - tx: getSignedBlob.blob - }) - .then((d) => { - return d; - }) - .catch((e) => { - return(e) - }); - }, getSignedBlob); + test('Asset Opt-in Tx', async () => { + console.log(`Entering Asset Create/Sign Opt-in Tx: ${assetIndex}`) + + let txn = { + "from": optInAddress, + "to": optInAddress, + "amount": 0, + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "axfer", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Opt-In" + }; + + localSignedBlob = await signTransaction(txn, appPage); - expect(getTxId.txId).toMatch(assetTxId) + if("message" in localSignedBlob) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`) + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID // Update the asset Tx that gets passed around + getSignedBlob = localSignedBlob; // Update the blob that gets passed around + console.log(`Exiting Asset Create/Sign Opt-in Tx: ${getSignedBlob.txID}`) + } }) - test('Verify Asset Pending Tx', async () => { + + test('Post Asset Opt-in Blob', async () => { + console.log(`Entering Post Asset Opt-in Tx`) + + getTxId = await postTransaction(getSignedBlob, appPage); - let lastRound = getParams['last-round'] - let pendingTx = await appPage.evaluate(async (assetTxId, lastRound) => { + if("message" in getTxId) { + console.log(`Error: ${JSON.stringify(getTxId)}`); + } + else { + expect(getTxId.txId).toMatch(assetTxId) - let validateCheck = true; - let pendingCheck; + console.log(`Exiting Post Asset Opt-in ${JSON.stringify(getTxId)}`) + } + }) - while(validateCheck) { - - pendingCheck = await AlgoSigner.algod({ - ledger: 'TestNet', - path: '/v2/transactions/pending/' + assetTxId - }) - .then((d) => { - return d; - }) - .catch((e) => { - return(e) - }); - - if("asset-index" in pendingCheck) { - validateCheck = false; - } - else { - lastRound++; - - await AlgoSigner.algod({ - ledger: 'TestNet', - path: '/v2/status/wait-for-block-after/' + lastRound - }) - .catch((e) => { - return(e) - }); - } - } + test('Verify Asset Opt-in Pending Tx', async () => { + console.log(`Verifying Asset Opt-in ${assetTxId}`) - return pendingCheck; - }, assetTxId, lastRound); + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) + + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(optInAddress) + + console.log(`Exiting Verify Asset Opt-in ${pendingTx["confirmed-round"]}`) + }) + + test('Asset Transfer Tx', async () => { + console.log(`Entering Asset Transfer Create/Sign Tx ${assetIndex}`) + + let txn = { + "from": testAccountAddress, + "to": optInAddress, + "amount": Math.round(Math.floor(Math.random() * 10)), + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "axfer", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Tx" + }; + + localSignedBlob = await signTransaction(txn, appPage); + + if("message" in getTxId) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`); + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID + getSignedBlob = localSignedBlob // Subtle easy to miss + transferAmount = txn.amount; + console.log(`Exiting Asset Transfer Create/Sign Tx ${localSignedBlob.txID} for ${assetIndex}`) + } + }) + + test('Post Asset Transfer Blob', async () => { + console.log(`Entering Post Asset Transfer Tx for ${getSignedBlob.txID} for ${assetIndex}`) + + getTxId = await postTransaction(getSignedBlob, appPage); + + if("message" in getTxId) { + console.log(`Error: ${JSON.stringify(getTxId)}`); + } + else { + expect(getTxId.txId).toMatch(assetTxId) + console.log(`Exiting Post Asset Transfer ${JSON.stringify(getTxId)}`) + } + + }) + + test('Verify Asset Transfer Tx', async () => { + console.log(`Starting Verify Asset Tx: ${assetTxId} for ${assetIndex}`) + + let lastRound = getParams['last-round'] + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) - assetIndex = pendingTx["asset-index"] expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + expect(pendingTx["txn"]["txn"]["arcv"]).toMatch(optInAddress) + expect( pendingTx["txn"]["txn"]["aamt"]).toEqual(transferAmount) + + console.log(`Finished verify asset tx: ${assetTxId} of Asset Index ${assetIndex} for ${pendingTx["txn"]["txn"]["aamt"]}`) }) - test('Asset OptIn Tx', async () => { - await appPage.waitForTimeout(1000); - getSignedBlob = await appPage.evaluate(async (optInAddress, getParams, assetIndex) => { - let txn = { - "from": optInAddress, - "to": optInAddress, - "amount": 0, - "fee": getParams['fee'], - "assetIndex": assetIndex, - "type": "axfer", - "firstRound": getParams['last-round'], - "lastRound": getParams['last-round'] + 1000, - "genesisID": getParams['genesis-id'], - "genesisHash": getParams['genesis-hash'], - "note": "Asset Optin" - }; + test('Asset Freeze Tx', async () => { + console.log(`Entering Asset Freeze Tx ${assetIndex}`) + + let txn = { + "from": testAccountAddress, + "freezeAccount": optInAddress, + "freezeState": true, + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "afrz", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Freeze Tx" + }; + + localSignedBlob = await signTransaction(txn, appPage) + + if("message" in localSignedBlob) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`); + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID + getSignedBlob = localSignedBlob + console.log(`Exiting Asset Freeze Create/Sign Tx ${getSignedBlob.txID} for ${assetIndex}`) + } + }) - // Get only the promise first - var signPromise = AlgoSigner.sign(txn); + test('Post Asset Freeze Blob', async () => { + console.log(`Entering Post Asset Freeze Tx for ${getSignedBlob.txID} for ${assetIndex}`) - // Initialize the sign process - await window.autosign(); - - // Return the final result of promise - return await signPromise; - - }, optInAddress, getParams, assetIndex); + getTxId = await postTransaction(getSignedBlob, appPage) + + if("message" in getTxId) { + console.log('Error - see message') + } + else { + expect(getTxId.txId).toMatch(assetTxId) + } - expect(getSignedBlob).toHaveProperty("txID"); - expect(getSignedBlob).toHaveProperty("blob"); - console.log(JSON.stringify(getSignedBlob)) - assetTxId = getSignedBlob.txID + console.log(`Exiting Post Asset Freeze ${JSON.stringify(getTxId)}`) }) - test('Post Asset Opt-in Blob', async () => { - getTxId = await appPage.evaluate( (getSignedBlob) => { - return AlgoSigner.send({ - ledger: 'TestNet', - tx: getSignedBlob.blob - }) - .then((d) => { - return d; - }) - .catch((e) => { - return(e) - }); - }, getSignedBlob); + test('Verify Asset Freeze Tx', async () => { + console.log(`Starting Verify Asset Freeze Tx: ${assetTxId} for ${assetIndex}`) + + let lastRound = getParams['last-round'] + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) + + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + + console.log(`Finished verify asset freeze tx: ${assetTxId} of Asset Index ${assetIndex}`) + console.log(`${pendingTx}`) + }) + + test('Asset Clawback Tx', async () => { + console.log(`Entering Asset Clawback Tx ${assetIndex} for ${transferAmount}`) + + let txn = { + "from": testAccountAddress, + "to": testAccountAddress, + "assetRevocationTarget": optInAddress, + "amount": transferAmount, + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "axfer", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Clawback Tx" + }; + + localSignedBlob = await signTransaction(txn, appPage) + + if("message" in localSignedBlob) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`); + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID + getSignedBlob = localSignedBlob + console.log(`Exiting Asset Clawback Create/Sign Tx ${getSignedBlob.txID} for ${assetIndex} for ${transferAmount}`) + } + }) + + test('Post Asset Clawback Blob', async () => { + console.log(`Entering Post Asset Clawback Tx for ${getSignedBlob.txID} for ${assetIndex}`) + + getTxId = await postTransaction(getSignedBlob, appPage) if("message" in getTxId) { - console.log('Likely OVERSPEND - ADD FUNDS TO optInAddress') - console.log(JSON.stringify(getTxId)) + console.log('Error - see message') } else { - expect(getTxId.txId).toMatch(assetTxId) } + expect(getTxId.txId).toMatch(assetTxId) + } + + console.log(`Exiting Post Asset Clawback ${JSON.stringify(getTxId)}`) }) - test('Asset Transfer Tx', async () => { - await appPage.waitForTimeout(1000); - getSignedBlob = await appPage.evaluate(async (optInAddress, getParams, assetIndex, testAccountAddress) => { - let txn = { - "from": testAccountAddress, - "to": optInAddress, - "amount": Math.round(Math.floor(Math.random() * 10)), - "fee": getParams['fee'], - "assetIndex": assetIndex, - "type": "axfer", - "firstRound": getParams['last-round'], - "lastRound": getParams['last-round'] + 1000, - "genesisID": getParams['genesis-id'], - "genesisHash": getParams['genesis-hash'], - "note": "Asset Tx" - }; + test('Verify Asset Clawback Tx', async () => { + console.log(`Starting Verify Asset Clawback Tx: ${assetTxId} for ${assetIndex}`) - // Get only the promise first - var signPromise = AlgoSigner.sign(txn); + let lastRound = getParams['last-round'] + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) + + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + transferAmount = pendingTx["txn"]["txn"]["aamt"] // set - // Initialize the sign process - await window.autosign(); - - // Return the final result of promise - return await signPromise; - - }, optInAddress, getParams, assetIndex, testAccountAddress); + console.log(`Finished verify asset clawback tx: ${assetTxId} of Asset Index ${assetIndex}`) + }) - expect(getSignedBlob).toHaveProperty("txID"); - expect(getSignedBlob).toHaveProperty("blob"); - console.log(JSON.stringify(getSignedBlob)) - assetTxId = getSignedBlob.txID + test('Asset Destroy Tx', async () => { + console.log(`Entering Asset Destroy Tx ${assetIndex} for ${transferAmount}`) + + let txn = { + "from": testAccountAddress, + "fee": getParams['fee'], + "assetIndex": assetIndex, + "type": "acfg", + "firstRound": getParams['last-round'], + "lastRound": getParams['last-round'] + 1000, + "genesisID": getParams['genesis-id'], + "genesisHash": getParams['genesis-hash'], + "note": "Asset Destroy Tx" + }; + + localSignedBlob = await signTransaction(txn, appPage) + + if("message" in localSignedBlob) { + console.log(`Error: ${JSON.stringify(localSignedBlob)}`); + } + else { + expect(localSignedBlob).toHaveProperty("txID"); + expect(localSignedBlob).toHaveProperty("blob"); + assetTxId = localSignedBlob.txID + getSignedBlob = localSignedBlob + console.log(`Exiting Asset Destroy Create/Sign Tx ${getSignedBlob.txID} for ${assetIndex}`) + } }) - test('Post Asset Opt-in Blob', async () => { - getTxId = await appPage.evaluate( (getSignedBlob) => { - return AlgoSigner.send({ - ledger: 'TestNet', - tx: getSignedBlob.blob - }) - .then((d) => { - return d; - }) - .catch((e) => { - return(e) - }); - }, getSignedBlob); + test('Post Asset Destroy Blob', async () => { + console.log(`Entering Post Asset Destroy Tx for ${getSignedBlob.txID} for ${assetIndex}`) + + getTxId = await postTransaction(getSignedBlob, appPage) if("message" in getTxId) { - console.log(JSON.stringify(getTxId)) + console.log('Error - see message') } else { - expect(getTxId.txId).toMatch(assetTxId) } + expect(getTxId.txId).toMatch(assetTxId) + } + + console.log(`Exiting Post Asset Destroy ${JSON.stringify(getTxId)}`) + }) + + test('Verify Asset Destroy Tx', async () => { + console.log(`Starting Verify Asset Destroy Tx: ${assetTxId} for ${assetIndex}`) + + let lastRound = getParams['last-round'] + let pendingTx = await verifyTransaction(assetTxId, getParams['last-round'] , appPage) + + expect(pendingTx["txn"]["txn"]["snd"]).toMatch(testAccountAddress) + + console.log(`Finished verify asset Destroy tx: ${assetTxId} of Asset Index ${assetIndex}`) }) - // tbd Asset Destroy! + // // The asset just doesn't appear fast enough for this yet + // // test('Wallet Verify Asset Transfer', async () => { + // // console.log(`Back in the browser to verify Asset transfer for ${assetIndex}`) + + // // await appPage.goto(baseUrl); + // // await appPage.waitForTimeout(5000) // wait on tx to process + // // await appPage.click('#selectLedger') + // // await appPage.waitFor(5000) + // // await appPage.click('#selectTestNet') + // // await appPage.waitFor(5000) + // // await appPage.waitForSelector(`#account_${optInAccount}`) + // // await appPage.click(`#account_${optInAccount}`) + // // await appPage.waitForTimeout(30000) + // // await expect(appPage.$eval('#accountName', e => e.innerText)).resolves.toMatch(/Opt-In/) + // // await expect(appPage.$eval(`#asset_${assetIndex}`, e => e.innerText)).resolves.toMatch(assetIndex) + + // // console.log(`Exiting Wallet Verify`) + // // }) }) \ No newline at end of file diff --git a/packages/ui/src/components/Account/AssetsList.ts b/packages/ui/src/components/Account/AssetsList.ts index 626a0936..51c07b0d 100644 --- a/packages/ui/src/components/Account/AssetsList.ts +++ b/packages/ui/src/components/Account/AssetsList.ts @@ -19,7 +19,7 @@ const AssetPreview: FunctionalComponent = (props: any) => { onClick=${() => setShowAsset(asset)}> ${ asset.name && asset.name.length > 0 && html` ${ asset.name } - ${asset['asset-id']} + ${asset['asset-id']} `} ${ (!asset.name || asset.name.length === 0) && html` ${ asset['asset-id'] } From ba177dbb2ca0d4a2149647c1a1027f5cd598a0a1 Mon Sep 17 00:00:00 2001 From: Brent Date: Wed, 28 Oct 2020 17:14:31 -0400 Subject: [PATCH 15/45] appl support, rekeyto alignment but still disabled --- packages/common/src/interfaces/appl.ts | 46 +++++-- packages/common/src/interfaces/baseTx.ts | 2 +- packages/common/src/interfaces/pay.ts | 1 - .../src/background/messaging/task.ts | 8 +- .../transaction/acfgCreateTransaction.ts | 2 +- .../transaction/acfgDestroyTransaction.ts | 2 +- .../background/transaction/acfgTransaction.ts | 2 +- .../background/transaction/afrzTransaction.ts | 2 +- .../background/transaction/applTransaction.ts | 35 +++-- .../transaction/axferAcceptTransaction.ts | 2 +- .../transaction/axferClawbackTransaction.ts | 2 +- .../transaction/axferTransaction.ts | 2 +- .../transaction/keyregTransaction.ts | 2 +- .../background/transaction/payTransaction.ts | 2 +- .../src/background/utils/validator.ts | 2 +- .../src/components/SignTransaction/TxAppl.ts | 127 ++++++++++++++++++ packages/ui/src/pages/SignTransaction.ts | 26 ++-- 17 files changed, 216 insertions(+), 49 deletions(-) create mode 100644 packages/ui/src/components/SignTransaction/TxAppl.ts diff --git a/packages/common/src/interfaces/appl.ts b/packages/common/src/interfaces/appl.ts index a934096d..f6b4d05a 100644 --- a/packages/common/src/interfaces/appl.ts +++ b/packages/common/src/interfaces/appl.ts @@ -1,16 +1,40 @@ +import { IBaseTx } from "./baseTx"; /// // Mapping interface of allowable fields for appl transactions. /// -export interface IApplTx { +export interface IApplTx extends IBaseTx { + // NOTE: Most fields are remapped in the algosdk - The comments indicate where they will be mapped too. type: string, //"appl" - apid: number, //uint64 "apid" ID of the application being configured or empty if creating. - apan: number, //uint64 "apan" Defines what additional actions occur with the transaction. See the OnComplete section of the TEAL spec for details. - apat?: string, //Address "apat" List of accounts in addition to the sender that may be accessed from the application's approval-program and clear-state-program. - apap?: string, //Address "apap" Logic executed for every application transaction, except when on-completion is set to "clear". It can read and write global state for the application, as well as account-specific local state. Approval programs may reject the transaction. - apaa?: any, //byte[] "apaa" Transaction specific arguments accessed from the application's approval-program and clear-state-program. - apsu?: string, //Address "apsu" Logic executed for application transactions with on-completion set to "clear". It can read and write global state for the application, as well as account-specific local state. Clear state programs cannot reject the transaction. - apfa?: string, //Address "apfa" Lists the applications in addition to the application-id whose global states may be accessed by this application's approval-program and clear-state-program. The access is read-only. - apas?: string, //Address "apas" Lists the assets whose AssetParams may be accessed by this application's approval-program and clear-state-program. The access is read-only. - apgs?: any, //StateSchema "apgs" Holds the maximum number of global state values defined within a StateSchema object. - apls?: any, //StateSchema "apls" Holds the maximum number of local state values defined within a StateSchema object. + + //apid: number, //uint64 "apid" ID of the application being configured or empty if creating. + appIndex?: any, + + //apan: number, //uint64 "apan" Defines what additional actions occur with the transaction. See the OnComplete section of the TEAL spec for details. + appOnComplete?: any, + + //apat?: string, //Address "apat" List of accounts in addition to the sender that may be accessed from the application's approval-program and clear-state-program. + appAccounts?: any, //Expects Array[Addresses] + + //apap?: string, //Address "apap" Logic executed for every application transaction, except when on-completion is set to "clear". It can read and write global state for the application, as well as account-specific local state. Approval programs may reject the transaction. + appApprovalProgram?: any, + + //apaa?: any, //byte[] "apaa" Transaction specific arguments accessed from the application's approval-program and clear-state-program. + appArgs?: any, //Array: Performs a Buffer.from() for each element + + //apsu?: string, //Address "apsu" Logic executed for application transactions with on-completion set to "clear". It can read and write global state for the application, as well as account-specific local state. Clear state programs cannot reject the transaction. + appClearProgram?: any, + + //apfa?: string, //Address "apfa" Lists the applications in addition to the application-id whose global states may be accessed by this application's approval-program and clear-state-program. The access is read-only. + appForeignApps?: any, + + //apas?: string, //Address "apas" Lists the assets whose AssetParams may be accessed by this application's approval-program and clear-state-program. The access is read-only. + appForeignAssets?: any + + //apgs?: any, //StateSchema "apgs" Holds the maximum number of global state values defined within a StateSchema object. + appGlobalInts?: any, + appGlobalByteSlices?: any, + + //apls?: any, //StateSchema "apls" Holds the maximum number of local state values defined within a StateSchema object. + appLocalInts?: any, + appLocalByteSlices?: any, } \ No newline at end of file diff --git a/packages/common/src/interfaces/baseTx.ts b/packages/common/src/interfaces/baseTx.ts index 80434041..be17b524 100644 --- a/packages/common/src/interfaces/baseTx.ts +++ b/packages/common/src/interfaces/baseTx.ts @@ -12,6 +12,6 @@ export interface IBaseTx { group?: any, //[32]byte "grp" The group specifies that the transaction is part of a group and, if so, specifies the hash of the transaction group. Assign a group ID to a transaction through the workflow described in the Atomic Transfers Guide. lease?: any, //[32]byte "lx" A lease enforces mutual exclusion of transactions. If this field is nonzero, then once the transaction is confirmed, it acquires the lease identified by the (Sender, Lease) pair of the transaction until the LastValid round passes. While this transaction possesses the lease, no other transaction specifying this lease can be confirmed. A lease is often used in the context of Algorand Smart Contracts to prevent replay attacks. (Buffer is created from the provided value) //txType //string "type" Specifies the type of transaction. This value is automatically generated using any of the developer tools. - rekey?: any, + reKeyTo?: any, //string "rekey" Remapped to reKeyTo for algosdk alignment } diff --git a/packages/common/src/interfaces/pay.ts b/packages/common/src/interfaces/pay.ts index b2b3080d..19798b0f 100644 --- a/packages/common/src/interfaces/pay.ts +++ b/packages/common/src/interfaces/pay.ts @@ -8,5 +8,4 @@ export interface IPaymentTx extends IBaseTx { to: string, //Address "rcv" The address of the account that receives the amount. amount: number, //uint64 "amt" The total amount to be sent in microAlgos. closeRemainderTo?: string, //Address "close" When set, it indicates that the transaction is requesting that the Sender account should be closed, and all remaining funds, after the fee and amount are paid, be transferred to this address. - rekey?: any, } diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 9cb1c11d..8d905cbd 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -145,12 +145,12 @@ export class Task { 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. // We can use a modified popup that allows users to review the transaction and invalid fields and close the transaction. - let invalidKeys = []; - for (const [key, value] of Object.entries(transactionWrap.validityObject)) { - if(value===ValidationStatus.Invalid){ + var invalidKeys = []; + Object.entries(transactionWrap.validityObject).forEach(([key,value]) => { + if(value['status']===ValidationStatus.Invalid){ invalidKeys.push(`${key}`); } - } + }) d.error = { message: `Validation failed for transaction because of invalid properties [${invalidKeys.join(',')}].` }; diff --git a/packages/extension/src/background/transaction/acfgCreateTransaction.ts b/packages/extension/src/background/transaction/acfgCreateTransaction.ts index 47b5b958..de1e55fe 100644 --- a/packages/extension/src/background/transaction/acfgCreateTransaction.ts +++ b/packages/extension/src/background/transaction/acfgCreateTransaction.ts @@ -26,7 +26,7 @@ class AssetCreateTx implements IAssetCreateTx { assetClawback?: string = null; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; }; /// diff --git a/packages/extension/src/background/transaction/acfgDestroyTransaction.ts b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts index 23a69ee5..b1863928 100644 --- a/packages/extension/src/background/transaction/acfgDestroyTransaction.ts +++ b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts @@ -16,7 +16,7 @@ class AssetDestroyTx implements IAssetDestroyTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; }; /// diff --git a/packages/extension/src/background/transaction/acfgTransaction.ts b/packages/extension/src/background/transaction/acfgTransaction.ts index 61429ede..136e3698 100644 --- a/packages/extension/src/background/transaction/acfgTransaction.ts +++ b/packages/extension/src/background/transaction/acfgTransaction.ts @@ -17,7 +17,7 @@ class AssetConfigTx implements IAssetConfigTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; // Modifications must include one of these assetTotal?: number = null; diff --git a/packages/extension/src/background/transaction/afrzTransaction.ts b/packages/extension/src/background/transaction/afrzTransaction.ts index 42f9ea7b..d1ca6c6c 100644 --- a/packages/extension/src/background/transaction/afrzTransaction.ts +++ b/packages/extension/src/background/transaction/afrzTransaction.ts @@ -18,7 +18,7 @@ class AssetFreezeTx implements IAssetFreezeTx{ genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; }; /// diff --git a/packages/extension/src/background/transaction/applTransaction.ts b/packages/extension/src/background/transaction/applTransaction.ts index cbf453b9..d6e49b4b 100644 --- a/packages/extension/src/background/transaction/applTransaction.ts +++ b/packages/extension/src/background/transaction/applTransaction.ts @@ -5,17 +5,30 @@ import { BaseValidatedTxnWrap } from "./baseValidatedTxnWrap"; // Mapping, validation and error checking for appl transactions prior to sign. /// export class ApplTx implements IApplTx { - type: string = undefined;; - apid: number = undefined;; - apan: number = undefined;; - apat?: string = null; - apap?: string = null; - apaa?: any = null; - apsu?: string = null; - apfa?: string = null; - apas?: string = null; - apgs?: any = null; - apls?: any = null; + type: string = undefined; + from: string = undefined; + fee: number = undefined; + firstRound: number = undefined; + lastRound: number = undefined; + note?: string = null; + genesisID: string = undefined; + genesisHash: any = undefined; + group?: any = null; + lease?: any = null; + reKeyTo?: any = null; + + appIndex?: any = null; + appOnComplete?: any = null; + appAccounts?: any = null; + appApprovalProgram?: any = null; + appArgs?: any = null; + appClearProgram?: any = null; + appForeignApps?: any = null; + appForeignAssets?: any = null; + appGlobalInts?: any = null; + appGlobalByteSlices?: any = null; + appLocalInts?: any = null; + appLocalByteSlices?: any = null; } /// diff --git a/packages/extension/src/background/transaction/axferAcceptTransaction.ts b/packages/extension/src/background/transaction/axferAcceptTransaction.ts index 2ae893c6..392f79ed 100644 --- a/packages/extension/src/background/transaction/axferAcceptTransaction.ts +++ b/packages/extension/src/background/transaction/axferAcceptTransaction.ts @@ -18,7 +18,7 @@ class AssetAcceptTx implements IAssetAcceptTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; amount?: number = null; } diff --git a/packages/extension/src/background/transaction/axferClawbackTransaction.ts b/packages/extension/src/background/transaction/axferClawbackTransaction.ts index 4d0ac3aa..498c9974 100644 --- a/packages/extension/src/background/transaction/axferClawbackTransaction.ts +++ b/packages/extension/src/background/transaction/axferClawbackTransaction.ts @@ -19,7 +19,7 @@ class AssetClawbackTx implements IAssetClawbackTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; assetRevocationTarget: string = undefined; } diff --git a/packages/extension/src/background/transaction/axferTransaction.ts b/packages/extension/src/background/transaction/axferTransaction.ts index e75cc223..6857cb7b 100644 --- a/packages/extension/src/background/transaction/axferTransaction.ts +++ b/packages/extension/src/background/transaction/axferTransaction.ts @@ -19,7 +19,7 @@ class AssetTransferTx implements IAssetTransferTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; } /// diff --git a/packages/extension/src/background/transaction/keyregTransaction.ts b/packages/extension/src/background/transaction/keyregTransaction.ts index 2f665c9b..ec8eabfe 100644 --- a/packages/extension/src/background/transaction/keyregTransaction.ts +++ b/packages/extension/src/background/transaction/keyregTransaction.ts @@ -20,7 +20,7 @@ class KeyRegistrationTx implements IKeyRegistrationTx { genesisHash: any = undefined; group?: any = null; lease?: any = null; - rekey?: any = null; + reKeyTo?: any = null; } /// diff --git a/packages/extension/src/background/transaction/payTransaction.ts b/packages/extension/src/background/transaction/payTransaction.ts index d97ca2a5..3e0ca77d 100644 --- a/packages/extension/src/background/transaction/payTransaction.ts +++ b/packages/extension/src/background/transaction/payTransaction.ts @@ -9,7 +9,7 @@ class PaymentTx implements IPaymentTx{ to: string = undefined; amount: number = undefined; closeRemainderTo?: string = null; - rekey?: any = null; + reKeyTo?: any = null; from: string = undefined; fee: number = undefined; firstRound: number = undefined; diff --git a/packages/extension/src/background/utils/validator.ts b/packages/extension/src/background/utils/validator.ts index 9e1a7d33..4238a4a8 100644 --- a/packages/extension/src/background/utils/validator.ts +++ b/packages/extension/src/background/utils/validator.ts @@ -111,7 +111,7 @@ export function Validate(field: any, value: any): ValidationResponse { return new ValidationResponse({status:ValidationStatus.Valid}); } - case "rekey": + case "reKeyTo": if(value) { return new ValidationResponse({status:ValidationStatus.Invalid, info:'Rekey transactions are not currently accepted in AlgoSigner.'}); } diff --git a/packages/ui/src/components/SignTransaction/TxAppl.ts b/packages/ui/src/components/SignTransaction/TxAppl.ts new file mode 100644 index 00000000..15495f3d --- /dev/null +++ b/packages/ui/src/components/SignTransaction/TxAppl.ts @@ -0,0 +1,127 @@ +import { html } from 'htm/preact'; +import { FunctionalComponent } from "preact"; +import { useState } from 'preact/hooks'; + +const TxPay: FunctionalComponent = (props: any) => { + const [tab, setTab] = useState('overview'); + const { tx, account, vo } = props; + + const txText = JSON.stringify(tx, null, 2); + + return html` +
+
+
+ ${account} +
+
+ YOU +
+
+
+

${'Application'}

+ +
+ +
+ + ${ tab==="overview" && html` +
+ ${tx.appIndex && html` +
+

Application Index:

+

${tx.appIndex}

+
+ `} + ${tx.appOnComplete && html` +
+

On Complete:

+

${tx.appOnComplete}

+
+ `} + ${tx.appAccounts && html` +
+

Accounts:

+

${tx.appAccounts}

+
+ `} + ${tx.appApprovalProgram && html` +
+

Approval Program:

+ +

${tx.appApprovalProgram}

+
+ `} + ${tx.appArgs && html` +
+

Args:

+

${tx.appArgs}

+
+ `} + ${tx.appClearProgram && html` +
+

Clear Program:

+

${tx.appClearProgram}

+
+ `} + ${tx.appForeignApps && html` +
+

Foreign Apps:

+

${tx.appForeignApps}

+
+ `} + ${tx.appForeignAssets && html` +
+

Foreign Assets:

+

${tx.appForeignAssets}

+
+ `} + ${tx.appGlobalInts && html` +
+

Global Ints:

+

${tx.appGlobalInts}

+
+ `} + ${tx.appGlobalByteSlices && html` +
+

Global Bytes:

+

${tx.appGlobalByteSlices}

+
+ `} + ${tx.appLocalInts && html` +
+

Local Ints:

+

${tx.appLocalInts}

+
+ `} + ${tx.appLocalByteSlices && html` +
+

Local Bytes:

+

${tx.appLocalByteSlices}

+
+ `} + +

Fee: ${tx.fee/1e6} Algos

+
+ `} + ${ tab==="details" && html` +
+
+          ${txText}
+        
+
+ `} + + `; +} + +export default TxPay \ No newline at end of file diff --git a/packages/ui/src/pages/SignTransaction.ts b/packages/ui/src/pages/SignTransaction.ts index c7ae27a5..d20a9a13 100644 --- a/packages/ui/src/pages/SignTransaction.ts +++ b/packages/ui/src/pages/SignTransaction.ts @@ -5,16 +5,17 @@ import { useState, useEffect, useContext } from 'preact/hooks'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { isFromExtension } from '@algosigner/common/utils'; -import TxAcfg from 'components/SignTransaction/TxAcfg' -import TxPay from 'components/SignTransaction/TxPay' -import TxAlert from 'components/SignTransaction/TxAlert' -import TxKeyreg from 'components/SignTransaction/TxKeyreg' -import TxAxfer from 'components/SignTransaction/TxAxfer' -import TxAfrz from 'components/SignTransaction/TxAfrz' -import Authenticate from 'components/Authenticate' -import { sendMessage } from 'services/Messaging' -import { StoreContext } from 'services/StoreContext' -import logotype from 'assets/logotype.png' +import TxAcfg from 'components/SignTransaction/TxAcfg'; +import TxPay from 'components/SignTransaction/TxPay'; +import TxAlert from 'components/SignTransaction/TxAlert'; +import TxKeyreg from 'components/SignTransaction/TxKeyreg'; +import TxAxfer from 'components/SignTransaction/TxAxfer'; +import TxAfrz from 'components/SignTransaction/TxAfrz'; +import TxAppl from 'components/SignTransaction/TxAppl'; +import Authenticate from 'components/Authenticate'; +import { sendMessage } from 'services/Messaging'; +import { StoreContext } from 'services/StoreContext'; +import logotype from 'assets/logotype.png'; function deny() { const params = { @@ -114,7 +115,7 @@ const SignTransaction: FunctionalComponent = (props) => { `}

- ${request.originTitle} wants to sign a transaction + ${request.originTitle} wants to sign a transaction for ${ledger.toLowerCase() == 'mainnet' ? html`${ledger}` : html`${ledger}`}

@@ -137,6 +138,9 @@ const SignTransaction: FunctionalComponent = (props) => { ${ request.body.params.transaction.type==="afrz" && html` <${TxAfrz} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} account=${account} ledger=${ledger} /> `} + ${ request.body.params.transaction.type==="appl" && html` + <${TxAppl} tx=${request.body.params.transaction} vo=${request.body.params.validityObject} account=${account} ledger=${ledger} /> + `} `}
From 1a4b1385336d78a6c0a932c170e8ca11a4535193 Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Fri, 30 Oct 2020 01:59:58 +0100 Subject: [PATCH 16/45] Added Cache object to save temp info in local.storage --- .../background/messaging/internalMethods.ts | 71 +++++++++++-------- .../src/background/messaging/types.ts | 5 ++ .../background/utils/assetsDetailsHelper.ts | 16 ++--- .../extension/src/background/utils/helper.ts | 52 ++++++++------ 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index dba238f7..fbe78aba 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -2,11 +2,12 @@ import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { logging } from '@algosigner/common/logging'; import { ExtensionStorage } from "@algosigner/storage/src/extensionStorage"; import { Task } from './task'; -import { Ledger, Backend, API } from './types'; +import { API, Backend, Cache, Ledger } from './types'; import { Settings } from '../config'; import encryptionWrap from "../encryptionWrap"; import Session from '../utils/session'; import AssetsDetailsHelper from '../utils/assetsDetailsHelper'; +import { initializeCache } from '../utils/helper'; import { ValidationResponse } from '../utils/validator'; import { getValidatedTxnWrap } from "../transaction/actions"; const algosdk = require("algosdk"); @@ -251,35 +252,46 @@ export class InternalMethods { const { ledger, address } = request.body.params; const algod = this.getAlgod(ledger); algod.accountInformation(address).do().then((res: any) => { - // Check for asset details saved in storage if needed - if ('assets' in res && res.assets.length > 0){ - new ExtensionStorage().getStorage('assets', (savedAssets: any) => { + let extensionStorage = new ExtensionStorage(); + extensionStorage.getStorage('cache', (storedCache: any) => { + let cache: Cache = initializeCache(storedCache, ledger); + + // Check for asset details saved in storage, if needed + if ('assets' in res && res.assets.length > 0){ let missingAssets = []; - if (savedAssets) { - for (var i = res.assets.length - 1; i >= 0; i--) { - const assetId = res.assets[i]['asset-id']; - if (assetId in savedAssets[ledger]) { - res.assets[i] = { - ...res.assets[i], - ...savedAssets[ledger][assetId] - }; - } else { - missingAssets.push(assetId); - } + for (var i = res.assets.length - 1; i >= 0; i--) { + const assetId = res.assets[i]['asset-id']; + if (assetId in cache.assets[ledger]) { + res.assets[i] = { + ...res.assets[i], + ...cache.assets[ledger][assetId] + }; + } else { + missingAssets.push(assetId); } - } else { - missingAssets = res.assets.map(x => x['asset-id']); } if (missingAssets.length > 0) AssetsDetailsHelper.add(missingAssets, ledger); res.assets.sort((a, b) => a['asset-id'] - b['asset-id']); - sendResponse(res); - }); - } else { + } + sendResponse(res); - } + + // Save account updated account details in cache + cache.accounts[ledger][address] = res; + extensionStorage.setStorage('cache', cache, null); + + // Add details to session + let wallet = session.wallet; + for (var i = wallet[ledger].length - 1; i >= 0; i--) { + if (!('details' in wallet[ledger][i])){ + wallet[ledger][i].details = res; + } + } + + }); }).catch((e: any) => { sendResponse({error: e.message}); }); @@ -309,14 +321,15 @@ export class InternalMethods { sendResponse(res); // Save asset details in storage if needed let extensionStorage = new ExtensionStorage(); - extensionStorage.getStorage('assets', (savedAssets: any) => { - let assets = savedAssets || { - TestNet: {}, - MainNet: {} - }; - if (!(assetId in assets[ledger])) { - assets[ledger][assetId] = res.asset.params; - extensionStorage.setStorage('assets', assets, null); + extensionStorage.getStorage('cache', (cache: any) => { + if (cache === undefined) + cache = new Cache; + if (!(ledger in cache.assets)) + cache.assets[ledger] = {}; + + if (!(assetId in cache.assets[ledger])) { + cache.assets[ledger][assetId] = res.asset.params; + extensionStorage.setStorage('cache', cache, null); } }); }).catch((e: any) => { diff --git a/packages/extension/src/background/messaging/types.ts b/packages/extension/src/background/messaging/types.ts index 7731d9de..e3da4dc0 100644 --- a/packages/extension/src/background/messaging/types.ts +++ b/packages/extension/src/background/messaging/types.ts @@ -12,4 +12,9 @@ export enum Backend { export enum API { Algod = "Algod", Indexer = "Indexer" +} + +export interface Cache { + assets: object; + accounts: object; } \ No newline at end of file diff --git a/packages/extension/src/background/utils/assetsDetailsHelper.ts b/packages/extension/src/background/utils/assetsDetailsHelper.ts index 68f24ad9..623cc310 100644 --- a/packages/extension/src/background/utils/assetsDetailsHelper.ts +++ b/packages/extension/src/background/utils/assetsDetailsHelper.ts @@ -1,6 +1,7 @@ import { ExtensionStorage } from "@algosigner/storage/src/extensionStorage"; import { InternalMethods } from '../messaging/internalMethods'; -import { Ledger } from "../messaging/types" +import { Cache, Ledger } from "../messaging/types" +import { initializeCache } from './helper'; const TIMEOUT = 500; @@ -31,14 +32,11 @@ export default class AssetsDetailsHelper { } let extensionStorage = new ExtensionStorage(); - extensionStorage.getStorage('assets', (savedAssets: any) => { - let assets = savedAssets || { - [Ledger.TestNet]: {}, - [Ledger.MainNet]: {} - }; + extensionStorage.getStorage('cache', (storedCache: any) => { + let cache: Cache = initializeCache(storedCache, ledger); let assetId = this.assetsToAdd[ledger][0]; - while (assetId in assets[ledger]) { + while (assetId in cache.assets[ledger]) { this.assetsToAdd[ledger].shift(); if (this.assetsToAdd[ledger].length === 0) { this.timeouts[ledger] = null; @@ -49,8 +47,8 @@ export default class AssetsDetailsHelper { let indexer = InternalMethods.getIndexer(ledger); indexer.lookupAssetByID(assetId).do().then((res: any) => { - assets[ledger][assetId] = res.asset.params; - extensionStorage.setStorage('assets', assets, null); + cache.assets[ledger][assetId] = res.asset.params; + extensionStorage.setStorage('cache', cache, null); }).finally(() => { this.timeouts[ledger] = setTimeout(() => this.run(ledger), TIMEOUT); }); diff --git a/packages/extension/src/background/utils/helper.ts b/packages/extension/src/background/utils/helper.ts index 266d85ca..90523518 100644 --- a/packages/extension/src/background/utils/helper.ts +++ b/packages/extension/src/background/utils/helper.ts @@ -1,27 +1,35 @@ -import {Ledger} from '../messaging/types'; +import { Settings } from '../config'; +import { API, Cache, Ledger } from '../messaging/types'; +const algosdk = require("algosdk"); -export default class Helper { - // Ledger helpers - public static ledger(): {[key: string]: any} { - return { - supported: (params: {[key: string]: any}): boolean => { - if( - !('ledger' in params) || - !(Helper.str().capitalize(params.ledger) in Ledger) - ) - return false; - return true; - } +export function getAlgod(ledger: Ledger) { + const params = Settings.getBackendParams(ledger, API.Algod); + return new algosdk.Algodv2(params.apiKey, params.url, params.port); +} + +export function getIndexer(ledger: Ledger) { + const params = Settings.getBackendParams(ledger, API.Indexer); + return new algosdk.Indexer(params.apiKey, params.url, params.port); +} + +// Helper functino +export function initializeCache(c: Cache | undefined, ledger: Ledger | undefined) : Cache { + let cache : Cache; + if (c === undefined) { + cache = { + assets: {}, + accounts: {} } + } else { + cache = c; } - // String helpers - public static str(): {[key: string]: any} { - return { - // Capitalize the first char - capitalize: (s: String): String => { - s = s.toLowerCase(); - return s[0].toUpperCase() + s.substr(1); - } - } + + if (ledger !== undefined) { + if (!(ledger in cache.assets)) + cache.assets[ledger] = {}; + if (!(ledger in cache.accounts)) + cache.accounts[ledger] = {}; } + + return cache; } \ No newline at end of file From fc456045d937f47eaf4a2dfbe21bc9b3b5cf1e4f Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Fri, 30 Oct 2020 02:00:27 +0100 Subject: [PATCH 17/45] Changed SendAlgos to also support sending of ASAs --- packages/ui/src/pages/Account.ts | 2 +- packages/ui/src/pages/SendAlgos.ts | 115 +++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/pages/Account.ts b/packages/ui/src/pages/Account.ts index 7acb20a9..0aae5994 100644 --- a/packages/ui/src/pages/Account.ts +++ b/packages/ui/src/pages/Account.ts @@ -69,7 +69,7 @@ const Account: FunctionalComponent = (props: any) => {
<${Link} id="sendAlgos" class="button is-primary is-fullwidth py-2" href=${`${url}/send`}> - Send Algos + Send Algos or ASAs
diff --git a/packages/ui/src/pages/SendAlgos.ts b/packages/ui/src/pages/SendAlgos.ts index 39712a0e..42478f2a 100644 --- a/packages/ui/src/pages/SendAlgos.ts +++ b/packages/ui/src/pages/SendAlgos.ts @@ -17,6 +17,9 @@ const SendAlgos: FunctionalComponent = (props: any) => { const { matches, ledger, address } = props; const [askAuth, setAskAuth] = useState(false); + const [ddActive, setDdActive] = useState(false); + // Asset {} is Algos + const [asset, setAsset] = useState({}); const [to, setTo] = useState(''); const [amount, setAmount] = useState(''); const [note, setNote] = useState(''); @@ -34,24 +37,60 @@ const SendAlgos: FunctionalComponent = (props: any) => { } } + let ddClass: string = "dropdown is-right"; + if (ddActive) + ddClass += " is-active"; + + const selectAsset = (selectAsset) => { + setDdActive(false); + + // Load details if they are not present in the session. + if ('decimals' in selectAsset) { + setAsset(selectAsset) + } else { + const params = { + 'ledger': ledger, + 'asset-id': asset['asset-id'] + }; + + sendMessage(JsonRpcMethod.AssetDetails, params, function(response) { + const keys = Object.keys(response.asset.params); + for (var i = keys.length - 1; i >= 0; i--) { + selectAsset[keys[i]] = response.asset.params[keys[i]]; + } + setAsset(selectAsset) + }); + } + }; + + const checkAndSubmit = () => { + + }; + const sendTx = async (pwd: string) => { setLoading(true); setAuthError(''); setError(''); - const params = { + let params : any = { ledger: ledger, passphrase: pwd, address: account.address, txnParams: { - type: "pay", from: account.address, to: to, note: note, - amount: +amount*1e6, + amount: amount } }; + if ('asset-id' in asset) { + params.txnParams.type = 'axfer'; + params.txnParams.assetIndex = asset['asset-id']; + } else { + params.txnParams.type = 'pay'; + } + sendMessage(JsonRpcMethod.SignSendTransaction, params, function(response) { if ('error' in response) { setLoading(false); @@ -75,18 +114,63 @@ const SendAlgos: FunctionalComponent = (props: any) => {
<${HeaderView} action="${() => route(`/${matches.ledger}/${matches.address}`)}" - title="Send Algos" /> + title="Send Algos or ASAs" />
-
- setAmount(e.target.value)} /> - Algos +
+
+ setAmount(e.target.value)} /> + ${'decimals' in asset && html` +

+ This asset allows for ${asset['decimals']} decimal(s) +

+ `} +
+
+ + +
+ From
{
- Payment + ${'asset-id' in asset && html` + Asset transfer + `} + ${!('asset-id' in asset) && html` + Payment + `}