├── .env.template
├── .eslintrc
├── .github
├── issue-comments-validate.yml
└── workflows
│ ├── ci.yml
│ └── comments-moderation.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc
├── .storybook
├── main.js
├── mocks
│ └── webextension-polyfill.mock.ts
└── preview.jsx
├── .tool-versions
├── LICENSE
├── MessageSigning.md
├── NOTICE
├── README.md
├── babel.config.js
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── secrets.testing.js
├── src
├── api
│ ├── extension
│ │ ├── index.js
│ │ └── wallet.js
│ ├── loader.js
│ ├── messaging.js
│ ├── migration-tool
│ │ ├── cross-extension-messaging
│ │ │ ├── nami-migration-client.extension.ts
│ │ │ ├── nami
│ │ │ │ ├── await-lace-pong-response.ts
│ │ │ │ ├── create-nami-migration-listener.spec.ts
│ │ │ │ ├── create-nami-migration-listener.ts
│ │ │ │ ├── environment.ts
│ │ │ │ └── set-migration-to-in-progress.ts
│ │ │ └── shared
│ │ │ │ ├── exceptions.ts
│ │ │ │ ├── test-helpers.ts
│ │ │ │ └── types.ts
│ │ └── migrator
│ │ │ ├── migration-data.data.ts
│ │ │ ├── migration-state.data.ts
│ │ │ ├── nami-storage-mapper.data.ts
│ │ │ ├── nami-storage-mapper.spec.ts
│ │ │ ├── nami-storage.data.ts
│ │ │ └── nami-storage.fixture.ts
│ ├── util.js
│ └── webpage
│ │ ├── eventRegistration.js
│ │ └── index.js
├── assets
│ └── img
│ │ ├── Nami.ai
│ │ ├── ada.png
│ │ ├── bannerBlack.svg
│ │ ├── bannerWhite.svg
│ │ ├── icon-128.png
│ │ ├── icon-34.png
│ │ ├── iohk.svg
│ │ ├── iohkWhite.svg
│ │ ├── ledgerLogo.svg
│ │ ├── logo.svg
│ │ ├── logoWhite.svg
│ │ └── trezorLogo.svg
├── config
│ ├── config.js
│ └── provider.js
├── features
│ ├── analytics
│ │ ├── config.ts
│ │ ├── event-tracker.ts
│ │ ├── events.ts
│ │ ├── hooks.ts
│ │ ├── posthog.ts
│ │ ├── provider.tsx
│ │ ├── services.ts
│ │ ├── types.ts
│ │ └── ui
│ │ │ └── AnalyticsConsentModal.jsx
│ ├── feature-flags
│ │ └── provider.tsx
│ ├── sentry.js
│ ├── settings
│ │ └── legal
│ │ │ └── LegalSettings.tsx
│ └── terms-and-privacy
│ │ ├── config.ts
│ │ ├── hooks.ts
│ │ ├── index.ts
│ │ ├── provider.tsx
│ │ ├── services.ts
│ │ └── ui
│ │ └── TermsAndPrivacyModal.tsx
├── manifest.json
├── migrations
│ ├── migration.js
│ └── versions
│ │ ├── 1.0.0.js
│ │ ├── 1.1.5.js
│ │ ├── 1.1.7.js
│ │ ├── 2.0.0.js
│ │ ├── 2.1.0.js
│ │ ├── 2.2.0.js
│ │ ├── 2.3.0.js
│ │ ├── 2.3.2.js
│ │ ├── 2.3.3.js
│ │ ├── 3.0.0.js
│ │ ├── 3.0.2.js
│ │ └── 3.3.0.js
├── pages
│ ├── Background
│ │ └── index.js
│ ├── Content
│ │ ├── index.js
│ │ ├── injected.js
│ │ └── trezorContentScript.js
│ ├── Popup
│ │ ├── internalPopup.html
│ │ └── mainPopup.html
│ └── Tab
│ │ ├── createWalletTab.html
│ │ ├── hwTab.html
│ │ └── trezorTx.html
├── test
│ └── unit
│ │ ├── api
│ │ ├── extension
│ │ │ └── index.test.js
│ │ ├── util.test.js
│ │ └── webpage
│ │ │ └── eventRegistration.test.js
│ │ └── migrations
│ │ └── migration.test.js
├── ui
│ ├── app.jsx
│ ├── app
│ │ ├── components
│ │ │ ├── UpgradeModal.jsx
│ │ │ ├── about.jsx
│ │ │ ├── account.jsx
│ │ │ ├── asset.jsx
│ │ │ ├── assetBadge.jsx
│ │ │ ├── assetPopover.jsx
│ │ │ ├── assetPopoverDiff.jsx
│ │ │ ├── assetsModal.jsx
│ │ │ ├── assetsViewer.jsx
│ │ │ ├── autocomplete.jsx
│ │ │ ├── avatarLoader.jsx
│ │ │ ├── changePasswordModal.jsx
│ │ │ ├── collectible.jsx
│ │ │ ├── collectiblesViewer.jsx
│ │ │ ├── confirmModal.jsx
│ │ │ ├── copy.jsx
│ │ │ ├── historyViewer.jsx
│ │ │ ├── privacyPolicy.jsx
│ │ │ ├── qrCode.jsx
│ │ │ ├── scrollbar.jsx
│ │ │ ├── styles.css
│ │ │ ├── termsOfUse.jsx
│ │ │ ├── transaction.jsx
│ │ │ ├── transactionBuilder.jsx
│ │ │ ├── trezorWidget.jsx
│ │ │ └── unitDisplay.jsx
│ │ ├── pages
│ │ │ ├── enable.jsx
│ │ │ ├── noWallet.jsx
│ │ │ ├── send.jsx
│ │ │ ├── settings.jsx
│ │ │ ├── signData.jsx
│ │ │ ├── signTx.jsx
│ │ │ ├── wallet.jsx
│ │ │ └── welcome.jsx
│ │ └── tabs
│ │ │ ├── createWallet.jsx
│ │ │ ├── hw.jsx
│ │ │ └── trezorTx.jsx
│ ├── index.jsx
│ ├── indexInternal.jsx
│ ├── indexMain.jsx
│ ├── lace-migration
│ │ ├── assets
│ │ │ ├── arrow.svg
│ │ │ ├── backpack.svg
│ │ │ ├── checkmark.svg
│ │ │ ├── chevron-left.svg
│ │ │ ├── chevron-right.svg
│ │ │ ├── clock.svg
│ │ │ ├── done-dark.svg
│ │ │ ├── done-white.svg
│ │ │ ├── download.svg
│ │ │ ├── features.svg
│ │ │ ├── grouped-dark-mode.svg
│ │ │ ├── grouped-white-mode.svg
│ │ │ ├── lace-icon.svg
│ │ │ ├── pending-dark-mode.svg
│ │ │ └── pending-white-mode.svg
│ │ └── components
│ │ │ ├── all-done
│ │ │ ├── all-done.component.jsx
│ │ │ └── all-done.stories.js
│ │ │ ├── almost-there
│ │ │ ├── almost-there.component.jsx
│ │ │ └── almost-there.stories.js
│ │ │ ├── carousel
│ │ │ ├── carousel.component.jsx
│ │ │ ├── carousel.stories.jsx
│ │ │ └── slides
│ │ │ │ ├── Slide1.component.jsx
│ │ │ │ ├── Slide1.stories.js
│ │ │ │ ├── Slide2.component.jsx
│ │ │ │ ├── Slide2.stories.js
│ │ │ │ ├── Slide3.component.jsx
│ │ │ │ └── Slide3.stories.js
│ │ │ ├── dismiss-btn.jsx
│ │ │ ├── get-color.js
│ │ │ ├── index.js
│ │ │ ├── migration-view
│ │ │ ├── migration-view.component.jsx
│ │ │ └── migration-view.stories.js
│ │ │ ├── migration.component.jsx
│ │ │ ├── no-wallet
│ │ │ ├── no-wallet.component.jsx
│ │ │ └── no-wallet.stories.js
│ │ │ ├── slide.component.jsx
│ │ │ └── text.component.jsx
│ ├── store.jsx
│ └── theme.jsx
└── wasm
│ └── cardano_message_signing
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── LICENSE
│ ├── cardano_message_signing.generated.js
│ ├── cardano_message_signing_bg.wasm
│ ├── nodejs
│ └── cardano_message_signing.generated.js
│ └── src
│ ├── builders.rs
│ ├── cbor.rs
│ ├── crypto.rs
│ ├── error.rs
│ ├── lib.rs
│ ├── serialization.rs
│ ├── tests.rs
│ └── utils.rs
├── tsconfig.json
├── utils
├── build.js
├── env.js
└── webserver.js
└── webpack.config.js
/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this file as .env
2 | # It's used for authentication when uploading source maps.
3 | SENTRY_AUTH_TOKEN=
4 | SENTRY_ORG=
5 | SENTRY_PROJECT=
6 | SENTRY_DSN=
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "globals": {
4 | "chrome": "readonly"
5 | },
6 | "env": {
7 | "es2020": true,
8 | "browser": true,
9 | "node": true
10 | },
11 | "parserOptions": {
12 | "babelOptions": {
13 | "presets": [
14 | ["babel-preset-react-app", false],
15 | "babel-preset-react-app/prod"
16 | ]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/issue-comments-validate.yml:
--------------------------------------------------------------------------------
1 | validates:
2 | - key: check1
3 | bodies:
4 | - "www"
5 | - ".com"
6 | - "dot com"
7 | - "http"
8 | - "https"
9 | - "support"
10 | - "Support"
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Test and Build
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | name: Build and test
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 | - name: Install dependencies
22 | run: npm ci
23 | - name: Test
24 | run: npm test
25 | - name: Generates Secrets File
26 | run: |
27 | echo "export default { PROJECT_ID_MAINNET: '${{ secrets.PROJECT_ID_MAINNET }}', PROJECT_ID_PREVIEW: '${{ secrets.PROJECT_ID_PREVIEW }}', PROJECT_ID_PREPROD: '${{ secrets.PROJECT_ID_PREPROD }}', POSTHOG_API_KEY:'${{ secrets.POSTHOG_API_KEY }}', POSTHOG_PROJECT_ID: '${{ secrets.POSTHOG_PROJECT_ID }}', LACE_EXTENSION_ID: '${{ secrets.LACE_EXTENSION_ID }}'};" > secrets.production.js
28 | - name: Build
29 | run: npm run build
30 | env:
31 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
32 | SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
33 | SENTRY_ORG: ${{ vars.SENTRY_ORG }}
34 | SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
35 | - name: Upload build
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: build
39 | path: build
40 |
--------------------------------------------------------------------------------
/.github/workflows/comments-moderation.yml:
--------------------------------------------------------------------------------
1 | name: Issue Comments Validate
2 | on:
3 | issue_comment:
4 | types:
5 | - created
6 | - edited
7 |
8 | jobs:
9 | issue_comment:
10 | name: Issue Comment
11 | if: ${{ !github.event.issue.pull_request }}
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Git Checkout
15 | id: git-checkout
16 | uses: actions/checkout@v2
17 | - name: Validate
18 | id: validate
19 | uses: fukuiretu/actions-issue-comment-validates@main
20 | with:
21 | debug: true
22 | github-token: ${{ secrets.GITHUB_TOKEN }}
23 | - name: Report
24 | id: report
25 | env:
26 | HTML_URL: ${{ github.event.comment.html_url }}
27 | ID: ${{ github.event.comment.id }}
28 | NUMBER: ${{ github.event.issue.number }}
29 | run: |
30 | echo "ISSUE_NUMBER=$NUMBER" >> "$GITHUB_OUTPUT"
31 | echo "Issue $NUMBER has had a comment (ID: $ID) with restricted words: ${{ steps.validate.outputs.check1 }}. Moderate at $HTML_URL if true"
32 | - name: Post Warning Comment
33 | id: post-warning-comment
34 | if: ${{ steps.validate.outputs.check1 == 'true' }}
35 | uses: peter-evans/create-or-update-comment@v3
36 | with:
37 | issue-number: ${{ steps.report.outputs.ISSUE_NUMBER }}
38 | body: |
39 | ## The previous comment MAY be from a malicious actor :warning:
40 |
41 | - Do not click **links** :x:
42 | - Do not reveal your **recovery phrase** (ever) :x:
43 |
44 | _This is an automated response aimed at protecting users from potential scams_
45 | reactions: 'eyes'
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 | .history
20 |
21 | # secrets
22 | secrets.production.js
23 | secrets.development.js
24 |
25 | # IDEs
26 | .idea
27 |
28 | *storybook.log
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | legacy-peer-deps=true
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.9.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "requirePragma": false,
5 | "arrowParens": "always"
6 | }
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { NormalModuleReplacementPlugin } = require('webpack');
3 |
4 | const config = {
5 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
6 | addons: [
7 | '@storybook/addon-webpack5-compiler-swc',
8 | '@storybook/addon-onboarding',
9 | '@storybook/addon-links',
10 | '@storybook/addon-essentials',
11 | '@chromatic-com/storybook',
12 | '@storybook/addon-interactions',
13 | ],
14 | framework: {
15 | name: '@storybook/react-webpack5',
16 | options: {},
17 | },
18 | webpackFinal: webPackconfig => {
19 | const fileLoaderRule = webPackconfig.module.rules.find(rule =>
20 | rule.test?.test('.svg'),
21 | );
22 | fileLoaderRule.exclude = /\.svg$/;
23 | webPackconfig.module.rules.push({
24 | test: /\.svg$/i,
25 | issuer: /\.[jt]sx?$/,
26 | use: [
27 | {
28 | loader: '@svgr/webpack',
29 | options: {
30 | icon: true,
31 | exportType: 'named',
32 | },
33 | },
34 | ],
35 | });
36 | webPackconfig.resolve.extensions.push('.svg');
37 | if (webPackconfig.plugins) {
38 | webPackconfig.plugins.push(
39 | new NormalModuleReplacementPlugin(
40 | /^webextension-polyfill$/,
41 | path.join(__dirname, './mocks/webextension-polyfill.mock.ts'),
42 | ),
43 | );
44 | }
45 |
46 | return webPackconfig;
47 | },
48 | };
49 | export default config;
50 |
--------------------------------------------------------------------------------
/.storybook/mocks/webextension-polyfill.mock.ts:
--------------------------------------------------------------------------------
1 | export const runtime = {};
2 | export const storage = {};
3 |
--------------------------------------------------------------------------------
/.storybook/preview.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, DarkMode, Flex, LightMode } from '@chakra-ui/react';
3 | import Theme from '../src/ui/theme';
4 |
5 | const preview = {
6 | parameters: {
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/i,
11 | },
12 | },
13 | },
14 | };
15 |
16 | export const decorators = [
17 | (Story) => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | },
35 | ];
36 |
37 | export default preview;
38 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 20.9.0
2 |
--------------------------------------------------------------------------------
/MessageSigning.md:
--------------------------------------------------------------------------------
1 | ```js
2 | const S = require('@emurgo/cardano-serialization-lib-nodejs'); //serialization-lib: https://github.com/Emurgo/cardano-serialization-lib
3 | const MS = require('./message-signing/rust/pkg/emurgo_message_signing'); //message-signing: https://github.com/Emurgo/message-signing/blob/master/examples/rust/src/main.rs
4 |
5 | // Example runs in Node.js (to verify in a browser, the libraries need to be imported asynchronously)
6 |
7 | /**
8 | *
9 | * @param {string} address - hex encoded
10 | * @param {string} payload - hex encoded
11 | * @param {string} coseSign1Hex - hex encoded
12 | */
13 | const verify = (address, payload, coseSign1Hex) => {
14 | const coseSign1 = MS.COSESign1.from_bytes(Buffer.from(coseSign1Hex, 'hex'));
15 | const payloadCose = coseSign1.payload();
16 |
17 | if (verifyPayload(payload, payloadCose))
18 | throw new Error('Payload does not match');
19 |
20 | const protectedHeaders = coseSign1
21 | .headers()
22 | .protected()
23 | .deserialized_headers();
24 | const addressCose = S.Address.from_bytes(
25 | protectedHeaders.header(MS.Label.new_text('address')).as_bytes()
26 | );
27 |
28 | // Commented out the below line in favor of CIP-30, only use if you are using the deprecated window.cardano.signedData(address, payload)
29 | //const publicKeyCose = S.PublicKey.from_bytes(protectedHeaders.key_id());
30 | const key = MS.COSEKey.from_bytes(
31 | Buffer.from(coseKey, 'hex')
32 | );
33 | const publicKeyBytes = key
34 | .header(
35 | MS.Label.new_int(
36 | MS.Int.new_negative(
37 | MS.BigNum.from_str('2')
38 | )
39 | )
40 | )
41 | .as_bytes();
42 | const publicKeyCose =
43 | APILoader.Cardano.PublicKey.from_bytes(publicKeyBytes);
44 |
45 | if (!verifyAddress(address, addressCose, publicKeyCose))
46 | throw new Error('Could not verify because of address mismatch');
47 |
48 | const signature = S.Ed25519Signature.from_bytes(coseSign1.signature());
49 | const data = coseSign1.signed_data().to_bytes();
50 | return publicKeyCose.verify(data, signature);
51 | };
52 |
53 | const verifyPayload = (payload, payloadCose) => {
54 | return Buffer.from(payloadCose, 'hex').compare(Buffer.from(payload, 'hex'));
55 | };
56 |
57 | const verifyAddress = (address, addressCose, publicKeyCose) => {
58 | const checkAddress = S.Address.from_bytes(Buffer.from(address, 'hex'));
59 | if (addressCose.to_bech32() !== checkAddress.to_bech32()) return false;
60 | // check if BaseAddress
61 | try {
62 | const baseAddress = S.BaseAddress.from_address(addressCose);
63 | //reconstruct address
64 | const paymentKeyHash = publicKeyCose.hash();
65 | const stakeKeyHash = baseAddress.stake().as_pub_key();
66 | const reconstructedAddress = S.BaseAddress.new(
67 | checkAddress.network_id(),
68 | S.Credential.new_pub_key(paymentKeyHash),
69 | S.Credential.new_pub_key(stakeKeyHash)
70 | );
71 | if (
72 | checkAddress.to_bech32() !== reconstructedAddress.to_address().to_bech32()
73 | )
74 | return false;
75 |
76 | return true;
77 | } catch (e) {}
78 | // check if RewardAddress
79 | try {
80 | //reconstruct address
81 | const stakeKeyHash = publicKeyCose.hash();
82 | const reconstructedAddress = S.RewardAddress.new(
83 | checkAddress.network_id(),
84 | S.Credential.new_pub_key(stakeKeyHash)
85 | );
86 | if (
87 | checkAddress.to_bech32() !== reconstructedAddress.to_address().to_bech32()
88 | )
89 | return false;
90 |
91 | return true;
92 | } catch (e) {}
93 | return false;
94 | };
95 |
96 | //test
97 | verify(
, , ) //: true or false
98 |
99 | ```
100 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2024 IOHK
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License”). You may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.txt
4 |
5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { presets: ['@babel/preset-env'] };
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleNameMapper: {
3 | // mock out the browser version of WASM bindings with the nodejs bindings
4 | '@dcspark/cardano-multiplatform-lib-browser':
5 | '@dcspark/cardano-multiplatform-lib-nodejs',
6 | '^(.*)../wasm/cardano_message_signing/cardano_message_signing.generated(.*)$':
7 | '$1../wasm/cardano_message_signing/nodejs/cardano_message_signing.generated$2',
8 | // blockfrost keys
9 | secrets: '../../secrets.testing.js',
10 | },
11 | transform: {
12 | '^.+\\.(ts|tsx)?$': 'ts-jest',
13 | '^.+\\.(js|jsx)$': 'babel-jest',
14 | },
15 | transformIgnorePatterns: [
16 | `/node_modules/(?!crypto-random-string)`
17 | ],
18 | setupFilesAfterEnv: ['./jest.setup.js'],
19 | };
20 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | Object.assign(global, require('jest-chrome'));
2 |
3 | // mocking the chrome.storage.local API
4 | global.mockStore = {};
5 | global.chrome.storage.local.get = (key, callback) =>
6 | callback(key ? { [key]: global.mockStore[key] } : global.mockStore);
7 | global.chrome.storage.local.set = (item, callback) => {
8 | global.mockStore = { ...global.mockStore, ...item };
9 | callback();
10 | };
11 | global.chrome.storage.local.clear = () => (global.mockStore = {});
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nami-wallet",
3 | "version": "3.9.6",
4 | "description": "Maintained by IOG",
5 | "license": "Apache-2.0",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/input-output-hk/nami"
9 | },
10 | "scripts": {
11 | "build": "node utils/build.js",
12 | "start": "node utils/webserver.js",
13 | "prettier": "prettier --write '**/*.{js,jsx,css,html}'",
14 | "test": "NODE_ENV=test jest",
15 | "storybook": "storybook dev -p 6006",
16 | "build-storybook": "storybook build"
17 | },
18 | "dependencies": {
19 | "@blaze-cardano/sdk": "^0.1.32",
20 | "@cardano-foundation/ledgerjs-hw-app-cardano": "^7.1.3",
21 | "@cardano-sdk/core": "^0.39.2",
22 | "@chakra-ui/button": "^2.1.0",
23 | "@chakra-ui/checkbox": "^2.3.2",
24 | "@chakra-ui/css-reset": "^2.3.0",
25 | "@chakra-ui/icons": "^2.1.1",
26 | "@chakra-ui/react": "^2.8.2",
27 | "@dcspark/cardano-multiplatform-lib-browser": "^6.0.0",
28 | "@dcspark/milkomeda-constants": "^0.4.0",
29 | "@dicebear/avatars": "^4.6.4",
30 | "@dicebear/avatars-bottts-sprites": "^4.6.4",
31 | "@emotion/react": "^11.4.0",
32 | "@emotion/styled": "^11.3.0",
33 | "@emurgo/cip14-js": "^3.0.1",
34 | "@fontsource/ubuntu": "^5.0.8",
35 | "@ledgerhq/hw-transport-webusb": "^6.28.0",
36 | "@sentry/react": "^8.32.0",
37 | "@sentry/webpack-plugin": "^2.22.4",
38 | "@trezor/connect-web": "^9.3.0",
39 | "bip39": "^3.0.4",
40 | "cardano-hw-interop-lib": "^3.0.2",
41 | "crc": "^4.1.1",
42 | "crypto-random-string": "^5.0.0",
43 | "debounce": "^2.0.0",
44 | "debounce-promise": "^3.1.2",
45 | "easy-peasy": "^6.0.4",
46 | "framer-motion": "^4.1.16",
47 | "javascript-time-ago": "^2.3.7",
48 | "jest-chrome": "^0.8.0",
49 | "lodash": "^4.17.21",
50 | "match-sorter": "^6.1.0",
51 | "node-polyfill-webpack-plugin": "^2.0.1",
52 | "posthog-js": "^1.93.6",
53 | "promise-latest": "^1.0.4",
54 | "qr-code-styling": "^1.6.0-rc.0",
55 | "react": "^18.2.0",
56 | "react-custom-scrollbars-2": "^4.5.0",
57 | "react-dom": "^18.2.0",
58 | "react-icons": "4.2.0",
59 | "react-kawaii": "^0.18.0",
60 | "react-lazy-load-image-component": "^1.5.1",
61 | "react-middle-ellipsis": "^1.2.1",
62 | "react-number-format": "^5.3.1",
63 | "react-router-dom": "^6.18.0",
64 | "react-time-ago": "^7.0.0",
65 | "react-window": "^1.8.10",
66 | "rxjs": "^7.8.1",
67 | "styled-components": "^6.1.1",
68 | "use-constant": "^1.1.0",
69 | "web3-validator": "^2.0.3",
70 | "webextension-polyfill": "^0.12.0"
71 | },
72 | "devDependencies": {
73 | "@babel/preset-env": "^7.23.8",
74 | "@chromatic-com/storybook": "^1.5.0",
75 | "@dcspark/cardano-multiplatform-lib-nodejs": "^6.0.0",
76 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
77 | "@storybook/addon-essentials": "^8.1.6",
78 | "@storybook/addon-interactions": "^8.1.6",
79 | "@storybook/addon-links": "^8.1.6",
80 | "@storybook/addon-onboarding": "^8.1.6",
81 | "@storybook/addon-webpack5-compiler-swc": "^1.0.3",
82 | "@storybook/blocks": "^8.1.6",
83 | "@storybook/react": "^8.1.6",
84 | "@storybook/react-webpack5": "^8.1.6",
85 | "@storybook/test": "^8.1.6",
86 | "@svgr/webpack": "^8.1.0",
87 | "@swc/core": "^1.3.100",
88 | "@tsconfig/node20": "^20.1.2",
89 | "@types/jest": "^29.5.12",
90 | "@types/react": "^18.2.42",
91 | "@types/webextension-polyfill": "^0.10.7",
92 | "babel-eslint": "^10.1.0",
93 | "babel-jest": "^29.7.0",
94 | "babel-loader": "^9.1.3",
95 | "babel-preset-react-app": "^10.0.0",
96 | "clean-webpack-plugin": "^4.0.0",
97 | "copy-webpack-plugin": "^11.0.0",
98 | "css-loader": "^6.8.1",
99 | "dotenv": "^16.4.5",
100 | "dotenv-defaults": "^5.0.2",
101 | "eslint": "^8.54.0",
102 | "eslint-config-react-app": "^7.0.1",
103 | "eslint-plugin-flowtype": "^8.0.3",
104 | "eslint-plugin-import": "^2.22.1",
105 | "eslint-plugin-jsx-a11y": "^6.4.1",
106 | "eslint-plugin-react": "^7.22.0",
107 | "eslint-plugin-react-hooks": "^4.2.0",
108 | "eslint-plugin-storybook": "^0.8.0",
109 | "expose-loader": "^4.1.0",
110 | "file-loader": "^6.2.0",
111 | "fs-extra": "^11.1.1",
112 | "html-loader": "^4.2.0",
113 | "html-webpack-plugin": "^5.2.0",
114 | "jest": "^29.7.0",
115 | "jest-environment-jsdom": "^29.7.0",
116 | "jest-webextension-mock": "^3.9.0",
117 | "prettier": "^3.1.0",
118 | "react-refresh": "^0.14.0",
119 | "sass": "1.69.5",
120 | "sass-loader": "^13.3.2",
121 | "source-map-loader": "^4.0.1",
122 | "storybook": "^8.1.6",
123 | "style-loader": "^3.3.3",
124 | "swc-loader": "^0.2.3",
125 | "terser-webpack-plugin": "^5.1.1",
126 | "ts-jest": "^29.1.1",
127 | "ts-loader": "^9.5.1",
128 | "typescript": "^5.3.2",
129 | "webpack": "^5.88.1",
130 | "webpack-cli": "^5.1.4",
131 | "webpack-dev-server": "^4.15.1"
132 | },
133 | "resolutions": {
134 | "nan": "2.17.0"
135 | },
136 | "engines": {
137 | "node": ">=20.0.0"
138 | },
139 | "eslintConfig": {
140 | "extends": [
141 | "plugin:storybook/recommended"
142 | ]
143 | },
144 | "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
145 | }
146 |
--------------------------------------------------------------------------------
/secrets.testing.js:
--------------------------------------------------------------------------------
1 | export default {
2 | PROJECT_ID_MAINNET: 'DUMMY_MAINNET',
3 | PROJECT_ID_TESTNET: 'DUMMY_TESTNET',
4 | PROJECT_ID_PREVIEW: 'DUMMY_PREVIEW',
5 | PROJECT_ID_PREPROD: 'DUMMY_PREPROD',
6 | POSTHOG_API_KEY: 'DUMMY_POSTHOG_API_KEY',
7 | POSTHOG_PROJECT_ID: 'DUMMY_POSTHOG_PROJECT_ID',
8 | LACE_EXTENSION_ID: 'DUMMY_EXT_ID',
9 | };
10 |
--------------------------------------------------------------------------------
/src/api/loader.js:
--------------------------------------------------------------------------------
1 | import * as wasm from '@dcspark/cardano-multiplatform-lib-browser';
2 | import * as wasm2 from '../wasm/cardano_message_signing/cardano_message_signing.generated';
3 |
4 | /**
5 | * Loads the WASM modules
6 | */
7 |
8 | class Loader {
9 | _wasm = wasm;
10 |
11 | /**
12 | * Instantiate message signing library.
13 | * Loader.Cardano is loaded synchronously and does not require async instantiation.
14 | */
15 | async load() {
16 | if (this._wasm2) return;
17 | try {
18 | await wasm2.instantiate();
19 | } catch (_e) {
20 | // Only happens when running with Jest (Node.js)
21 | }
22 |
23 | /**
24 | * @private
25 | */
26 | this._wasm2 = wasm2;
27 | }
28 |
29 | get Cardano() {
30 | return this._wasm;
31 | }
32 |
33 | get Message() {
34 | return this._wasm2;
35 | }
36 | }
37 |
38 | export default new Loader();
39 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami-migration-client.extension.ts:
--------------------------------------------------------------------------------
1 | import { runtime, storage } from 'webextension-polyfill';
2 |
3 | import {
4 | MigrationState,
5 | MIGRATION_KEY,
6 | DISMISS_MIGRATION_UNTIL,
7 | } from '../migrator/migration-state.data';
8 | import { setMigrationToInProgress } from './nami/set-migration-to-in-progress';
9 | import { LACE_EXTENSION_ID } from './nami/environment';
10 | import { createNamiMigrationListener } from './nami/create-nami-migration-listener';
11 | import { NamiMessages } from './shared/types';
12 | import { awaitLacePongResponse } from './nami/await-lace-pong-response';
13 |
14 | export const checkLaceInstallation = () =>
15 | awaitLacePongResponse(LACE_EXTENSION_ID, runtime.sendMessage);
16 |
17 | export const enableMigration = () => {
18 | storage.local.remove(DISMISS_MIGRATION_UNTIL);
19 | return setMigrationToInProgress(storage.local);
20 | };
21 |
22 | export const dismissMigration = ({
23 | dismissMigrationUntil,
24 | }: {
25 | dismissMigrationUntil: number;
26 | }) => {
27 | storage.local.set({
28 | [MIGRATION_KEY]: MigrationState.Dismissed,
29 | [DISMISS_MIGRATION_UNTIL]: dismissMigrationUntil,
30 | });
31 | };
32 |
33 | export const disableMigration = async () => {
34 | const date = new Date();
35 | const timeInPast = date.setTime(date.getTime() - 1000);
36 | await storage.local.set({
37 | [DISMISS_MIGRATION_UNTIL]: timeInPast,
38 | [MIGRATION_KEY]: MigrationState.None,
39 | });
40 | };
41 |
42 | export const handleLaceMigrationRequests = () =>
43 | runtime.onMessageExternal.addListener(
44 | createNamiMigrationListener(LACE_EXTENSION_ID, storage.local)
45 | );
46 |
47 | export const openLace = () => {
48 | runtime.sendMessage(LACE_EXTENSION_ID, NamiMessages.open);
49 | };
50 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami/await-lace-pong-response.ts:
--------------------------------------------------------------------------------
1 | import { NamiLacePingProtocol, SendMessageToExtension } from '../shared/types';
2 |
3 | export const awaitLacePongResponse = async (
4 | laceExtensionId: string,
5 | sendMessage: SendMessageToExtension,
6 | ): Promise => {
7 | console.log(
8 | '[MIGRATION TO LACE] Checking if Lace is installed by sending ping message',
9 | );
10 | try {
11 | const response = await sendMessage(
12 | laceExtensionId,
13 | NamiLacePingProtocol.ping,
14 | );
15 | console.log('[MIGRATION TO LACE] response from Lace', response);
16 | if (response === NamiLacePingProtocol.pong) {
17 | return true;
18 | }
19 | } catch {
20 | console.log('[MIGRATION TO LACE] got no response from Lace');
21 | return false;
22 | }
23 | console.log('[MIGRATION TO LACE] got wrong pong response from Lace');
24 | return false;
25 | };
26 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami/create-nami-migration-listener.spec.ts:
--------------------------------------------------------------------------------
1 | import { createNamiMigrationListener } from './create-nami-migration-listener';
2 | import {
3 | MIGRATION_KEY,
4 | MigrationState,
5 | } from '../../migrator/migration-state.data';
6 | import { State } from '../../migrator/nami-storage.data';
7 | import { MigrationExceptions } from '../shared/exceptions';
8 | import { fixture } from '../../migrator/nami-storage.fixture';
9 | import { map } from '../../migrator/nami-storage-mapper.data';
10 | import { createMockNamiStore } from '../shared/test-helpers';
11 |
12 | describe('createNamiMigrationListener', () => {
13 | const LACE_ID = 'fakeLaceId';
14 |
15 | it('should return undefined for messages from other extensions than Lace', async () => {
16 | const namiMigrationListener = createNamiMigrationListener(
17 | LACE_ID,
18 | createMockNamiStore(),
19 | );
20 | expect(
21 | await namiMigrationListener(
22 | { type: 'status' },
23 | { id: 'fakeOtherExtensionId' },
24 | ),
25 | ).toBeUndefined();
26 | });
27 |
28 | it('should set migration state to none when receiving abort message from Lace', async () => {
29 | const store = createMockNamiStore();
30 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
31 | const returnValue = await namiMigrationListener(
32 | { type: 'abort' },
33 | { id: LACE_ID },
34 | );
35 | expect(returnValue).toBeUndefined();
36 | expect(store.set).toHaveBeenCalledWith({
37 | [MIGRATION_KEY]: MigrationState.None,
38 | });
39 | });
40 |
41 | it('should set migration state to completed when receiving completed message from Lace', async () => {
42 | const store = createMockNamiStore();
43 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
44 | const returnValue = await namiMigrationListener(
45 | { type: 'completed' },
46 | { id: LACE_ID },
47 | );
48 | expect(returnValue).toBeUndefined();
49 | expect(store.set).toHaveBeenCalledWith({
50 | [MIGRATION_KEY]: MigrationState.Completed,
51 | });
52 | });
53 |
54 | describe('Lace requesting status from Nami', () => {
55 | it('should return migration state none if no migration state has been saved', async () => {
56 | const store = createMockNamiStore();
57 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
58 | const returnValue = await namiMigrationListener(
59 | { type: 'status' },
60 | { id: LACE_ID },
61 | );
62 | expect(returnValue).toBe(MigrationState.None);
63 | });
64 | it('should return the previously saved migration state', async () => {
65 | const savedState: Partial = {
66 | laceMigration: MigrationState.Completed,
67 | };
68 | const store = createMockNamiStore(savedState);
69 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
70 |
71 | const returnValue = await namiMigrationListener(
72 | { type: 'status' },
73 | { id: LACE_ID },
74 | );
75 | expect(returnValue).toBe(savedState.laceMigration);
76 | });
77 | });
78 |
79 | describe('Lace requesting data from Nami', () => {
80 | it('should throw an error if data cant be mapped', async () => {
81 | const store = createMockNamiStore({
82 | laceMigration: MigrationState.InProgress,
83 | });
84 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
85 | await expect(
86 | namiMigrationListener({ type: 'data' }, { id: LACE_ID }),
87 | ).rejects.toBe(MigrationExceptions.FailedToParse);
88 | });
89 |
90 | it('should return mapped nami to migration state', async () => {
91 | const store = createMockNamiStore({
92 | laceMigration: MigrationState.InProgress,
93 | ...fixture,
94 | });
95 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
96 | await expect(
97 | namiMigrationListener({ type: 'data' }, { id: LACE_ID }),
98 | ).resolves.toEqual(map(fixture));
99 | });
100 |
101 | it('should return the data also if migration was already completed in Nami', async () => {
102 | const store = createMockNamiStore({
103 | laceMigration: MigrationState.Completed,
104 | ...fixture,
105 | });
106 | const namiMigrationListener = createNamiMigrationListener(LACE_ID, store);
107 | await expect(
108 | namiMigrationListener({ type: 'data' }, { id: LACE_ID }),
109 | ).resolves.toEqual(map(fixture));
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami/create-nami-migration-listener.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MIGRATION_KEY,
3 | MigrationState,
4 | } from '../../migrator/migration-state.data';
5 | import { MigrationExceptions } from '../shared/exceptions';
6 | import * as Nami from '../../migrator/nami-storage.data';
7 | import { map } from '../../migrator/nami-storage-mapper.data';
8 | import * as Migration from '../../migrator/migration-data.data';
9 | import { LaceMessages, MessageSender, NamiStore } from '../shared/types';
10 |
11 | // Creates a runtime.onMessageExternal event listener as documented here
12 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessageExternal#addlistener_syntax
13 | export const createNamiMigrationListener =
14 | (laceExtensionId: string, store: NamiStore) =>
15 | async (
16 | message: LaceMessages,
17 | sender: MessageSender,
18 | ): Promise => {
19 | console.log(
20 | '[NAMI MIGRATION] createNamiMigrationListener',
21 | message,
22 | sender,
23 | );
24 | if (sender.id !== laceExtensionId) return undefined;
25 |
26 | if (message.type === 'abort') {
27 | await store.set({
28 | [MIGRATION_KEY]: MigrationState.None,
29 | });
30 | return;
31 | }
32 |
33 | if (message.type === 'completed') {
34 | await store.set({
35 | [MIGRATION_KEY]: MigrationState.Completed,
36 | });
37 | return;
38 | }
39 |
40 | const data: Partial = await store.get();
41 |
42 | if (message.type === 'status') {
43 | return data?.laceMigration ?? MigrationState.None;
44 | }
45 |
46 | if (message.type === 'data') {
47 | try {
48 | return map(data);
49 | } catch {
50 | return Promise.reject(MigrationExceptions.FailedToParse);
51 | }
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami/environment.ts:
--------------------------------------------------------------------------------
1 | import secrets from '../../../../config/provider';
2 |
3 | if (secrets.LACE_EXTENSION_ID === undefined) {
4 | throw new Error('LACE_EXTENSION_ID must be defined');
5 | }
6 | export const LACE_EXTENSION_ID = secrets.LACE_EXTENSION_ID;
7 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/nami/set-migration-to-in-progress.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MIGRATION_KEY,
3 | MigrationState,
4 | } from '../../migrator/migration-state.data';
5 | import { NamiStore } from '../shared/types';
6 |
7 | export const setMigrationToInProgress = (store: NamiStore) => {
8 | return store.set({ [MIGRATION_KEY]: MigrationState.InProgress });
9 | };
10 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/shared/exceptions.ts:
--------------------------------------------------------------------------------
1 | export enum MigrationExceptions {
2 | NotInProgress = 'not-in-progress',
3 | FailedToParse = 'failed-to-parse',
4 | }
5 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/shared/test-helpers.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../../migrator/nami-storage.data';
2 |
3 | export const createMockNamiStore = (mockedState: Partial = {}) => {
4 | const store = {
5 | set: jest.fn(),
6 | get: jest.fn(),
7 | };
8 | store.get.mockResolvedValue(mockedState);
9 | return store;
10 | };
11 |
--------------------------------------------------------------------------------
/src/api/migration-tool/cross-extension-messaging/shared/types.ts:
--------------------------------------------------------------------------------
1 | import * as Nami from '../../migrator/nami-storage.data';
2 |
3 | export interface MessageSender {
4 | id?: string;
5 | }
6 |
7 | export type SendMessageToExtension = (
8 | id: string,
9 | message: unknown,
10 | ) => Promise;
11 |
12 | export enum NamiLacePingProtocol {
13 | ping = 'ping',
14 | pong = 'pong',
15 | }
16 |
17 | export type NamiStore = {
18 | set: (value: Partial) => Promise;
19 | get: () => Promise>;
20 | };
21 |
22 | export type LaceMessages =
23 | | {
24 | type: 'status';
25 | }
26 | | {
27 | type: 'data';
28 | }
29 | | {
30 | type: 'abort';
31 | }
32 | | {
33 | type: 'completed';
34 | };
35 |
36 | export enum NamiMessages {
37 | 'open' = 'open',
38 | }
39 |
--------------------------------------------------------------------------------
/src/api/migration-tool/migrator/migration-data.data.ts:
--------------------------------------------------------------------------------
1 | export interface State {
2 | encryptedPrivateKey: string;
3 | accounts: Account[];
4 | hardwareWallets: HarwareWallet[];
5 | dapps: string[];
6 | currency: 'usd' | 'eur';
7 | analytics: Analytics;
8 | themeColor: string;
9 | }
10 |
11 | export interface Account {
12 | index: number;
13 | name: string;
14 | extendedAccountPublicKey: string;
15 | collaterals: Record;
16 | paymentAddresses: Record;
17 | }
18 |
19 | export interface HarwareWallet extends Account {
20 | vendor: 'ledger' | 'trezor';
21 | }
22 |
23 | export interface Collateral {
24 | lovelace: string;
25 | tx: {
26 | hash: string;
27 | index: number;
28 | };
29 | }
30 |
31 | export type Networks = 'mainnet' | 'preview' | 'preprod';
32 |
33 | export interface Analytics {
34 | enabled: boolean;
35 | userId: string;
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/migration-tool/migrator/migration-state.data.ts:
--------------------------------------------------------------------------------
1 | export enum MigrationState {
2 | None = 'none',
3 | InProgress = 'in-progress',
4 | Completed = 'completed',
5 | Dismissed = 'dismissed',
6 | Dormant = 'dormant', // only available to set during canary phase
7 | }
8 |
9 | export const MIGRATION_KEY = 'laceMigration' as const;
10 | export const DISMISS_MIGRATION_UNTIL = 'dismissMigrationUntil' as const;
11 |
--------------------------------------------------------------------------------
/src/api/migration-tool/migrator/nami-storage-mapper.data.ts:
--------------------------------------------------------------------------------
1 | import * as Nami from './nami-storage.data';
2 | import {
3 | Account,
4 | Collateral,
5 | HarwareWallet,
6 | Networks,
7 | State,
8 | } from './migration-data.data';
9 |
10 | const mapCollateral = (network: Nami.NetworkInfo): Collateral | undefined => {
11 | if (!network.collateral) return undefined;
12 | return {
13 | lovelace: network.collateral.lovelace!,
14 | tx: {
15 | hash: network.collateral.txHash!,
16 | index: network.collateral.txId!,
17 | },
18 | };
19 | };
20 |
21 | export const map = (namiState: Partial): State => {
22 | if (namiState.encryptedKey === undefined || namiState.userId === undefined) {
23 | throw new Error('provided nami state is not correct');
24 | }
25 | const namiAccounts = Object.entries(namiState.accounts!);
26 |
27 | const accounts: Account[] = [];
28 | const hardwareWallets: HarwareWallet[] = [];
29 |
30 | while (namiAccounts.length > 0) {
31 | const [key, account] = namiAccounts.pop()!;
32 |
33 | const collaterals: Record = {
34 | mainnet: mapCollateral(account.mainnet),
35 | preview: mapCollateral(account.preview),
36 | preprod: mapCollateral(account.preprod),
37 | };
38 |
39 | const paymentAddresses: Record = {
40 | mainnet: account.mainnet.paymentAddr,
41 | preview: account.preview.paymentAddr,
42 | preprod: account.preprod.paymentAddr,
43 | };
44 |
45 | if (key.startsWith('ledger') || key.startsWith('trezor')) {
46 | const [type, _, index] = key.split('-');
47 | hardwareWallets.push({
48 | index: parseInt(index),
49 | name: account.name,
50 | extendedAccountPublicKey: account.publicKey,
51 | collaterals,
52 | vendor: type === 'ledger' ? 'ledger' : 'trezor',
53 | paymentAddresses,
54 | });
55 | } else {
56 | accounts.push({
57 | index: parseInt(key),
58 | name: account.name,
59 | extendedAccountPublicKey: account.publicKey,
60 | collaterals,
61 | paymentAddresses,
62 | });
63 | }
64 | }
65 |
66 | accounts.sort((a, b) => a.index - b.index);
67 | hardwareWallets.sort((a, b) => a.index - b.index);
68 |
69 | return {
70 | encryptedPrivateKey: namiState.encryptedKey,
71 | accounts,
72 | hardwareWallets,
73 | dapps: namiState.whitelisted || [],
74 | currency: namiState.currency === 'usd' ? 'usd' : 'eur',
75 | analytics: {
76 | enabled: Boolean(namiState.analytics),
77 | userId: namiState.userId,
78 | },
79 | themeColor: namiState.themeColor || 'light',
80 | };
81 | };
82 |
--------------------------------------------------------------------------------
/src/api/migration-tool/migrator/nami-storage-mapper.spec.ts:
--------------------------------------------------------------------------------
1 | import { map } from './nami-storage-mapper.data';
2 | import { fixture } from './nami-storage.fixture';
3 |
4 | test('map nami storage to migration payload', async () => {
5 | const migrationPayload = map(fixture);
6 |
7 | expect(migrationPayload).toEqual({
8 | encryptedPrivateKey:
9 | 'da7af7c22eeaf4bb460c02b426792d556a9242a7e8dca47e7628350f4290d97a0078f3dad5812606f4fa993772dc1c0fc5b7941dc91796a111a8d789b8d3f473eb9c67b32f89f5a2518ff02bb5595a00638e99e7799858b42a639edab14d1bd997eeb8ebe518939ca0522a527219062d1585bfd34434dd84c2a3f895d34863158fce54c1c2c2ab8e8fd3d23d70bc3114fd302badca2c850160597443',
10 | accounts: [
11 | {
12 | index: 0,
13 | name: 'Nami',
14 | extendedAccountPublicKey:
15 | 'a5f18f73dde7b6f11df448913d60a86bbb397a435269e5024193b293f28892fd33d1225d468aac8f5a9d3cfedceacabe80192fcf0beb5c5c9b7988151f3353cc',
16 | collaterals: {
17 | mainnet: undefined,
18 | preview: undefined,
19 | preprod: undefined,
20 | },
21 | paymentAddresses: {
22 | mainnet:
23 | 'addr1qymlvzkhufx0f94vqtfsmfa7yzxtwql8ga8aq5yk6u98gn593a82g73tm9n0l6vehusxn3fxwwxhrssgmvnwnlaa6p4qw7srzg',
24 | preprod:
25 | 'addr_test1qqmlvzkhufx0f94vqtfsmfa7yzxtwql8ga8aq5yk6u98gn593a82g73tm9n0l6vehusxn3fxwwxhrssgmvnwnlaa6p4qdgdrwh',
26 | preview:
27 | 'addr_test1qqmlvzkhufx0f94vqtfsmfa7yzxtwql8ga8aq5yk6u98gn593a82g73tm9n0l6vehusxn3fxwwxhrssgmvnwnlaa6p4qdgdrwh',
28 | },
29 | },
30 | {
31 | index: 1,
32 | name: 'xxx',
33 | extendedAccountPublicKey:
34 | '5280ef1287dfa35605891eb788590dbfe43b59682ada939ee111f8667d4a0847b43c08b5dce7aab937e860626e95f05ef6cc12758fa9ee16a4fc394bd9f684e4',
35 | collaterals: {
36 | mainnet: undefined,
37 | preview: undefined,
38 | preprod: undefined,
39 | },
40 | paymentAddresses: {
41 | mainnet:
42 | 'addr1qxaqtjxrdnaxm2a74r0xtd0a5jkg3nd9zdzuvy7yc5a67qgte8c6lysmrq57uy7hgq9daj5sylgttehecjjrmgn0n2nsgwaxqs',
43 | preprod:
44 | 'addr_test1qzaqtjxrdnaxm2a74r0xtd0a5jkg3nd9zdzuvy7yc5a67qgte8c6lysmrq57uy7hgq9daj5sylgttehecjjrmgn0n2nstcqxv0',
45 | preview:
46 | 'addr_test1qzaqtjxrdnaxm2a74r0xtd0a5jkg3nd9zdzuvy7yc5a67qgte8c6lysmrq57uy7hgq9daj5sylgttehecjjrmgn0n2nstcqxv0',
47 | },
48 | },
49 | ],
50 | hardwareWallets: [
51 | {
52 | index: 0,
53 | name: 'Ledger 1',
54 | extendedAccountPublicKey:
55 | '7eefc2120ec17dc280f7f7adba233bcd75c00d59d9442ded45e44e00745e28d4d06673111ee5aad359f25fafbb787c55f1f80e0d9f0b567959d0a3587276210c',
56 | collaterals: {
57 | mainnet: undefined,
58 | preview: {
59 | lovelace: '5000000',
60 | tx: {
61 | hash: '5aeae083cceb3a930f3402d367096d2d524d03abf915fd9452cc59b3063a6aad',
62 | index: 0,
63 | },
64 | },
65 | preprod: undefined,
66 | },
67 | paymentAddresses: {
68 | mainnet:
69 | 'addr1q85g8x8lpu6ydhe5ytggl6vrlt6prtdzzk7nljh6uf23ktknjausjhrf7f92l3ze9pp5njhkwrv45phhryjxjhtmq7rqnmrrjv',
70 | preprod:
71 | 'addr_test1qr5g8x8lpu6ydhe5ytggl6vrlt6prtdzzk7nljh6uf23ktknjausjhrf7f92l3ze9pp5njhkwrv45phhryjxjhtmq7rqsd7r7n',
72 | preview:
73 | 'addr_test1qr5g8x8lpu6ydhe5ytggl6vrlt6prtdzzk7nljh6uf23ktknjausjhrf7f92l3ze9pp5njhkwrv45phhryjxjhtmq7rqsd7r7n',
74 | },
75 | vendor: 'ledger',
76 | },
77 | {
78 | index: 1,
79 | name: 'Ledger 2',
80 | extendedAccountPublicKey:
81 | '18b35d8e07c1dd096ce359f4ce5ac669a27c8ac23583f9e6a53b7508efd28c849a7b1eda5ac98ed02d6048d0cbe84f91570b9f0cc3acff935cf229cd798da730',
82 | collaterals: {
83 | mainnet: undefined,
84 | preview: undefined,
85 | preprod: undefined,
86 | },
87 | paymentAddresses: {
88 | mainnet:
89 | 'addr1qyhxhljsyfuzv4f2dyaxpn6why7eher90twkcyjvpyzmm27wxzqczmxjrtvpm39rjcqj4tqt2vjwt3a4t92ujuwes4jqxx3xgd',
90 | preprod:
91 | 'addr_test1qqhxhljsyfuzv4f2dyaxpn6why7eher90twkcyjvpyzmm27wxzqczmxjrtvpm39rjcqj4tqt2vjwt3a4t92ujuwes4jq9svxyj',
92 | preview:
93 | 'addr_test1qqhxhljsyfuzv4f2dyaxpn6why7eher90twkcyjvpyzmm27wxzqczmxjrtvpm39rjcqj4tqt2vjwt3a4t92ujuwes4jq9svxyj',
94 | },
95 | vendor: 'ledger',
96 | },
97 | ],
98 | dapps: ['https://preview.handle.me'],
99 | currency: 'usd',
100 | analytics: {
101 | enabled: true,
102 | userId:
103 | 'b60f45ed66f596ebfd2ca19ff704cfee33e316795da50f295fc1f85d6ddf539c',
104 | },
105 | themeColor: 'light',
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/src/api/migration-tool/migrator/nami-storage.data.ts:
--------------------------------------------------------------------------------
1 | import { MigrationState } from './migration-state.data';
2 |
3 | interface Asset {
4 | decimals?: number | null;
5 | has_nft_onchain_metadata?: boolean;
6 | quantity?: string;
7 | unit?: string;
8 | }
9 |
10 | interface BlockDetail {
11 | block_vrf?: string;
12 | confirmations?: number;
13 | epoch?: number;
14 | epoch_slot?: number;
15 | fees?: string;
16 | hash?: string;
17 | height?: number;
18 | next_block?: string;
19 | op_cert?: string;
20 | op_cert_counter?: string;
21 | output?: string;
22 | previous_block?: string;
23 | size?: number;
24 | slot?: number;
25 | slot_leader?: string;
26 | time?: number;
27 | tx_count?: number;
28 | }
29 |
30 | interface TransactionInfo {
31 | asset_mint_or_burn_count?: number;
32 | block?: string;
33 | block_height?: number;
34 | block_time?: number;
35 | delegation_count?: number;
36 | deposit?: string;
37 | fees?: string;
38 | hash?: string;
39 | index?: number;
40 | invalid_before?: null | string;
41 | invalid_hereafter?: string;
42 | mir_cert_count?: number;
43 | output_amount?: Array<{
44 | quantity?: string;
45 | unit?: string;
46 | }>;
47 | pool_retire_count?: number;
48 | pool_update_count?: number;
49 | redeemer_count?: number;
50 | size?: number;
51 | slot?: number;
52 | stake_cert_count?: number;
53 | utxo_count?: number;
54 | valid_contract?: boolean;
55 | withdrawal_count?: number;
56 | }
57 |
58 | interface Utxo {
59 | address?: string;
60 | amount?: Array<{
61 | quantity?: string;
62 | unit?: string;
63 | }>;
64 | collateral?: boolean;
65 | data_hash?: null | string;
66 | inline_datum?: null | string;
67 | output_index?: number;
68 | reference?: boolean;
69 | reference_script_hash?: null | string;
70 | tx_hash?: string;
71 | }
72 |
73 | interface TransactionUtxos {
74 | hash?: string;
75 | inputs?: Utxo[];
76 | outputs?: Utxo[];
77 | }
78 |
79 | interface HistoryDetail {
80 | block?: BlockDetail;
81 | info?: TransactionInfo;
82 | utxos?: TransactionUtxos;
83 | }
84 |
85 | interface TxHistory {
86 | confirmed?: string[];
87 | details?: Record;
88 | }
89 |
90 | export interface NetworkInfo {
91 | assets?: Asset[];
92 | history?: TxHistory;
93 | lovelace?: string | null;
94 | minAda?: number | string;
95 | paymentAddr: string;
96 | rewardAddr?: string;
97 | collateral?: {
98 | lovelace?: string;
99 | txHash?: string;
100 | txId?: number;
101 | };
102 | lastUpdate?: string;
103 | }
104 |
105 | export interface Account {
106 | avatar?: string;
107 | index: number | string;
108 | mainnet: NetworkInfo;
109 | name: string;
110 | paymentKeyHash?: string;
111 | paymentKeyHashBech32?: string;
112 | preprod: NetworkInfo;
113 | preview: NetworkInfo;
114 | publicKey: string;
115 | stakeKeyHash?: string;
116 | testnet: NetworkInfo;
117 | }
118 |
119 | interface Migration {
120 | completed?: string[];
121 | version?: string;
122 | }
123 |
124 | interface Network {
125 | id?: string;
126 | node?: string;
127 | }
128 |
129 | export interface State {
130 | acceptedLegalDocsVersion?: number;
131 | accounts?: Record;
132 | analytics?: boolean;
133 | currency?: string;
134 | currentAccount?: string;
135 | encryptedKey: string;
136 | migration?: Migration;
137 | network?: Network;
138 | userId: string;
139 | whitelisted?: string[];
140 | laceMigration?: MigrationState;
141 | themeColor?: string;
142 | }
143 |
--------------------------------------------------------------------------------
/src/api/webpage/eventRegistration.js:
--------------------------------------------------------------------------------
1 | import { TARGET } from '../../config/config';
2 |
3 | /**
4 | * @param {string} eventName
5 | * @param {Function} callback
6 | */
7 | export const on = (eventName, callback) => {
8 | const handler = (event) => callback(event.detail);
9 |
10 | const events = window.cardano.nami._events[eventName] || [];
11 |
12 | window.cardano.nami._events[eventName] = [...events, [callback, handler]];
13 |
14 | window.addEventListener(`${TARGET}${eventName}`, handler);
15 | };
16 |
17 | /**
18 | * @param {string} eventName
19 | * @param {Function} callback
20 | */
21 | export const off = (eventName, callback) => {
22 | const filterHandlersBy = (predicate) => (handlers) =>
23 | handlers.filter(([savedCallback]) => predicate(savedCallback));
24 |
25 | const filterByMatchingHandlers = filterHandlersBy((cb) => cb === callback);
26 | const filterByNonMatchingHandlers = filterHandlersBy((cb) => cb !== callback);
27 |
28 | const eventHandlers = window.cardano.nami._events[eventName];
29 |
30 | if (typeof eventHandlers !== 'undefined') {
31 | const matchingHandlers = filterByMatchingHandlers(eventHandlers);
32 |
33 | for (const [, handler] of matchingHandlers) {
34 | window.removeEventListener(`${TARGET}${eventName}`, handler);
35 | }
36 |
37 | window.cardano.nami._events[eventName] =
38 | filterByNonMatchingHandlers(eventHandlers);
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/api/webpage/index.js:
--------------------------------------------------------------------------------
1 | import { METHOD } from '../../config/config';
2 | import { Messaging } from '../messaging';
3 |
4 | export const getBalance = async () => {
5 | const result = await Messaging.sendToContent({ method: METHOD.getBalance });
6 | return result.data;
7 | };
8 |
9 | export const enable = async () => {
10 | const result = await Messaging.sendToContent({ method: METHOD.enable });
11 | return result.data;
12 | };
13 |
14 | export const isEnabled = async () => {
15 | const result = await Messaging.sendToContent({ method: METHOD.isEnabled });
16 | return result.data;
17 | };
18 |
19 | //deprecated soon
20 | export const signData = async (address, payload) => {
21 | const result = await Messaging.sendToContent({
22 | method: METHOD.signData,
23 | data: { address, payload },
24 | });
25 | return result.data;
26 | };
27 |
28 | export const signDataCIP30 = async (address, payload) => {
29 | const result = await Messaging.sendToContent({
30 | method: METHOD.signData,
31 | data: { address, payload, CIP30: true },
32 | });
33 | return result.data;
34 | };
35 |
36 | export const signTx = async (tx, partialSign = false) => {
37 | const result = await Messaging.sendToContent({
38 | method: METHOD.signTx,
39 | data: { tx, partialSign },
40 | });
41 | return result.data;
42 | };
43 |
44 | export const getAddress = async () => {
45 | const result = await Messaging.sendToContent({
46 | method: METHOD.getAddress,
47 | });
48 | return result.data;
49 | };
50 |
51 | export const getRewardAddress = async () => {
52 | const result = await Messaging.sendToContent({
53 | method: METHOD.getRewardAddress,
54 | });
55 | return result.data;
56 | };
57 |
58 | export const getNetworkId = async () => {
59 | const result = await Messaging.sendToContent({
60 | method: METHOD.getNetworkId,
61 | });
62 | return result.data;
63 | };
64 |
65 | export const getUtxos = async (amount = undefined, paginate = undefined) => {
66 | const result = await Messaging.sendToContent({
67 | method: METHOD.getUtxos,
68 | data: { amount, paginate },
69 | });
70 | return result.data;
71 | };
72 |
73 | export const getCollateral = async () => {
74 | const result = await Messaging.sendToContent({
75 | method: METHOD.getCollateral,
76 | });
77 | return result.data;
78 | };
79 |
80 | export const submitTx = async (tx) => {
81 | const result = await Messaging.sendToContent({
82 | method: METHOD.submitTx,
83 | data: tx,
84 | });
85 | return result.data;
86 | };
87 |
88 | export { on, off } from './eventRegistration';
89 |
--------------------------------------------------------------------------------
/src/assets/img/Nami.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/assets/img/Nami.ai
--------------------------------------------------------------------------------
/src/assets/img/ada.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/assets/img/ada.png
--------------------------------------------------------------------------------
/src/assets/img/bannerBlack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/bannerWhite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/assets/img/icon-128.png
--------------------------------------------------------------------------------
/src/assets/img/icon-34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/assets/img/icon-34.png
--------------------------------------------------------------------------------
/src/assets/img/ledgerLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
107 |
--------------------------------------------------------------------------------
/src/assets/img/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/logoWhite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/trezorLogo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/config/provider.js:
--------------------------------------------------------------------------------
1 | import { NODE } from './config';
2 | import secrets from 'secrets';
3 | import { version } from '../../package.json';
4 |
5 | const networkToProjectId = {
6 | mainnet: secrets.PROJECT_ID_MAINNET,
7 | testnet: secrets.PROJECT_ID_TESTNET,
8 | preprod: secrets.PROJECT_ID_PREPROD,
9 | preview: secrets.PROJECT_ID_PREVIEW,
10 | };
11 |
12 | export default {
13 | api: {
14 | ipfs: 'https://ipfs.blockfrost.dev/ipfs',
15 | base: (node = NODE.mainnet) => node,
16 | header: { [secrets.NAMI_HEADER || 'dummy']: version },
17 | key: (network = 'mainnet') => ({
18 | project_id: networkToProjectId[network],
19 | }),
20 | price: (currency = 'usd') =>
21 | fetch(
22 | `https://api.coingecko.com/api/v3/simple/price?ids=cardano&vs_currencies=${currency}`
23 | )
24 | .then((res) => res.json())
25 | .then((res) => res.cardano[currency]),
26 | },
27 | posthog_api_token: secrets.POSTHOG_API_KEY,
28 | posthog_project_id: secrets.POSTHOG_PROJECT_ID,
29 | LACE_EXTENSION_ID: secrets.LACE_EXTENSION_ID,
30 | };
31 |
--------------------------------------------------------------------------------
/src/features/analytics/config.ts:
--------------------------------------------------------------------------------
1 | import secrets from '../../config/provider';
2 |
3 | export const PUBLIC_POSTHOG_HOST = 'https://eu.posthog.com';
4 |
5 | export const PRODUCTION_TRACKING_MODE_ENABLED = 'true';
6 |
7 | export const POSTHOG_API_KEY = secrets.posthog_api_token;
8 | export const POSTHOG_PROJECT_ID = secrets.posthog_project_id;
9 |
--------------------------------------------------------------------------------
/src/features/analytics/event-tracker.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import debounce from 'debounce';
3 | import { useCaptureEvent } from './hooks';
4 | import { Events } from './events';
5 |
6 | const PAGE_VIEW_DEBOUNCE_DELAY = 1000;
7 |
8 | const debouncedPageView = debounce((fn) => fn(), PAGE_VIEW_DEBOUNCE_DELAY);
9 |
10 | export const EventTracker = () => {
11 | const capture = useCaptureEvent();
12 |
13 | // Track page changes with PostHog in order to keep the user session alive
14 | useEffect(() => {
15 | const trackActivePageChange = () =>
16 | debouncedPageView(() => {
17 | capture(Events.PageView);
18 | });
19 |
20 | window.addEventListener('load', trackActivePageChange);
21 | window.addEventListener('popstate', trackActivePageChange);
22 |
23 | trackActivePageChange();
24 |
25 | return () => {
26 | window.removeEventListener('load', trackActivePageChange);
27 | window.removeEventListener('popstate', trackActivePageChange);
28 | };
29 | }, [capture]);
30 |
31 | return null;
32 | };
33 |
--------------------------------------------------------------------------------
/src/features/analytics/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { usePostHog } from 'posthog-js/react';
3 | import { Events, Properties } from './events';
4 | import { getAnalyticsConsent, getEventMetadata } from './services';
5 | import { useAnalyticsContext } from './provider';
6 |
7 | export const useCaptureEvent = () => {
8 | const posthog = usePostHog();
9 | const [analytics] = useAnalyticsContext();
10 |
11 | const captureEvent = useCallback(
12 | async (event: Events, properties: Properties = {}) => {
13 | const [hasConsent, metadata] = await Promise.all([
14 | getAnalyticsConsent(),
15 | getEventMetadata(analytics.view),
16 | ]);
17 |
18 | if (posthog && hasConsent) {
19 | posthog.capture(event, {
20 | ...properties,
21 | ...metadata,
22 | });
23 | }
24 | },
25 | [posthog, analytics.view]
26 | );
27 |
28 | return captureEvent;
29 | };
30 |
--------------------------------------------------------------------------------
/src/features/analytics/posthog.ts:
--------------------------------------------------------------------------------
1 | import { PostHogConfig } from 'posthog-js';
2 | import { PUBLIC_POSTHOG_HOST } from './config';
3 |
4 | export const getOptions = (userId: string): Partial => ({
5 | request_batching: false,
6 | api_host: PUBLIC_POSTHOG_HOST,
7 | autocapture: false,
8 | disable_session_recording: true,
9 | capture_pageview: false,
10 | capture_pageleave: false,
11 | disable_compression: true,
12 | // Disables PostHog user ID persistence - we manage ID ourselves with userIdService
13 | disable_persistence: true,
14 | disable_cookie: true,
15 | persistence: 'memory',
16 | bootstrap: {
17 | distinctID: userId,
18 | isIdentifiedID: true,
19 | },
20 | property_blacklist: [
21 | '$autocapture_disabled_server_side',
22 | '$console_log_recording_enabled_server_side',
23 | '$device_id',
24 | '$session_recording_recorder_version_server_side',
25 | '$time',
26 | ],
27 | });
28 |
--------------------------------------------------------------------------------
/src/features/analytics/provider.tsx:
--------------------------------------------------------------------------------
1 | import { PostHogProvider } from 'posthog-js/react';
2 | import { PostHogConfig } from 'posthog-js';
3 | import React, {
4 | ReactNode,
5 | useMemo,
6 | createContext,
7 | useContext,
8 | useState,
9 | useEffect,
10 | } from 'react';
11 | import { getOptions } from './posthog';
12 | import { ExtensionViews } from './types';
13 | import { POSTHOG_API_KEY } from './config';
14 | import {
15 | getAnalyticsConsent,
16 | getUserId,
17 | setAnalyticsConsent,
18 | } from './services';
19 | import { FeatureFlagProvider } from '../feature-flags/provider';
20 |
21 | /**
22 | * Represents the user's consent to tracking analytics events
23 | * as well as the current extension view for tracked events
24 | */
25 | interface AnalyticsState {
26 | consent?: boolean;
27 | userId?: string;
28 | view: ExtensionViews;
29 | }
30 |
31 | /**
32 | * Provides access to the AnalyticsState and handling
33 | * the storage of userId and consent in chrome.storage.local
34 | */
35 | const useAnalyticsState = (
36 | view: ExtensionViews
37 | ): [AnalyticsState, (consent: boolean) => Promise] => {
38 | // Store the consent in React state to trigger component updates
39 | const [consentState, setConsentState] = useState({
40 | view,
41 | });
42 |
43 | // Fetch the stored user consent and assign to React state
44 | useEffect(() => {
45 | (async function () {
46 | const [consent, userId] = await Promise.all([
47 | getAnalyticsConsent(),
48 | getUserId(),
49 | ]);
50 | setConsentState({
51 | consent,
52 | userId,
53 | view,
54 | });
55 | })();
56 | }, []);
57 |
58 | return [
59 | consentState,
60 | async (consent) => {
61 | // Allow to set the consent state and store it too
62 | await setAnalyticsConsent(consent);
63 | setConsentState({
64 | consent,
65 | userId: consentState.userId,
66 | view,
67 | });
68 | },
69 | ];
70 | };
71 |
72 | /**
73 | * The analytics React context which is exposed by the hook below
74 | */
75 | const AnalyticsContext = createContext | null>(null);
78 |
79 | /**
80 | * The public hook that should be used by components to interact with the
81 | * analytics state.
82 | */
83 | export const useAnalyticsContext = () => {
84 | const analyticsContext = useContext(AnalyticsContext);
85 | if (analyticsContext === null) throw new Error('context not defined');
86 | return analyticsContext;
87 | };
88 |
89 | /**
90 | * The analytics provider that wraps the current extension
91 | * view to set up the PostHog provider and the API to interact
92 | * with the analytics state.
93 | */
94 | export const AnalyticsProvider = ({
95 | children,
96 | view,
97 | }: {
98 | children: ReactNode;
99 | view: ExtensionViews;
100 | }) => {
101 | const [analyticsState, setAnalyticsConsent] = useAnalyticsState(view);
102 |
103 | const options = useMemo | undefined>(() => {
104 | const id = analyticsState?.userId;
105 |
106 | if (id === undefined) {
107 | return undefined;
108 | }
109 |
110 | return getOptions(id);
111 | }, [analyticsState?.userId]);
112 |
113 | if (options === undefined) {
114 | // avoid rendering everything twice when waiting for Posthog options being fetched
115 | return null;
116 | }
117 |
118 | return (
119 |
120 |
121 |
124 | {children}
125 |
126 |
127 |
128 | );
129 | };
130 |
--------------------------------------------------------------------------------
/src/features/analytics/services.ts:
--------------------------------------------------------------------------------
1 | import { getStorage, setStorage } from '../../api/extension';
2 | import { STORAGE } from '../../config/config';
3 | import cryptoRandomString from 'crypto-random-string';
4 | import { POSTHOG_PROJECT_ID, PRODUCTION_TRACKING_MODE_ENABLED } from './config';
5 | import { ExtensionViews, PostHogMetadata } from './types';
6 |
7 | export const getAnalyticsConsent = (): Promise =>
8 | getStorage(STORAGE.analyticsConsent);
9 |
10 | export const setAnalyticsConsent = (consent: boolean): Promise =>
11 | setStorage({ [STORAGE.analyticsConsent]: consent });
12 |
13 | let userIdCache: string | undefined;
14 |
15 | export const getUserId = async (): Promise => {
16 | if (userIdCache) {
17 | return userIdCache;
18 | }
19 |
20 | const userId = await getStorage(STORAGE.userId);
21 |
22 | if (userId === undefined) {
23 | const newUserId = cryptoRandomString({ length: 64 });
24 |
25 | await setStorage({
26 | [STORAGE.userId]: newUserId,
27 | });
28 |
29 | userIdCache = newUserId;
30 | return newUserId;
31 | }
32 |
33 | userIdCache = userId;
34 |
35 | return userId;
36 | };
37 |
38 | const getNetwork = (): Promise<{ id: 'mainnet' | 'preprod' | 'preview' }> =>
39 | getStorage(STORAGE.network);
40 |
41 | export const getEventMetadata = async (
42 | view: ExtensionViews
43 | ): Promise => {
44 | const [userId, currentNetwork] = await Promise.all([
45 | getUserId(),
46 | getNetwork(),
47 | ]);
48 |
49 | return {
50 | view: view,
51 | sent_at_local: new Date().toISOString(),
52 | distinct_id: userId,
53 | posthog_project_id: POSTHOG_PROJECT_ID,
54 | network: currentNetwork?.id,
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/features/analytics/types.ts:
--------------------------------------------------------------------------------
1 | export enum ExtensionViews {
2 | Extended = 'extended',
3 | Popup = 'popup',
4 | }
5 |
6 | export type PostHogMetadata = {
7 | distinct_id?: string;
8 | alias_id?: string;
9 | view: ExtensionViews;
10 | sent_at_local: string;
11 | posthog_project_id: number;
12 | network: 'mainnet' | 'preprod' | 'preview';
13 | };
14 |
--------------------------------------------------------------------------------
/src/features/analytics/ui/AnalyticsConsentModal.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/button';
2 | import {
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | } from '@chakra-ui/modal';
11 | import { Link, Text } from '@chakra-ui/layout';
12 | import React from 'react';
13 | import PrivacyPolicy from '../../../ui/app/components/privacyPolicy';
14 |
15 | export const AnalyticsConsentModal = ({ askForConsent, setConsent }) => {
16 | const privacyPolRef = React.useRef();
17 |
18 | return (
19 | <>
20 | setConsent(false)}
25 | blockScrollOnMount={false}
26 | closeOnOverlayClick={false}
27 | >
28 |
29 |
30 | Legal & Analytics
31 |
32 |
38 | Give us a hand to improve your Nami experience
39 |
40 |
41 | We would like to collect anonymous information from your browser
42 | extension to help us improve the quality and performance of Nami.
43 | This may include data about how you use our service, your
44 | preferences and information about your system. You can always
45 | opt-out (see the
46 | window.open('https://www.namiwallet.io/')}
48 | textDecoration="underline"
49 | >
50 | FAQ
51 |
52 | for more details). For more information on our privacy
53 | practices, see our
54 | privacyPolRef.current.openModal()}
56 | textDecoration="underline"
57 | >
58 | Privacy Policy
59 |
60 | .
61 |
62 |
63 |
64 |
67 |
70 |
71 |
72 |
73 |
74 | >
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/features/feature-flags/provider.tsx:
--------------------------------------------------------------------------------
1 | import { EarlyAccessFeature } from 'posthog-js';
2 | import { usePostHog, useActiveFeatureFlags } from 'posthog-js/react';
3 | import React, {
4 | ReactNode,
5 | createContext,
6 | useContext,
7 | useEffect,
8 | useState,
9 | } from 'react';
10 |
11 | type MigrationFlag = {
12 | dismissInterval: number; // seconds to add to current time until migration prompt appears again
13 | dismissable: true; // can we dismiss the migration
14 | };
15 |
16 | type FeatureFlags = {
17 | ['is-migration-active']?: MigrationFlag;
18 | [key: string]: unknown;
19 | };
20 |
21 | /**
22 | * The feature flag React context which is exposed by the hook below
23 | */
24 | export const FeatureFlagContext = createContext<{
25 | featureFlags?: FeatureFlags;
26 | earlyAccessFeatures?: EarlyAccessFeature[];
27 | isFFLoaded?: boolean;
28 | }>({});
29 |
30 | /**
31 | * The public hook that should be used by components to interact with the
32 | * feature flags.
33 | */
34 | export const useFeatureFlagsContext = () => {
35 | const featureFlagContext = useContext(FeatureFlagContext);
36 | if (featureFlagContext === null)
37 | throw new Error('feature flag context not defined');
38 | return featureFlagContext;
39 | };
40 |
41 | /**
42 | * The feature flag provider is separated from analytics
43 | * to provide the ability to read flags without the need
44 | * to accept analytics tracking.
45 | */
46 | export const FeatureFlagProvider = ({ children }: { children: ReactNode }) => {
47 | const [featureFlags, setFeatureFlags] = useState();
48 | const [isFFLoaded, setIsFFLoaded] = useState(false);
49 | const [earlyAccessFeatures, setEarlyAccessFeatures] =
50 | useState();
51 | const posthog = usePostHog();
52 | const activeFeatureFlags = useActiveFeatureFlags();
53 |
54 | useEffect(() => {
55 | posthog.getEarlyAccessFeatures((features) => {
56 | setEarlyAccessFeatures(features);
57 | });
58 | }, []);
59 |
60 | useEffect(() => {
61 | posthog.onFeatureFlags(() => setIsFFLoaded(true));
62 | });
63 |
64 | useEffect(() => {
65 | let enabledFlags: FeatureFlags = {};
66 | activeFeatureFlags?.forEach((flagName) => {
67 | const isEnabled = posthog.isFeatureEnabled(flagName);
68 | if (isEnabled) {
69 | const payload = posthog.getFeatureFlagPayload(flagName);
70 | if (payload) {
71 | enabledFlags[flagName] = payload;
72 | } else {
73 | enabledFlags[flagName] = true;
74 | }
75 | }
76 | });
77 | setFeatureFlags(enabledFlags);
78 | }, [activeFeatureFlags, posthog]);
79 |
80 | return (
81 |
84 | {children}
85 |
86 | );
87 | };
88 |
--------------------------------------------------------------------------------
/src/features/sentry.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/react';
2 |
3 | Sentry.init({
4 | environment: process.env.NODE_ENV,
5 | dsn: process.env.SENTRY_DSN,
6 | integrations: [
7 | Sentry.browserTracingIntegration(),
8 | Sentry.browserProfilingIntegration(),
9 | Sentry.replayIntegration(),
10 | ],
11 | // Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled
12 | tracePropagationTargets: [
13 | 'localhost',
14 | 'chrome-extension://lpfcbjknijpeeillifnkikgncikgfhdo',
15 | ],
16 | // .5%
17 | tracesSampleRate: 0.05,
18 | profilesSampleRate: 0.05,
19 | // Since profilesSampleRate is relative to tracesSampleRate,
20 | // the final profiling rate can be computed as tracesSampleRate * profilesSampleRate
21 | // A tracesSampleRate of 0.05 and profilesSampleRate of 0.05 results in 2.5% of
22 | // transactions being profiled (0.05*0.05=0.0025)
23 |
24 | // Capture Replay for 0.05% of all sessions,
25 | replaysSessionSampleRate: 0.005,
26 | // ...plus for 100% of sessions with an error
27 | replaysOnErrorSampleRate: 1.0,
28 | });
29 |
--------------------------------------------------------------------------------
/src/features/settings/legal/LegalSettings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | Link,
6 | Popover,
7 | PopoverArrow,
8 | PopoverBody,
9 | PopoverContent,
10 | PopoverTrigger,
11 | Spacer,
12 | Switch,
13 | Text,
14 | Tooltip,
15 | } from '@chakra-ui/react';
16 | import React, { useRef } from 'react';
17 | import { ChevronRightIcon, InfoOutlineIcon } from '@chakra-ui/icons';
18 | import PrivacyPolicy from '../../../ui/app/components/privacyPolicy';
19 | import TermsOfUse from '../../../ui/app/components/termsOfUse';
20 | import { useAnalyticsContext } from '../../analytics/provider';
21 |
22 | export const LegalSettings = () => {
23 | const [analytics, setAnalyticsConsent] = useAnalyticsContext();
24 | const termsRef = useRef<{ openModal: () => void }>();
25 | const privacyPolicyRef = useRef<{ openModal: () => void }>();
26 | return (
27 | <>
28 |
29 |
30 | Legal
31 |
32 |
33 |
34 |
35 | Analytics
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
56 | We collect anonymous information from your browser extension
57 | to help us improve the quality and performance of Nami. This
58 | may include data about how you use our service, your
59 | preferences and information about your system. Read more
60 | window.open('https://namiwallet.io')}
62 | textDecoration="underline"
63 | >
64 | here
65 |
66 | .
67 |
68 |
69 |
70 |
71 |
72 |
73 | setAnalyticsConsent(!analytics.consent)}
76 | />
77 |
78 |
79 | }
83 | variant="ghost"
84 | onClick={() => termsRef.current?.openModal()}
85 | >
86 | Terms of Use
87 |
88 |
89 | }
93 | variant="ghost"
94 | onClick={() => privacyPolicyRef.current?.openModal()}
95 | >
96 | Privacy Policy
97 |
98 |
99 |
100 | >
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/config.ts:
--------------------------------------------------------------------------------
1 | export const CURRENT_VERSION = 1;
2 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import {
3 | getAcceptedLegalDocsVersion,
4 | setAcceptedLegalDocsVersion,
5 | } from './services';
6 | import { CURRENT_VERSION } from './config';
7 |
8 | export const useAcceptDocs = () => {
9 | const [accepted, setAccepted] = useState(false);
10 |
11 | return {
12 | accepted,
13 | setAccepted: useCallback(
14 | (accepted: boolean) => {
15 | setAccepted(accepted);
16 | setAcceptedLegalDocsVersion(accepted ? CURRENT_VERSION : undefined);
17 | },
18 | [setAccepted]
19 | ),
20 | };
21 | };
22 |
23 | export const useShowUpdatePrompt = () => {
24 | const [shouldShowUpdatePrompt, setShouldShowUpdatePrompt] = useState(
25 | undefined
26 | );
27 |
28 | useEffect(() => {
29 | const init = async () => {
30 | const acceptedVersion = await getAcceptedLegalDocsVersion();
31 |
32 | if (acceptedVersion) {
33 | setShouldShowUpdatePrompt(acceptedVersion < CURRENT_VERSION);
34 | } else {
35 | setShouldShowUpdatePrompt(true);
36 | }
37 | };
38 |
39 | init();
40 | }, []);
41 |
42 | return {
43 | shouldShowUpdatePrompt,
44 | hideUpdatePrompt: useCallback(() => {
45 | setShouldShowUpdatePrompt(false);
46 | }, [setShouldShowUpdatePrompt]),
47 | };
48 | };
49 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/index.ts:
--------------------------------------------------------------------------------
1 | export { TermsAndPrivacyProvider } from './provider';
2 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/provider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { TermsAndPrivacyModal } from './ui/TermsAndPrivacyModal';
3 | import { useShowUpdatePrompt } from './hooks';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | }
8 |
9 | export const TermsAndPrivacyProvider = ({ children }: Props) => {
10 | const { shouldShowUpdatePrompt, hideUpdatePrompt } = useShowUpdatePrompt();
11 |
12 | if (shouldShowUpdatePrompt === undefined) {
13 | return;
14 | }
15 |
16 | return shouldShowUpdatePrompt ? (
17 |
18 | ) : (
19 | children
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/services.ts:
--------------------------------------------------------------------------------
1 | import { getStorage, removeStorage, setStorage } from '../../api/extension';
2 | import { STORAGE } from '../../config/config';
3 |
4 | export const getAcceptedLegalDocsVersion = async (): Promise<
5 | number | undefined
6 | > => {
7 | const version = await getStorage(STORAGE.acceptedLegalDocsVersion);
8 |
9 | return version ? Number(version) : undefined;
10 | };
11 |
12 | export const setAcceptedLegalDocsVersion = (
13 | version: number | undefined
14 | ): Promise => {
15 | return version
16 | ? setStorage({ [STORAGE.acceptedLegalDocsVersion]: version })
17 | : removeStorage(STORAGE.acceptedLegalDocsVersion);
18 | };
19 |
--------------------------------------------------------------------------------
/src/features/terms-and-privacy/ui/TermsAndPrivacyModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@chakra-ui/button';
2 | import {
3 | Modal,
4 | ModalBody,
5 | ModalContent,
6 | ModalFooter,
7 | ModalHeader,
8 | ModalOverlay,
9 | } from '@chakra-ui/modal';
10 | import { Checkbox } from '@chakra-ui/checkbox';
11 | import { Link, Text, Box } from '@chakra-ui/layout';
12 | import React, { useRef } from 'react';
13 | import PrivacyPolicy from '../../../ui/app/components/privacyPolicy';
14 | import TermsOfUse from '../../../ui/app/components/termsOfUse';
15 | import { useAcceptDocs } from '../hooks';
16 |
17 | interface Props {
18 | onContinue: () => void;
19 | }
20 |
21 | export const TermsAndPrivacyModal = ({ onContinue }: Props) => {
22 | const termsRef = useRef<{ openModal: () => void }>();
23 | const privacyPolicyRef = useRef<{ openModal: () => void }>();
24 | const { accepted, setAccepted } = useAcceptDocs();
25 |
26 | return (
27 | <>
28 | void 0}
33 | blockScrollOnMount={false}
34 | >
35 |
36 |
37 |
38 | Terms of use and Privacy Policy
39 |
40 |
41 |
42 |
43 | The terms of use and privacy policy have been updated.
44 |
45 |
46 | setAccepted(e.target.checked)} />
47 |
48 |
49 | I read and accepted the{' '}
50 | termsRef.current?.openModal()}
52 | textDecoration="underline"
53 | >
54 | Terms of use
55 |
56 | and
57 | privacyPolicyRef.current?.openModal()}
59 | textDecoration="underline"
60 | >
61 | Privacy Policy
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
76 |
77 |
78 |
79 |
80 |
81 | >
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Nami",
4 | "version": "3.9.6",
5 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoy3Y6s5Q72zsN6+sBJL8EBCyGL/USyXXjTJGIIV/3xfcrkoZ53I+o9B0Euo8yz2GXnBjm3+ZIC1YwN5ZPC/uQpPHyq8GLksu3VvDdupKMNIWXvMByLjF0pyD4YNdNueU4r3fWPPBsbvG98cmNbeZ1NmwV2Byad4PQtUMx76jSk6KHcp3qNTMEo8utY49EIPaC9wr/Fg9gaqI83SFKNoe2FeKrb1HXaTT366myKLupHXm7hoD7U87a2itNfR7kohBoO6RBlrTNmYuq65wYO1eY5h/4tCkZrztjTKjfMWbuVc0kjL8VFv2IR1ETV8dMDgaBKxWGpccaDzF2GJfYNjK2QIDAQAB",
6 | "description": "Maintained by IOG",
7 | "background": { "service_worker": "background.bundle.js" },
8 | "action": {
9 | "default_popup": "mainPopup.html",
10 | "default_icon": "icon-34.png"
11 | },
12 | "icons": {
13 | "128": "icon-128.png"
14 | },
15 | "content_scripts": [
16 | {
17 | "matches": ["http://*/*", "https://*/*", ""],
18 | "js": ["contentScript.bundle.js"],
19 | "run_at": "document_start"
20 | },
21 | {
22 | "matches": ["*://connect.trezor.io/*/popup.html*"],
23 | "js": ["trezorContentScript.bundle.js"],
24 | "all_frames": true
25 | }
26 | ],
27 | "web_accessible_resources": [
28 | {
29 | "resources": [
30 | "icon-128.png",
31 | "icon-34.png",
32 | "injected.bundle.js",
33 | "internalPopup.html"
34 | ],
35 | "matches": ["http://*/*", "https://*/*", "file://*/*"]
36 | }
37 | ],
38 | "content_security_policy": {
39 | "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; frame-src https://connect.trezor.io/;"
40 | },
41 | "permissions": ["storage", "unlimitedStorage", "favicon"]
42 | }
43 |
--------------------------------------------------------------------------------
/src/migrations/versions/1.0.0.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '1.0.0',
3 | up: (pwd) => {
4 | // Upgrade logic here
5 | },
6 | down: (pwd) => {
7 | // Downgrade logic here
8 | },
9 | info: [
10 | {
11 | title: 'Feature #1',
12 | detail: 'This new feature allow the user to see the Feature #1',
13 | },
14 | { title: 'Bug fix: Feature #2', detail: null },
15 | {
16 | title: 'Feature #3',
17 | detail: 'Allow to send feature #3 assets to multiple addresses',
18 | },
19 | { title: 'Feature #2', detail: null },
20 | {
21 | title: 'Feature #3 - Suspendisse eget nibh',
22 | detail:
23 | 'Donec consectetur enim in urna consectetur fringilla rhoncus mattis quam.',
24 | },
25 | ],
26 | pwdRequired: true,
27 | };
28 |
29 | export default migration;
30 |
--------------------------------------------------------------------------------
/src/migrations/versions/1.1.5.js:
--------------------------------------------------------------------------------
1 | import { NETWORK_ID, STORAGE } from '../../config/config';
2 | import {
3 | decryptWithPassword,
4 | getStorage,
5 | setStorage,
6 | } from '../../api/extension/index';
7 | import { initTx } from '../../api/extension/wallet';
8 | import Loader from '../../api/loader';
9 | import { assetsToValue } from '../../api/util';
10 |
11 | const harden = (num) => {
12 | return 0x80000000 + num;
13 | };
14 |
15 | const migration = {
16 | version: '1.1.5',
17 | up: async (pwd) => {
18 | await Loader.load();
19 | const protocolParameters = await initTx();
20 | const networks = Object.keys(NETWORK_ID);
21 | const storage = await getStorage(STORAGE.accounts);
22 | const accounts = Object.keys(storage);
23 |
24 | // add minAda
25 | for (let i = 0; i < accounts.length; i++) {
26 | for (let j = 0; j < networks.length; j++) {
27 | if (storage[accounts[i]][networks[j]]) {
28 | const currentAccountNetwork = storage[accounts[i]][networks[j]];
29 | let assets = currentAccountNetwork.assets;
30 | if (assets.length > 0) {
31 | const amount = await assetsToValue(assets);
32 | currentAccountNetwork.minAda = Loader.Cardano.min_ada_required(
33 | amount,
34 | BigInt(protocolParameters.minUtxo)
35 | ).toString();
36 | } else {
37 | currentAccountNetwork.minAda = 0;
38 | }
39 | }
40 | }
41 | }
42 |
43 | //add public key
44 | const encryptedKey = await getStorage(STORAGE.encryptedKey);
45 | const decryptedKey = await decryptWithPassword(pwd, encryptedKey);
46 | let privateKey = Loader.Cardano.Bip32PrivateKey.from_raw_bytes(
47 | Buffer.from(decryptedKey, 'hex')
48 | );
49 |
50 | Object.keys(storage).forEach(async (index) => {
51 | const account = storage[index];
52 | account.publicKey = Buffer.from(
53 | privateKey
54 | .derive(harden(1852))
55 | .derive(harden(1815))
56 | .derive(harden(parseInt(account.index)))
57 | .to_public()
58 | .as_bytes()
59 | ).toString('hex');
60 | });
61 |
62 | privateKey.free();
63 | privateKey = null;
64 |
65 | await setStorage({ [STORAGE.accounts]: storage });
66 | },
67 | down: async (pwd) => {
68 | const networks = Object.keys(NETWORK_ID);
69 | let storage = await getStorage(STORAGE.accounts);
70 | const accounts = Object.keys(storage);
71 |
72 | for (let i = 0; i < accounts.length; i++) {
73 | for (let j = 0; j < networks.length; j++) {
74 | if (
75 | storage[accounts[i]][networks[j]] &&
76 | storage[accounts[i]][networks[j]].minAda
77 | ) {
78 | delete storage[accounts[i]][networks[j]].minAda;
79 | }
80 | }
81 | }
82 | await setStorage({ [STORAGE.accounts]: storage });
83 | },
84 | info: [
85 | {
86 | title: 'Show only spendable Ada',
87 | detail:
88 | 'In previous version, the wallet was showing the complete Ada balance. This lead the user to think that all Ada were available for spending. Native Assets (ie: NFT) require a small amount of Ada to be locked with them at all time. Locked Ada are now hidden.',
89 | },
90 | {
91 | title: 'Bug fix: Inconsistent Balance',
92 | detail:
93 | 'Balance was reported many times to be out of sync. The wallet now tries to fetch the balance until it succeeds.',
94 | },
95 | {
96 | title: 'Bug fix: CoinSelection',
97 | detail:
98 | 'The underlying algorithm managing UTxO set was behaving inconsistently, making it impossible to send out certain asset amounts.',
99 | },
100 | ],
101 | pwdRequired: true,
102 | };
103 |
104 | export default migration;
105 |
--------------------------------------------------------------------------------
/src/migrations/versions/1.1.7.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '1.1.7',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'Bug fix: Inconsistent Balance',
8 | detail:
9 | 'Balance was reported many times to be out of sync, even with the latest fix. The logic to decide whether an update is needed or not has been reworked.',
10 | },
11 | {
12 | title: 'Bug fix: Coin Selection',
13 | detail:
14 | 'The Coin Selection algorithm failed calculating properly requirements for some UTxO set, thus making impossible to send amount within a certain range.',
15 | },
16 | {
17 | title: 'Bug fix: Blank Screen',
18 | detail:
19 | 'A blank screen was reported a few times. The causing issue was being able to press the Confirm button multiple times.',
20 | },
21 | ],
22 | pwdRequired: false,
23 | };
24 |
25 | export default migration;
26 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.0.0.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '2.0.0',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'Added collateral',
8 | detail:
9 | 'A new option "Collateral" was added to the menu. You can enable it in order to interact with smart contracts.',
10 | },
11 | {
12 | title: 'Bug fix: ADA input amount',
13 | detail:
14 | 'Input box is highlighted in red correctly again, when amount is below minimum or above account balance.',
15 | },
16 | {
17 | title: 'Bug fix: Coin selection',
18 | detail: 'Fixed bug, where small amounts could not be sent.',
19 | },
20 | ],
21 | pwdRequired: false,
22 | };
23 |
24 | export default migration;
25 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.1.0.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '2.1.0',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'Bug fix: Collateral',
8 | detail:
9 | 'Fixed issue with setting collateral amount, where user had to try it multiple times.',
10 | },
11 | {
12 | title: 'Enhancement: Mempool checking',
13 | detail:
14 | 'Checking now if the mempool is full and letting the user know if he needs to resubmit the transaction.',
15 | },
16 | {
17 | title: 'Enhancement: Coin selection',
18 | detail: 'Allowing now to send a lot of small UTxOs.',
19 | },
20 | {
21 | title: 'Enhancement: Addresses',
22 | detail:
23 | 'Preventing users from sending to a contract address. Improved checking, if the user interacts with a contract under the sign screen.',
24 | },
25 | ],
26 | pwdRequired: false,
27 | };
28 |
29 | export default migration;
30 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.2.0.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '2.2.0',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'HW support',
8 | detail:
9 | 'Nami has now full hardware wallet support for Ledger and Trezor.',
10 | },
11 | ],
12 | pwdRequired: false,
13 | };
14 |
15 | export default migration;
16 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.3.0.js:
--------------------------------------------------------------------------------
1 | import { getStorage, setStorage } from '../../api/extension';
2 | import { NETWORK_ID, STORAGE } from '../../config/config';
3 |
4 | const migration = {
5 | version: '2.3.0',
6 | up: async () => {
7 | const accounts = await getStorage(STORAGE.accounts);
8 | Object.keys(accounts).forEach((accountIndex) => {
9 | accounts[accountIndex][NETWORK_ID.mainnet].forceUpdate = true;
10 | accounts[accountIndex][NETWORK_ID.testnet].forceUpdate = true;
11 | });
12 | await setStorage({ [STORAGE.accounts]: accounts });
13 | },
14 | down: async () => {},
15 | info: [
16 | {
17 | title: 'Seperated assets view',
18 | detail: 'Nami now separates collectibles/NFTs and normal assets (FTs)',
19 | },
20 | {
21 | title: 'Improved Coin selection',
22 | },
23 | {
24 | title: 'Small bug fixing',
25 | },
26 | {
27 | title: 'Customizable avatar for account',
28 | },
29 | ],
30 | pwdRequired: false,
31 | };
32 |
33 | export default migration;
34 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.3.2.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '2.3.2',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'Deregistration of stake keys',
8 | },
9 | {
10 | title: 'Improved Inputs',
11 | detail: 'Number inputs are automatically formatted now.',
12 | },
13 | ],
14 | pwdRequired: false,
15 | };
16 |
17 | export default migration;
18 |
--------------------------------------------------------------------------------
/src/migrations/versions/2.3.3.js:
--------------------------------------------------------------------------------
1 | import { NETWORK_ID, STORAGE } from '../../config/config';
2 | import { getStorage, setStorage } from '../../api/extension/index';
3 | import Loader from '../../api/loader';
4 |
5 | const migration = {
6 | version: '2.3.3',
7 | up: async (pwd) => {
8 | await Loader.load();
9 | const networks = Object.keys(NETWORK_ID);
10 | const storage = await getStorage(STORAGE.accounts);
11 | const accounts = Object.keys(storage);
12 |
13 | for (let i = 0; i < accounts.length; i++) {
14 | for (let j = 0; j < networks.length; j++) {
15 | if (storage[accounts[i]][networks[j]]) {
16 | const currentAccount = storage[accounts[i]];
17 | const network = networks[j];
18 | const currentAccountNetwork = currentAccount[network];
19 | const paymentKeyHash = Loader.Cardano.Ed25519KeyHash.from_raw_bytes(
20 | Buffer.from(currentAccount.paymentKeyHash, 'hex')
21 | );
22 | const stakeKeyHash = Loader.Cardano.Ed25519KeyHash.from_raw_bytes(
23 | Buffer.from(currentAccount.stakeKeyHash, 'hex')
24 | );
25 | const paymentAddr = Loader.Cardano.BaseAddress.new(
26 | network === NETWORK_ID.mainnet
27 | ? Loader.Cardano.NetworkInfo.mainnet().network_id()
28 | : Loader.Cardano.NetworkInfo.testnet().network_id(),
29 | Loader.Cardano.Credential.new_pub_key(paymentKeyHash),
30 | Loader.Cardano.Credential.new_pub_key(stakeKeyHash)
31 | )
32 | .to_address()
33 | .to_bech32();
34 |
35 | const rewardAddr = Loader.Cardano.RewardAddress.new(
36 | network === NETWORK_ID.mainnet
37 | ? Loader.Cardano.NetworkInfo.mainnet().network_id()
38 | : Loader.Cardano.NetworkInfo.testnet().network_id(),
39 | Loader.Cardano.Credential.new_pub_key(stakeKeyHash)
40 | )
41 | .to_address()
42 | .to_bech32();
43 |
44 | currentAccountNetwork.paymentAddr = paymentAddr;
45 | currentAccountNetwork.rewardAddr = rewardAddr;
46 | }
47 | }
48 | }
49 |
50 | await setStorage({ [STORAGE.accounts]: storage });
51 | },
52 | down: async (pwd) => {
53 | const networks = Object.keys(NETWORK_ID);
54 | const storage = await getStorage(STORAGE.accounts);
55 | const accounts = Object.keys(storage);
56 |
57 | for (let i = 0; i < accounts.length; i++) {
58 | for (let j = 0; j < networks.length; j++) {
59 | if (storage[accounts[i]][networks[j]]) {
60 | const currentAccount = storage[accounts[i]];
61 | const network = networks[j];
62 | const currentAccountNetwork = currentAccount[network];
63 |
64 | delete currentAccountNetwork.paymentAddr;
65 | delete currentAccountNetwork.rewardAddr;
66 | }
67 | }
68 | }
69 |
70 | await setStorage({ [STORAGE.accounts]: storage });
71 | },
72 | info: [
73 | { title: 'Improved Coin selection' },
74 | { title: 'Collateral bug fixing' },
75 | ],
76 | pwdRequired: false,
77 | };
78 |
79 | export default migration;
80 |
--------------------------------------------------------------------------------
/src/migrations/versions/3.0.0.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '3.0.0',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: '$handle support',
8 | detail:
9 | '$handle address resolution is now supported under the send screen.',
10 | },
11 | {
12 | title: 'NFT as avatar',
13 | detail: 'You can now use your NFT/collectible as avatar in each account.',
14 | },
15 | {
16 | title: 'Wallet creation in separate tab',
17 | detail:
18 | 'The wallet creation and importing section is now under a seperate tab.',
19 | },
20 | ],
21 | pwdRequired: false,
22 | };
23 |
24 | export default migration;
25 |
--------------------------------------------------------------------------------
/src/migrations/versions/3.0.2.js:
--------------------------------------------------------------------------------
1 | const migration = {
2 | version: '3.0.2',
3 | up: async () => {},
4 | down: async () => {},
5 | info: [
6 | {
7 | title: 'Sending screen reworked',
8 | detail:
9 | 'Little UX and layout redesign. There is now a confirmation screen when clicking on send. An optional message can also be appended now to the transaction (CIP-0020).',
10 | },
11 | {
12 | title: 'Milkomeda support',
13 | detail:
14 | 'Assets can now be moved to the Milkomeda sidechain. This functionality is available under the send screen (only testnet for now).',
15 | },
16 | {
17 | title: 'ADA as asset',
18 | detail: 'ADA is shown now under assets on the main screen.',
19 | },
20 | ],
21 | pwdRequired: false,
22 | };
23 |
24 | export default migration;
25 |
--------------------------------------------------------------------------------
/src/migrations/versions/3.3.0.js:
--------------------------------------------------------------------------------
1 | import { NETWORK_ID, STORAGE } from '../../config/config';
2 | import { getStorage, setStorage } from '../../api/extension/index';
3 | import Loader from '../../api/loader';
4 | import { initTx } from '../../api/extension/wallet';
5 | import { assetsToValue } from '../../api/util';
6 |
7 | const migration = {
8 | version: '3.3.0',
9 | up: async (pwd) => {
10 | const networkDefault = {
11 | lovelace: null,
12 | minAda: 0,
13 | assets: [],
14 | history: { confirmed: [], details: {} },
15 | };
16 |
17 | await Loader.load();
18 | const storage = await getStorage(STORAGE.accounts);
19 | const accounts = Object.keys(storage);
20 | const networks = Object.keys(NETWORK_ID);
21 | const protocolParameters = await initTx();
22 |
23 | for (let i = 0; i < accounts.length; i++) {
24 | const currentAccount = storage[accounts[i]];
25 | const paymentKeyHash = Loader.Cardano.Ed25519KeyHash.from_raw_bytes(
26 | Buffer.from(currentAccount.paymentKeyHash, 'hex')
27 | );
28 | const paymentKeyHashBech32 = paymentKeyHash.to_bech32('addr_vkh');
29 | currentAccount.paymentKeyHashBech32 = paymentKeyHashBech32;
30 |
31 | currentAccount[NETWORK_ID.preview] = {
32 | ...networkDefault,
33 | paymentAddr: currentAccount[NETWORK_ID.testnet].paymentAddr,
34 | rewardAddr: currentAccount[NETWORK_ID.testnet].rewardAddr,
35 | };
36 |
37 | currentAccount[NETWORK_ID.preprod] = {
38 | ...networkDefault,
39 | paymentAddr: currentAccount[NETWORK_ID.testnet].paymentAddr,
40 | rewardAddr: currentAccount[NETWORK_ID.testnet].rewardAddr,
41 | };
42 | }
43 |
44 | // add minAda
45 | for (let i = 0; i < accounts.length; i++) {
46 | for (let j = 0; j < networks.length; j++) {
47 | if (storage[accounts[i]][networks[j]]) {
48 | const currentAccountNetwork = storage[accounts[i]][networks[j]];
49 | let assets = currentAccountNetwork.assets;
50 | if (assets.length > 0) {
51 | const amount = await assetsToValue(assets);
52 | const checkOutput = Loader.Cardano.TransactionOutput.new(
53 | Loader.Cardano.Address.from_bech32(
54 | currentAccountNetwork.paymentAddr
55 | ),
56 | amount
57 | );
58 | currentAccountNetwork.minAda = Loader.Cardano.min_ada_required(
59 | checkOutput,
60 | BigInt(
61 | // protocolParameters.coinsPerUtxoWord
62 | (4310).toString() // We hardcode this, since we don't know if Blockfrost switches PP quickly during the epoch transition
63 | )
64 | ).toString();
65 | } else {
66 | currentAccountNetwork.minAda = 0;
67 | }
68 | }
69 | }
70 | }
71 |
72 | await setStorage({ [STORAGE.accounts]: storage });
73 | },
74 | down: async (pwd) => {
75 | const storage = await getStorage(STORAGE.accounts);
76 | const accounts = Object.keys(storage);
77 |
78 | for (let i = 0; i < accounts.length; i++) {
79 | const currentAccount = storage[accounts[i]];
80 | delete currentAccount.paymentKeyHashBech32;
81 |
82 | delete currentAccount[NETWORK_ID.preview];
83 | delete currentAccount[NETWORK_ID.preprod];
84 | }
85 |
86 | await setStorage({ [STORAGE.accounts]: storage });
87 | },
88 | info: [
89 | { title: 'Support: Vasil era' },
90 | {
91 | title: 'Support: Mangled addresses',
92 | detail:
93 | 'Nami keeps track now of all addresses with the same payment credential.',
94 | },
95 | { title: 'Support: Preview and Preprod network' },
96 | { title: 'Bug fixing' },
97 | ],
98 | pwdRequired: false,
99 | };
100 |
101 | export default migration;
102 |
--------------------------------------------------------------------------------
/src/pages/Content/index.js:
--------------------------------------------------------------------------------
1 | import { Messaging } from '../../api/messaging';
2 |
3 | const injectScript = () => {
4 | const script = document.createElement('script');
5 | script.async = false;
6 | script.src = chrome.runtime.getURL('injected.bundle.js');
7 | script.onload = function () {
8 | this.remove();
9 | };
10 | (document.head || document.documentElement).appendChild(script);
11 | };
12 |
13 | async function shouldInject() {
14 | // do not inject since the migration is not dismissible anymore
15 | return false
16 | }
17 |
18 | if (await shouldInject()) {
19 | injectScript();
20 | Messaging.createProxyController();
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/Content/injected.js:
--------------------------------------------------------------------------------
1 | import {
2 | enable,
3 | getAddress,
4 | getBalance,
5 | getCollateral,
6 | getNetworkId,
7 | getRewardAddress,
8 | getUtxos,
9 | isEnabled,
10 | off,
11 | on,
12 | signData,
13 | signDataCIP30,
14 | signTx,
15 | submitTx,
16 | } from '../../api/webpage';
17 | import { EVENT } from '../../config/config';
18 |
19 | //dApp connector API follows https://github.com/cardano-foundation/CIPs/pull/88
20 |
21 | const logDeprecated = () => {
22 | console.warn(
23 | 'This Nami API implementation is deprecated soon. Please follow the API under the window.cardano.nami namespace. For more information check out CIP-30: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0030'
24 | );
25 | return true;
26 | };
27 |
28 | //Initial version (deprecated soon)
29 | window.cardano = {
30 | ...(window.cardano || {}),
31 | enable: () => logDeprecated() && enable(),
32 | isEnabled: () => logDeprecated() && isEnabled(),
33 | getBalance: () => logDeprecated() && getBalance(),
34 | signData: (address, payload) => logDeprecated() && signData(address, payload),
35 | signTx: (tx, partialSign) => logDeprecated() && signTx(tx, partialSign),
36 | submitTx: (tx) => logDeprecated() && submitTx(tx),
37 | getUtxos: (amount, paginate) => logDeprecated() && getUtxos(amount, paginate),
38 | getCollateral: () => logDeprecated() && getCollateral(),
39 | getUsedAddresses: async () => logDeprecated() && [await getAddress()],
40 | getUnusedAddresses: async () => logDeprecated() && [],
41 | getChangeAddress: () => logDeprecated() && getAddress(),
42 | getRewardAddress: () => logDeprecated() && getRewardAddress(),
43 | getNetworkId: () => logDeprecated() && getNetworkId(),
44 | onAccountChange: (callback) =>
45 | logDeprecated() && on(EVENT.accountChange, callback),
46 | onNetworkChange: (callback) =>
47 | logDeprecated() && on(EVENT.networkChange, callback),
48 | off,
49 | _events: {},
50 | };
51 |
52 | // // CIP-30
53 |
54 | window.cardano = {
55 | ...(window.cardano || {}),
56 | nami: {
57 | enable: async () => {
58 | if (await enable()) {
59 | return {
60 | getBalance: () => getBalance(),
61 | signData: (address, payload) => signDataCIP30(address, payload),
62 | signTx: (tx, partialSign) => signTx(tx, partialSign),
63 | submitTx: (tx) => submitTx(tx),
64 | getUtxos: (amount, paginate) => getUtxos(amount, paginate),
65 | getUsedAddresses: async () => [await getAddress()],
66 | getUnusedAddresses: async () => [],
67 | getChangeAddress: () => getAddress(),
68 | getRewardAddresses: async () => [await getRewardAddress()],
69 | getNetworkId: () => getNetworkId(),
70 | experimental: {
71 | on: (eventName, callback) => on(eventName, callback),
72 | off: (eventName, callback) => off(eventName, callback),
73 | getCollateral: () => getCollateral(),
74 | },
75 | };
76 | }
77 | },
78 | isEnabled: () => isEnabled(),
79 | apiVersion: '0.1.0',
80 | name: 'Nami',
81 | icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 486.17 499.86'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23349ea3;%7D%3C/style%3E%3C/defs%3E%3Cg id='Layer_2' data-name='Layer 2'%3E%3Cg id='Layer_1-2' data-name='Layer 1'%3E%3Cpath id='path16' class='cls-1' d='M73.87,52.15,62.11,40.07A23.93,23.93,0,0,1,41.9,61.87L54,73.09,486.17,476ZM102.4,168.93V409.47a23.76,23.76,0,0,1,32.13-2.14V245.94L395,499.86h44.87Zm303.36-55.58a23.84,23.84,0,0,1-16.64-6.68v162.8L133.46,15.57H84L421.28,345.79V107.6A23.72,23.72,0,0,1,405.76,113.35Z'/%3E%3Cpath id='path18' class='cls-1' d='M38.27,0A38.25,38.25,0,1,0,76.49,38.27v0A38.28,38.28,0,0,0,38.27,0ZM41.9,61.8a22,22,0,0,1-3.63.28A23.94,23.94,0,1,1,62.18,38.13V40A23.94,23.94,0,0,1,41.9,61.8Z'/%3E%3Cpath id='path20' class='cls-1' d='M405.76,51.2a38.24,38.24,0,0,0,0,76.46,37.57,37.57,0,0,0,15.52-3.3A38.22,38.22,0,0,0,405.76,51.2Zm15.52,56.4a23.91,23.91,0,1,1,8.39-18.18A23.91,23.91,0,0,1,421.28,107.6Z'/%3E%3Cpath id='path22' class='cls-1' d='M134.58,390.81A38.25,38.25,0,1,0,157.92,426a38.24,38.24,0,0,0-23.34-35.22Zm-15,59.13A23.91,23.91,0,1,1,143.54,426a23.9,23.9,0,0,1-23.94,23.91Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E",
82 | _events: {},
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/src/pages/Content/trezorContentScript.js:
--------------------------------------------------------------------------------
1 | // Communicate from background script to popup
2 | let port = chrome.runtime.connect({ name: 'trezor-connect' });
3 | port.onMessage.addListener((message) => {
4 | window.postMessage(message, window.location.origin);
5 | });
6 | port.onDisconnect.addListener(() => {
7 | // eslint-disable-next-line unicorn/no-null
8 | port = null;
9 | });
10 |
11 | // communicate from popup to background script
12 | window.addEventListener('message', (event) => {
13 | if (port && event.source === window && event.data) {
14 | port.postMessage({ data: event.data });
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/pages/Popup/internalPopup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nami
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/pages/Popup/mainPopup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nami
6 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/pages/Tab/createWalletTab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nami - Create Wallet
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/Tab/hwTab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nami - Connect HW
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/Tab/trezorTx.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nami - Sign Trezor transaction
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/test/unit/api/extension/index.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | getStorage,
3 | encryptWithPassword,
4 | decryptWithPassword,
5 | createWallet,
6 | switchAccount,
7 | createAccount,
8 | setWhitelisted,
9 | getWhitelisted,
10 | getNetwork,
11 | setNetwork,
12 | getCurrentAccount,
13 | } from '../../../../api/extension';
14 | import Loader from '../../../../api/loader';
15 | import { ERROR, NODE, STORAGE } from '../../../../config/config';
16 |
17 | beforeAll(async () => {
18 | const seed =
19 | 'midnight draft salt dirt woman tragic cause immense dad later jaguar finger nerve nerve sign job erase citizen cube neglect token bracket orient narrow';
20 | const name = 'Wallet 1';
21 | const password = 'password123';
22 | await Loader.load();
23 | await createWallet(name, seed, password);
24 | });
25 |
26 | test('storage initialized correctly', async () => {
27 | const store = await getStorage();
28 | expect(store).toHaveProperty(STORAGE.accounts);
29 | expect(store).toHaveProperty(STORAGE.currency);
30 | expect(store).toHaveProperty(STORAGE.encryptedKey);
31 | expect(store).toHaveProperty(STORAGE.currentAccount);
32 | expect(store).toHaveProperty(STORAGE.network);
33 | expect(Object.keys(store).length).toBe(5);
34 | });
35 |
36 | test('should have whitelist', async () => {
37 | await setWhitelisted('https://namiwallet.io');
38 | const store = await getStorage();
39 | expect(store).toHaveProperty(STORAGE.whitelisted);
40 | const whitelisted = await getWhitelisted();
41 | expect(whitelisted).toEqual(['https://namiwallet.io']);
42 | expect(Object.keys(store).length).toBe(6);
43 | });
44 |
45 | test('account structure is correct', async () => {
46 | const store = await getStorage();
47 | const account = store.accounts[store.currentAccount];
48 | expect(account).toHaveProperty('avatar');
49 | expect(account).toHaveProperty('name');
50 | expect(account).toHaveProperty('index');
51 | expect(account).toHaveProperty('paymentKeyHash');
52 | expect(account).toHaveProperty('stakeKeyHash');
53 | expect(account).toHaveProperty('mainnet');
54 | expect(account).toHaveProperty('testnet');
55 | expect(account.mainnet).toHaveProperty('lovelace');
56 | expect(account.mainnet).toHaveProperty('assets');
57 | expect(account.mainnet).toHaveProperty('history');
58 | expect(account.mainnet.history).toHaveProperty('confirmed');
59 | expect(account.mainnet.history).toHaveProperty('details');
60 | });
61 |
62 | test('current account should be 0', async () => {
63 | const currentAccount = await getStorage('currentAccount');
64 | expect(currentAccount).toBe(0);
65 | });
66 |
67 | test('current account should be 1', async () => {
68 | const name = 'Wallet 2';
69 | const password = 'password123';
70 | await createAccount(name, password);
71 | await switchAccount(1);
72 | const currentAccount = await getStorage('currentAccount');
73 | expect(currentAccount).toBe(1);
74 | });
75 |
76 | test('expect error because of wrong password', async () => {
77 | const name = 'Wallet 3';
78 | const password = 'password456';
79 | expect.assertions(1);
80 | try {
81 | const index = await createAccount(name, password);
82 | await switchAccount(index);
83 | } catch (e) {
84 | expect(e).toBe(ERROR.wrongPassword);
85 | }
86 | });
87 |
88 | test('expect mainnet', async () => {
89 | const network = await getNetwork();
90 | expect(network.id).toBe('mainnet');
91 | });
92 |
93 | test('expect testnet address', async () => {
94 | await setNetwork({ id: 'testnet', node: NODE.testnet });
95 | const account = await getCurrentAccount();
96 | expect(account.paymentAddr).toContain('addr_');
97 | });
98 |
99 | test('should encrypt/decrypt root key correctly', async () => {
100 | const rootKey = Loader.Cardano.Bip32PrivateKey.generate_ed25519_bip32();
101 | const password = 'test123';
102 | const rootKeyBytes = rootKey.to_raw_key().to_raw_bytes();
103 | const encryptedKey = await encryptWithPassword(password, rootKeyBytes);
104 | expect(Buffer.from(rootKeyBytes, 'hex').toString('hex')).not.toBe(
105 | encryptedKey
106 | );
107 | const decryptedKey = await decryptWithPassword(password, encryptedKey);
108 | expect(Buffer.from(rootKeyBytes, 'hex').toString('hex')).toBe(decryptedKey);
109 | });
110 |
--------------------------------------------------------------------------------
/src/test/unit/api/util.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | assetsToValue,
3 | convertMetadataPropToString,
4 | linkToSrc,
5 | valueToAssets,
6 | } from '../../../api/util';
7 | import Loader from '../../../api/loader';
8 | import provider from '../../../config/provider';
9 |
10 | beforeAll(() => {
11 | Loader.load();
12 | });
13 |
14 | test('expect correct assets to value conversion', async () => {
15 | const assetId = '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef0074074657374313233';
16 | const policyId = assetId.slice(0, 56);
17 | const assetName = assetId.slice(56);
18 | const assets = [
19 | { unit: 'lovelace', quantity: '1000000' },
20 | {
21 | unit: assetId,
22 | quantity: '10',
23 | },
24 | ];
25 | const value = await assetsToValue(assets);
26 |
27 | expect(value.coin()).toBe(1000000n);
28 | const multiAsset = value.multi_asset();
29 | expect(multiAsset.keys().len()).toBe(1);
30 | const scriptHash = multiAsset.keys().get(0);
31 | expect(scriptHash.to_hex()).toBe(policyId);
32 | const assetsMap = multiAsset.get_assets(scriptHash);
33 | expect(assetsMap.len()).toBe(1);
34 | const cmlAssetName = assetsMap.keys().get(0);
35 | expect(cmlAssetName.to_hex()).toBe(assetName);
36 | expect(assetsMap.get(cmlAssetName)).toBe(10n)
37 | });
38 |
39 | test('expect correct value to assets conversion', async () => {
40 | const multiAsset = Loader.Cardano.MultiAsset.new();
41 | const value = Loader.Cardano.Value.new(
42 | BigInt('1000000'), multiAsset
43 | );
44 | const assetsSet = Loader.Cardano.MapAssetNameToCoin.new();
45 | assetsSet.insert(
46 | Loader.Cardano.AssetName.from_raw_bytes(Buffer.from('74657374313233', 'hex')),
47 | BigInt('10')
48 | );
49 | multiAsset.insert_assets(
50 | Loader.Cardano.ScriptHash.from_raw_bytes(
51 | Buffer.from(
52 | '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740',
53 | 'hex'
54 | )
55 | ),
56 | assetsSet
57 | );
58 | const assets = await valueToAssets(value);
59 | const testAssets = [
60 | { unit: 'lovelace', quantity: '1000000' },
61 | {
62 | unit: '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef0074074657374313233',
63 | quantity: '10',
64 | },
65 | ];
66 | assets.forEach((asset, i) => {
67 | const testAsset = testAssets[i];
68 | expect(asset.unit).toEqual(testAsset.unit);
69 | expect(asset.quantity).toEqual(testAsset.quantity);
70 | });
71 | });
72 |
73 | describe('test linkToSrc', () => {
74 | test('expect right source from ipfs link', () => {
75 | const testLink = 'ipfs://QmVSameQt9i37hdrLwMSfoAg1aVKrjtBtuDHeQTgyVhUXC';
76 | const testLink1 =
77 | 'ipfs://ipfs/QmVSameQt9i37hdrLwMSfoAg1aVKrjtBtuDHeQTgyVhUXC';
78 | const httpsLink = linkToSrc(testLink);
79 | const httpsLink1 = linkToSrc(testLink1);
80 | expect(httpsLink).toEqual(
81 | provider.api.ipfs + '/' + 'QmVSameQt9i37hdrLwMSfoAg1aVKrjtBtuDHeQTgyVhUXC'
82 | );
83 | expect(httpsLink1).toEqual(
84 | provider.api.ipfs + '/' + 'QmVSameQt9i37hdrLwMSfoAg1aVKrjtBtuDHeQTgyVhUXC'
85 | );
86 | });
87 | test('expect right source from https', () => {
88 | const testLink =
89 | 'https://ipfs.io/ipfs/QmVSameQt9i37hdrLwMSfoAg1aVKrjtBtuDHeQTgyVhUXC';
90 | const httpsLink = linkToSrc(testLink);
91 | expect(httpsLink).toEqual(testLink);
92 | });
93 | test('expect no source', () => {
94 | const testLink = 'invalid src';
95 | const httpsLink = linkToSrc(testLink);
96 | expect(httpsLink).toBe(null);
97 | });
98 | test('expect data uri for png', () => {
99 | const testLink = 'YWJj';
100 | const httpsLink = linkToSrc(testLink, true);
101 | expect(httpsLink).toBe('data:image/png;base64,YWJj');
102 | });
103 | test('expect data uri', () => {
104 | const link =
105 | 'data:image/png;base64,iVB\
106 | ORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEU\
107 | AAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8\
108 | yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAEl\
109 | FTkSuQmCC';
110 | const testLink = linkToSrc(link);
111 | expect(testLink).toEqual(link);
112 | });
113 | test('expect correct array to string conversion', () => {
114 | const normalString = 'metadatateststring';
115 | const array = ['meta', 'data', 'teststring'];
116 |
117 | const convertedString = convertMetadataPropToString(normalString);
118 | const convertedArray = convertMetadataPropToString(array);
119 | expect(convertedArray).toEqual(normalString);
120 | expect(convertedArray).toEqual(convertedString);
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/src/test/unit/api/webpage/eventRegistration.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | import { on, off } from '../../../../api/webpage/eventRegistration';
6 | import { TARGET } from '../../../../config/config';
7 |
8 | describe('webpage/eventRegistring', () => {
9 | const makeEvent = (eventType, detail) =>
10 | new CustomEvent(`${TARGET}${eventType}`, { detail });
11 |
12 | describe('on', () => {
13 | beforeEach(() => {
14 | window.cardano = {
15 | nami: {
16 | _events: {},
17 | },
18 | };
19 | });
20 |
21 | test('invokes the callback when a the target event is triggered', () => {
22 | const eventType = 'mock-event';
23 | const mockPayload = 'mock-payload';
24 | const event = makeEvent(eventType, mockPayload);
25 | const mockFn = jest.fn();
26 |
27 | on(eventType, mockFn);
28 |
29 | window.dispatchEvent(event);
30 |
31 | expect(mockFn).toHaveBeenCalledTimes(1);
32 | expect(mockFn).toHaveBeenCalledWith(mockPayload);
33 | });
34 |
35 | test('does not invoke the callback when a different event is triggered', () => {
36 | const mockFn = jest.fn();
37 |
38 | on('event-A', mockFn);
39 |
40 | const mockPayload = 'mock-payload';
41 | const event = makeEvent('event-B', mockPayload);
42 |
43 | window.dispatchEvent(event);
44 |
45 | expect(mockFn).not.toHaveBeenCalled();
46 | });
47 | });
48 |
49 | describe('of', () => {
50 | const mockEventType = 'mock-event';
51 | const mockCallback = jest.fn();
52 | const mockHandler = jest.fn().mockImplementation(() => mockCallback());
53 |
54 | beforeEach(() => {
55 | jest.resetAllMocks();
56 |
57 | window.cardano = {
58 | nami: {
59 | _events: {
60 | [mockEventType]: [[mockCallback, mockHandler]],
61 | },
62 | },
63 | };
64 | });
65 |
66 | test('clean out matching callbacks fom the given event', () => {
67 | off(mockEventType, mockCallback);
68 |
69 | expect(window.cardano.nami._events).toEqual({
70 | [mockEventType]: [],
71 | });
72 | });
73 |
74 | test('stops the given callback from being invoked when cleaned out', () => {
75 | off(mockEventType, mockCallback);
76 |
77 | const event = makeEvent(mockEventType);
78 |
79 | window.dispatchEvent(event);
80 |
81 | expect(mockCallback).not.toHaveBeenCalled();
82 | });
83 |
84 | test('does not stop other callbacks from being invoked after cleaned one out', () => {
85 | const mockFn = jest.fn();
86 |
87 | on('event-A', mockFn);
88 |
89 | off('mockEventType', mockCallback);
90 |
91 | const event = makeEvent('event-A');
92 |
93 | window.dispatchEvent(event);
94 |
95 | expect(mockFn).toHaveBeenCalledTimes(1);
96 | });
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/src/test/unit/migrations/migration.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | getStorage,
3 | setStorage,
4 | createWallet,
5 | createAccount,
6 | } from '../../../api/extension';
7 | import Loader from '../../../api/loader';
8 | import { STORAGE } from '../../../config/config';
9 |
10 | const harden = (num) => {
11 | return 0x80000000 + num;
12 | };
13 |
14 | beforeAll(async () => {
15 | const seed =
16 | 'midnight draft salt dirt woman tragic cause immense dad later jaguar finger nerve nerve sign job erase citizen cube neglect token bracket orient narrow';
17 | const name = 'Wallet 1';
18 | const password = 'password123';
19 | await Loader.load();
20 | await createWallet(name, seed, password);
21 | await createAccount('Wallet 2', password);
22 | await createAccount('Wallet 3', password);
23 | let accounts = await getStorage(STORAGE.accounts);
24 | for (const accountIndex in accounts) {
25 | const account = accounts[accountIndex];
26 | account.mainnet = {
27 | ...account.mainnet,
28 | lovelace: Math.floor(Math.random() * 10000000).toString(),
29 | assets: [
30 | {
31 | unit: '0996b4e1e1fef04e29d9ea3c4282e011ddb1263513c7bd2ddac2fff9012',
32 | quantity: Math.floor(Math.random() * 1000).toString(),
33 | },
34 | ],
35 | };
36 | //test account function from 1.1.4 without minAda
37 | delete account.mainnet.minAda;
38 | delete account.testnet.minAda;
39 | }
40 | await setStorage({ [STORAGE.accounts]: accounts });
41 | });
42 |
43 | describe('no migration', () => {
44 | test('no migration', () => {});
45 | });
46 |
--------------------------------------------------------------------------------
/src/ui/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes, Route, useLocation } from 'react-router-dom';
3 | import { useNavigate } from 'react-router-dom';
4 | import { AnalyticsConsentModal } from '../features/analytics/ui/AnalyticsConsentModal';
5 | import { Box, Spinner } from '@chakra-ui/react';
6 | import Welcome from './app/pages/welcome';
7 | import Wallet from './app/pages/wallet';
8 | import { getAccounts } from '../api/extension';
9 | import Settings from './app/pages/settings';
10 | import Send from './app/pages/send';
11 | import { useStoreActions, useStoreState } from 'easy-peasy';
12 | import { useAnalyticsContext } from '../features/analytics/provider';
13 | import { TermsAndPrivacyProvider } from '../features/terms-and-privacy';
14 |
15 | export const App = () => {
16 | const route = useStoreState((state) => state.globalModel.routeStore.route);
17 | const setRoute = useStoreActions(
18 | (actions) => actions.globalModel.routeStore.setRoute
19 | );
20 | const navigate = useNavigate();
21 | const location = useLocation();
22 | const [isLoading, setIsLoading] = React.useState(true);
23 | const [analytics, setAnalyticsConsent] = useAnalyticsContext();
24 | const init = async () => {
25 | const hasWallet = await getAccounts();
26 | if (hasWallet) {
27 | navigate('/wallet');
28 | // Set route from localStorage if available
29 | if (route && route !== '/wallet') {
30 | route
31 | .slice(1)
32 | .split('/')
33 | .reduce((acc, r) => {
34 | const fullRoute = acc + `/${r}`;
35 | navigate(fullRoute);
36 | return fullRoute;
37 | }, '');
38 | }
39 | } else {
40 | navigate('/welcome');
41 | }
42 | setIsLoading(false);
43 | };
44 |
45 | React.useEffect(() => {
46 | init();
47 | }, []);
48 |
49 | React.useEffect(() => {
50 | if (!isLoading) {
51 | setRoute(location.pathname);
52 | }
53 | }, [location, isLoading, setRoute]);
54 |
55 | return isLoading ? (
56 |
63 |
64 |
65 | ) : (
66 |
67 |
68 |
72 |
73 |
74 | }
75 | />
76 | } />
77 | } />
78 | } />
79 |
80 |
84 |
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/src/ui/app/components/UpgradeModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IoRocketSharp } from 'react-icons/io5';
3 | import {
4 | Icon,
5 | Box,
6 | Text,
7 | Button,
8 | Modal,
9 | ModalOverlay,
10 | ModalContent,
11 | ModalHeader,
12 | ModalBody,
13 | ModalCloseButton,
14 | UnorderedList,
15 | ListItem,
16 | Heading,
17 | useDisclosure
18 | } from '@chakra-ui/react';
19 |
20 | export const UpgradeModal = React.forwardRef((props, ref) => {
21 | const { isOpen, onOpen, onClose } = useDisclosure();
22 |
23 | React.useImperativeHandle(ref, () => ({
24 | openModal() {
25 | onOpen();
26 | },
27 | }));
28 |
29 | return (
30 |
38 |
39 |
40 |
41 |
42 | What's new in Nami ?
43 |
44 |
45 |
46 | {props.info.map((item) => (
47 |
48 |
55 | Version {item.version}
56 |
57 |
58 | {item.info.map((info, index) => (
59 |
60 | {info.title}
61 | {info.detail}
62 |
63 | ))}
64 |
65 |
66 | ))}
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 | );
77 | });
78 |
--------------------------------------------------------------------------------
/src/ui/app/components/about.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Modal,
4 | ModalOverlay,
5 | ModalContent,
6 | ModalHeader,
7 | ModalBody,
8 | ModalCloseButton,
9 | useDisclosure,
10 | useColorModeValue,
11 | Image,
12 | Text,
13 | Box,
14 | Link,
15 | } from '@chakra-ui/react';
16 |
17 | import LogoWhite from '../../../assets/img/logoWhite.svg';
18 | import LogoBlack from '../../../assets/img/logo.svg';
19 | import IOHKWhite from '../../../assets/img/iohkWhite.svg';
20 | import IOHKBlack from '../../../assets/img/iohk.svg';
21 | import TermsOfUse from './termsOfUse';
22 | import PrivacyPolicy from './privacyPolicy';
23 | import { useCaptureEvent } from '../../../features/analytics/hooks';
24 | import { Events } from '../../../features/analytics/events';
25 |
26 | const { version } = require('../../../../package.json');
27 |
28 | const About = React.forwardRef((props, ref) => {
29 | const capture = useCaptureEvent();
30 | const { isOpen, onOpen, onClose } = useDisclosure();
31 | const Logo = useColorModeValue(LogoBlack, LogoWhite);
32 | const IOHK = useColorModeValue(IOHKWhite, IOHKBlack);
33 |
34 | const termsRef = React.useRef();
35 | const privacyPolRef = React.useRef();
36 |
37 | React.useImperativeHandle(ref, () => ({
38 | openModal() {
39 | onOpen();
40 | },
41 | closeModal() {
42 | onClose();
43 | },
44 | }));
45 | return (
46 | <>
47 |
54 |
55 |
56 | About
57 |
58 |
64 | window.open('https://namiwallet.io')}
67 | width="90px"
68 | src={Logo}
69 | />
70 |
71 | {version}
72 |
73 |
79 |
80 | Maintained by{' '}
81 | window.open('https://iohk.io/')}
83 | style={{ textDecoration: 'underline', cursor: 'pointer' }}
84 | >
85 | IOG
86 |
87 |
88 |
89 | window.open('https://iohk.io/')}
92 | src={IOHK}
93 | width="66px"
94 | />
95 |
96 |
97 | {/* Footer */}
98 |
99 | {
101 | capture(Events.SettingsTermsAndConditionsClick);
102 | termsRef.current.openModal();
103 | }}
104 | color="GrayText"
105 | >
106 | Terms of use
107 |
108 | |
109 | privacyPolRef.current.openModal()}
111 | color="GrayText"
112 | >
113 | Privacy Policy
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | >
123 | );
124 | });
125 |
126 | export default About;
127 |
--------------------------------------------------------------------------------
/src/ui/app/components/account.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getCurrentAccount } from '../../../api/extension';
3 |
4 | import Logo from '../../../assets/img/logoWhite.svg';
5 | import { Box, Text, Image, useColorModeValue } from '@chakra-ui/react';
6 | import AvatarLoader from './avatarLoader';
7 |
8 | const Account = React.forwardRef((props, ref) => {
9 | const avatarBg = useColorModeValue('white', 'gray.700');
10 | const panelBg = useColorModeValue('#349EA3', 'gray.800');
11 | const [account, setAccount] = React.useState(null);
12 |
13 | const initAccount = () =>
14 | getCurrentAccount().then((account) => setAccount(account));
15 |
16 | React.useImperativeHandle(ref, () => ({
17 | updateAccount() {
18 | initAccount();
19 | },
20 | }));
21 |
22 | React.useEffect(() => {
23 | initAccount();
24 | }, []);
25 |
26 | return (
27 |
35 |
46 |
47 |
48 |
61 |
66 |
67 |
76 |
77 | {account && account.name}
78 |
79 |
80 |
81 | );
82 | });
83 |
84 | export default Account;
85 |
--------------------------------------------------------------------------------
/src/ui/app/components/assetPopover.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Popover,
3 | PopoverArrow,
4 | PopoverBody,
5 | PopoverCloseButton,
6 | PopoverContent,
7 | PopoverTrigger,
8 | Box,
9 | Portal,
10 | Image,
11 | Avatar,
12 | Text,
13 | } from '@chakra-ui/react';
14 | import React from 'react';
15 | import Copy from './copy';
16 | import UnitDisplay from './unitDisplay';
17 |
18 | const AssetPopover = ({ asset, gutter, ...props }) => {
19 | return (
20 |
21 |
22 | {props.children}
23 |
24 |
25 |
26 |
27 |
28 |
37 | {asset && (
38 |
47 | }
53 | />
54 |
55 |
59 |
67 | {asset.displayName || asset.name}
68 |
69 |
70 |
71 |
72 |
76 |
77 |
78 |
79 |
80 |
81 | Policy: {asset.policy}
82 |
83 |
84 |
85 |
86 |
91 |
92 |
93 | Asset: {asset.fingerprint}
94 |
95 |
96 |
97 |
98 | )}
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default AssetPopover;
107 |
--------------------------------------------------------------------------------
/src/ui/app/components/assetsModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Box,
4 | Button,
5 | Modal,
6 | ModalBody,
7 | ModalContent,
8 | useColorModeValue,
9 | useDisclosure,
10 | } from '@chakra-ui/react';
11 | import { Scrollbars } from './scrollbar';
12 | import { LazyLoadComponent } from 'react-lazy-load-image-component';
13 | import Asset from './asset';
14 |
15 | const AssetsModal = React.forwardRef((props, ref) => {
16 | const { isOpen, onOpen, onClose } = useDisclosure();
17 | const [data, setData] = React.useState({
18 | title: '',
19 | assets: [],
20 | background: '',
21 | color: 'inherit',
22 | });
23 | const background = useColorModeValue('white', 'gray.800');
24 |
25 | const abs = (big) => {
26 | return big < 0 ? BigInt(big) * BigInt(-1) : big;
27 | };
28 |
29 | React.useImperativeHandle(ref, () => ({
30 | openModal(data) {
31 | setData(data);
32 | onOpen();
33 | },
34 | }));
35 |
36 | return (
37 |
43 |
49 |
50 |
51 |
58 |
59 |
65 | {data.title}
66 |
67 |
68 | {data.assets.map((asset, index) => {
69 | asset = {
70 | ...asset,
71 | quantity: abs(asset.quantity).toString(),
72 | };
73 | return (
74 |
75 |
76 |
83 |
88 |
89 |
90 |
91 | );
92 | })}
93 |
101 |
109 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | });
121 |
122 | export default AssetsModal;
123 |
--------------------------------------------------------------------------------
/src/ui/app/components/autocomplete.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import {
3 | Box,
4 | Flex,
5 | IconButton,
6 | Input,
7 | InputGroup,
8 | InputRightElement,
9 | List,
10 | ListItem,
11 | } from '@chakra-ui/react';
12 | import { matchSorter } from 'match-sorter';
13 | import {
14 | CheckCircleIcon,
15 | ChevronDownIcon,
16 | SmallAddIcon,
17 | } from '@chakra-ui/icons';
18 |
19 | const Autocomplete = ({
20 | options,
21 | result,
22 | setResult,
23 | placeholder,
24 | renderBadge,
25 | inputName,
26 | inputId,
27 | bgHoverColor,
28 | createText,
29 | allowCreation,
30 | notFoundText,
31 | isInvalid,
32 | width,
33 | ...props
34 | }) => {
35 | const [optionsCopy, setOptionsCopy] = useState(options);
36 | const [partialResult, setPartialResult] = useState();
37 | const [displayOptions, setDisplayOptions] = useState(false);
38 | const [inputValue, setInputValue] = useState();
39 | const inputRef = useRef(null);
40 |
41 | const filterOptions = (inputValue) => {
42 | if (inputValue) {
43 | setDisplayOptions(true);
44 | setPartialResult(
45 | matchSorter(optionsCopy, inputValue, { keys: ['label', 'value'] })
46 | );
47 | setInputValue(inputValue);
48 | } else {
49 | setDisplayOptions(false);
50 | }
51 | };
52 |
53 | const selectOption = (option) => {
54 | if (result.includes(option)) {
55 | setResult([
56 | ...result.filter(
57 | (existingOption) => existingOption.value !== option.value
58 | ),
59 | ]);
60 | } else {
61 | setResult([option, ...result]);
62 | }
63 | };
64 |
65 | const isOptionSelected = (option) => {
66 | return (
67 | result.filter((selectedOption) => selectedOption.value === option.value)
68 | .length > 0
69 | );
70 | };
71 |
72 | const createOption = () => {
73 | if (inputValue && allowCreation) {
74 | const newOption = {
75 | label: inputValue,
76 | value: inputValue,
77 | };
78 | setOptionsCopy([newOption, ...optionsCopy]);
79 | selectOption(newOption);
80 | setDisplayOptions(false);
81 | if (inputRef && inputRef.current !== null) {
82 | inputRef.current.value = '';
83 | }
84 | }
85 | };
86 |
87 | const selectOptionFromList = (option) => {
88 | selectOption(option);
89 | setDisplayOptions(false);
90 | if (inputRef && inputRef.current !== null) {
91 | inputRef.current.value = '';
92 | }
93 | };
94 |
95 | const renderCheckIcon = (option) => {
96 | if (isOptionSelected(option)) {
97 | if (props.renderCheckIcon) {
98 | return props.renderCheckIcon;
99 | } else {
100 | return ;
101 | }
102 | }
103 | return null;
104 | };
105 |
106 | const renderCreateIcon = () => {
107 | if (props.renderCreateIcon) {
108 | return props.renderCreateIcon;
109 | } else {
110 | return ;
111 | }
112 | };
113 |
114 | return (
115 |
116 |
117 | filterOptions(e.currentTarget.value)}
122 | ref={inputRef}
123 | isInvalid={isInvalid}
124 | />
125 | } />}
127 | />
128 |
129 |
130 | {displayOptions && (
131 |
138 | {partialResult?.map((option) => {
139 | return (
140 | selectOptionFromList(option)}
147 | >
148 |
149 | {renderCheckIcon(option)}
150 | {option.label}
151 |
152 |
153 | );
154 | })}
155 | {!partialResult?.length && allowCreation && (
156 | createOption()}
163 | >
164 |
165 | {renderCreateIcon()}
166 | {createText}
167 |
168 |
169 | )}
170 | {!partialResult?.length && !allowCreation && (
171 |
172 | {notFoundText}
173 |
174 | )}
175 |
176 | )}
177 |
178 | );
179 | };
180 |
181 | Autocomplete.defaultProps = {
182 | notFoundText: 'Not found',
183 | allowCreation: true,
184 | createText: 'Create option',
185 | };
186 |
187 | export default Autocomplete;
188 |
--------------------------------------------------------------------------------
/src/ui/app/components/avatarLoader.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Image } from '@chakra-ui/react';
2 | import React from 'react';
3 | import { avatarToImage } from '../../../api/extension';
4 |
5 | const AvatarLoader = ({ avatar, width, smallRobot }) => {
6 | const [loaded, setLoaded] = React.useState('');
7 |
8 | const fetchAvatar = async () => {
9 | if (!avatar || avatar === loaded) return;
10 | setLoaded(Number(avatar) ? avatarToImage(avatar) : avatar);
11 | };
12 |
13 | React.useEffect(() => {
14 | fetchAvatar();
15 | }, [avatar]);
16 | return (
17 |
26 | );
27 | };
28 |
29 | export default AvatarLoader;
30 |
--------------------------------------------------------------------------------
/src/ui/app/components/collectible.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Avatar,
4 | Image,
5 | Skeleton,
6 | useColorModeValue,
7 | } from '@chakra-ui/react';
8 | import React from 'react';
9 | import './styles.css';
10 | import { getAsset } from '../../../api/extension';
11 | import { useCaptureEvent } from '../../../features/analytics/hooks';
12 | import { Events } from '../../../features/analytics/events';
13 |
14 | const useIsMounted = () => {
15 | const isMounted = React.useRef(false);
16 | React.useEffect(() => {
17 | isMounted.current = true;
18 | return () => (isMounted.current = false);
19 | }, []);
20 | return isMounted;
21 | };
22 |
23 | const Collectible = React.forwardRef(({ asset }, ref) => {
24 | const capture = useCaptureEvent();
25 | const isMounted = useIsMounted();
26 | const [token, setToken] = React.useState(null);
27 | const background = useColorModeValue('gray.300', 'white');
28 | const [showInfo, setShowInfo] = React.useState(false);
29 |
30 | const fetchMetadata = async () => {
31 | const detailedConstructedAsset = await getAsset(asset.unit);
32 | const detailedAsset = {
33 | ...detailedConstructedAsset,
34 | quantity: asset.quantity,
35 | fingerprint: asset.fingerprint ?? detailedConstructedAsset.fingerprint,
36 | };
37 | if (!isMounted.current) return;
38 | setToken(detailedAsset);
39 | };
40 |
41 | React.useEffect(() => {
42 | fetchMetadata();
43 | }, [asset]);
44 |
45 | return (
46 | <>
47 | {
49 | capture(Events.NFTsImageClick);
50 | token && ref.current.openModal(token);
51 | }}
52 | position="relative"
53 | display="flex"
54 | alignItems="center"
55 | justifyContent="center"
56 | flexDirection="column"
57 | width="160px"
58 | height="160px"
59 | overflow="hidden"
60 | rounded="3xl"
61 | background={background}
62 | border="solid 1px"
63 | borderColor={background}
64 | onMouseEnter={() => setShowInfo(true)}
65 | onMouseLeave={() => setShowInfo(false)}
66 | cursor="pointer"
67 | userSelect="none"
68 | >
69 |
76 | {!token ? (
77 |
78 | ) : (
79 |
86 | ) : (
87 |
88 | )
89 | }
90 | />
91 | )}
92 |
93 | {token && (
94 |
104 |
115 |
124 | {token.displayName}
125 |
126 |
133 | x {token.quantity}
134 |
135 |
136 |
137 | )}
138 |
139 | >
140 | );
141 | });
142 |
143 | const Fallback = ({ name }) => {
144 | const [timedOut, setTimedOut] = React.useState(false);
145 | const isMounted = useIsMounted();
146 | React.useEffect(() => {
147 | setTimeout(() => isMounted.current && setTimedOut(true), 30000);
148 | }, []);
149 | if (timedOut) return ;
150 | return ;
151 | };
152 |
153 | export default Collectible;
154 |
--------------------------------------------------------------------------------
/src/ui/app/components/copy.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Tooltip } from '@chakra-ui/react';
2 | import React from 'react';
3 |
4 | const Copy = ({ label, copy, onClick, ...props }) => {
5 | const [copied, setCopied] = React.useState(false);
6 | return (
7 |
8 | {
11 | if (onClick) onClick();
12 | navigator.clipboard.writeText(copy);
13 | setCopied(true);
14 | setTimeout(() => setCopied(false), 800);
15 | }}
16 | >
17 | {props.children}
18 |
19 |
20 | );
21 | };
22 |
23 | export default Copy;
24 |
--------------------------------------------------------------------------------
/src/ui/app/components/historyViewer.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Text, Spinner, Accordion, Button } from '@chakra-ui/react';
2 | import { ChevronDownIcon } from '@chakra-ui/icons';
3 | import React from 'react';
4 | import { File } from 'react-kawaii';
5 | import {
6 | getTransactions,
7 | setTransactions,
8 | setTxDetail,
9 | } from '../../../api/extension';
10 | import Transaction from './transaction';
11 | import { useCaptureEvent } from '../../../features/analytics/hooks';
12 | import { Events } from '../../../features/analytics/events';
13 |
14 | const BATCH = 5;
15 |
16 | let slice = [];
17 |
18 | let txObject = {};
19 |
20 | const HistoryViewer = ({ history, network, currentAddr, addresses }) => {
21 | const capture = useCaptureEvent();
22 | const [historySlice, setHistorySlice] = React.useState(null);
23 | const [page, setPage] = React.useState(1);
24 | const [final, setFinal] = React.useState(false);
25 | const [loadNext, setLoadNext] = React.useState(false);
26 | const getTxs = async () => {
27 | if (!history) {
28 | slice = [];
29 | setHistorySlice(null);
30 | setPage(1);
31 | setFinal(false);
32 | return;
33 | }
34 | await new Promise((res, rej) => setTimeout(() => res(), 10));
35 | slice = slice.concat(
36 | history.confirmed.slice((page - 1) * BATCH, page * BATCH)
37 | );
38 |
39 | if (slice.length < page * BATCH) {
40 | const txs = await getTransactions(page, BATCH);
41 |
42 | if (txs.length <= 0) {
43 | setFinal(true);
44 | } else {
45 | slice = Array.from(new Set(slice.concat(txs.map((tx) => tx.txHash))));
46 | await setTransactions(slice);
47 | }
48 | }
49 | if (slice.length < page * BATCH) setFinal(true);
50 | setHistorySlice(slice);
51 | };
52 |
53 | React.useEffect(() => {
54 | getTxs();
55 | }, [history, page]);
56 |
57 | React.useEffect(() => {
58 | const storeTx = setInterval(() => {
59 | if (Object.keys(txObject).length <= 0) return;
60 | setTimeout(() => setTxDetail(txObject));
61 | }, 1000);
62 | return () => {
63 | slice = [];
64 | setHistorySlice(null);
65 | setPage(1);
66 | setFinal(false);
67 | clearInterval(storeTx);
68 | };
69 | }, []);
70 |
71 | React.useEffect(() => {
72 | if (!historySlice) return;
73 | if (historySlice.length >= (page - 1) * BATCH) setLoadNext(false);
74 | }, [historySlice]);
75 |
76 | return (
77 |
78 | {!(history && historySlice) ? (
79 |
80 | ) : historySlice.length <= 0 ? (
81 |
89 |
90 |
91 |
92 | No History
93 |
94 |
95 | ) : (
96 | <>
97 | {
101 | capture(Events.ActivityActivityActivityRowClick);
102 | }}
103 | >
104 | {historySlice.map((txHash, index) => {
105 | if (!history.details[txHash]) history.details[txHash] = {};
106 |
107 | return (
108 | {
110 | txObject[txHash] = txDetail;
111 | }}
112 | key={index}
113 | txHash={txHash}
114 | detail={history.details[txHash]}
115 | currentAddr={currentAddr}
116 | addresses={addresses}
117 | network={network}
118 | />
119 | );
120 | })}
121 |
122 | {final ? (
123 |
130 | ... nothing more
131 |
132 | ) : (
133 |
134 |
149 |
150 | )}
151 | >
152 | )}
153 |
154 | );
155 | };
156 |
157 | const HistorySpinner = () => (
158 |
159 |
160 |
161 | );
162 |
163 | export default HistoryViewer;
164 |
--------------------------------------------------------------------------------
/src/ui/app/components/qrCode.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QRCodeStyling from 'qr-code-styling';
3 | import Ada from '../../../assets/img/ada.png';
4 | import { useColorModeValue } from '@chakra-ui/react';
5 |
6 | const qrCode = new QRCodeStyling({
7 | width: 150,
8 | height: 150,
9 | image: Ada,
10 | dotsOptions: {
11 | color: '#319795',
12 | type: 'dots',
13 | },
14 | cornersSquareOptions: { type: 'extra-rounded', color: '#DD6B20' },
15 | imageOptions: {
16 | crossOrigin: 'anonymous',
17 | margin: 8,
18 | },
19 | });
20 |
21 | const QrCode = ({ value }) => {
22 | const ref = React.useRef(null);
23 | const bgColor = useColorModeValue('white', '#2D3748');
24 | const contentColor = useColorModeValue(
25 | { corner: '#DD6B20', dots: '#319795' },
26 | { corner: '#FBD38D', dots: '#81E6D9' }
27 | );
28 |
29 | React.useEffect(() => {
30 | qrCode.append(ref.current);
31 | }, []);
32 |
33 | React.useEffect(() => {
34 | qrCode.update({
35 | data: value,
36 | backgroundOptions: {
37 | color: bgColor,
38 | },
39 | dotsOptions: {
40 | color: contentColor.dots,
41 | },
42 | cornersSquareOptions: { color: contentColor.corner },
43 | });
44 | }, [value, bgColor]);
45 |
46 | return ;
47 | };
48 |
49 | export default QrCode;
50 |
--------------------------------------------------------------------------------
/src/ui/app/components/scrollbar.jsx:
--------------------------------------------------------------------------------
1 | export { Scrollbars } from 'react-custom-scrollbars-2';
2 |
--------------------------------------------------------------------------------
/src/ui/app/components/styles.css:
--------------------------------------------------------------------------------
1 | .lineClamp {
2 | display: -webkit-box;
3 | -webkit-line-clamp: 2;
4 | -webkit-box-orient: vertical;
5 | }
6 |
7 | .lineClamp3 {
8 | display: -webkit-box;
9 | -webkit-line-clamp: 3;
10 | -webkit-box-orient: vertical;
11 | }
12 |
13 | body::-webkit-scrollbar {
14 | display: none;
15 | }
16 |
--------------------------------------------------------------------------------
/src/ui/app/components/trezorWidget.jsx:
--------------------------------------------------------------------------------
1 | import { CloseIcon } from '@chakra-ui/icons';
2 | import { Box, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
3 | import React from 'react';
4 |
5 | const TrezorWidget = React.forwardRef((props, ref) => {
6 | const { isOpen, onOpen, onClose } = useDisclosure();
7 | React.useImperativeHandle(ref, () => ({
8 | openModal() {
9 | onOpen();
10 | },
11 | closeModal() {
12 | onClose();
13 | },
14 | }));
15 | return (
16 |
22 |
23 |
32 |
40 |
49 |
50 |
56 |
57 |
58 |
59 |
60 | );
61 | });
62 |
63 | export default TrezorWidget;
64 |
--------------------------------------------------------------------------------
/src/ui/app/components/unitDisplay.jsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react';
2 | import React from 'react';
3 | import { displayUnit } from '../../../api/extension';
4 |
5 | const hideZero = (str) =>
6 | str[str.length - 1] == 0 ? hideZero(str.slice(0, -1)) : str;
7 |
8 | const UnitDisplay = ({ quantity, decimals, symbol, hide, ...props }) => {
9 | const num = displayUnit(quantity, decimals)
10 | .toLocaleString('en-EN', { minimumFractionDigits: decimals })
11 | .split('.')[0];
12 | const subNum = displayUnit(quantity, decimals)
13 | .toLocaleString('en-EN', { minimumFractionDigits: decimals })
14 | .split('.')[1];
15 | return (
16 |
17 | {quantity || quantity === 0 ? (
18 | <>
19 | {num}
20 | {(hide && hideZero(subNum).length <= 0) || decimals == 0 ? '' : '.'}
21 |
22 | {hide ? hideZero(subNum) : subNum}
23 | {' '}
24 | >
25 | ) : (
26 | '... '
27 | )}
28 | {symbol}
29 |
30 | );
31 | };
32 |
33 | export default UnitDisplay;
34 |
--------------------------------------------------------------------------------
/src/ui/app/pages/enable.jsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from '@chakra-ui/icons';
2 | import { Box, Button, Text, Image, useColorModeValue } from '@chakra-ui/react';
3 | import React from 'react';
4 | import { setWhitelisted } from '../../../api/extension';
5 | import { APIError } from '../../../config/config';
6 |
7 | import Account from '../components/account';
8 | import { useCaptureEvent } from '../../../features/analytics/hooks';
9 | import { Events } from '../../../features/analytics/events';
10 |
11 | const Enable = ({ request, controller }) => {
12 | const capture = useCaptureEvent();
13 | const background = useColorModeValue('gray.100', 'gray.700');
14 | return (
15 |
22 |
23 |
24 |
30 |
39 |
45 |
46 |
47 | {request.origin.split('//')[1]}
48 |
49 | This app would like to:
50 |
51 |
59 |
60 | {' '}
61 | View your balance and addresses
62 |
63 |
64 |
65 | {' '}
66 | Request approval for transactions
67 |
68 |
69 |
70 | Only connect with sites you trust
71 |
72 |
80 |
91 |
92 |
105 |
106 |
107 | );
108 | };
109 |
110 | export default Enable;
111 |
--------------------------------------------------------------------------------
/src/ui/app/pages/noWallet.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Text, Image, useColorModeValue } from '@chakra-ui/react';
2 | import React from 'react';
3 | import { Backpack } from 'react-kawaii';
4 |
5 | import BannerWhite from '../../../assets/img/bannerWhite.svg';
6 | import BannerBlack from '../../../assets/img/bannerBlack.svg';
7 |
8 | const NoWallet = () => {
9 | const Banner = useColorModeValue(BannerBlack, BannerWhite);
10 | return (
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | No Wallet
27 |
28 |
29 |
30 | Open the panel at the top right in order to create a wallet.
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default NoWallet;
38 |
--------------------------------------------------------------------------------
/src/ui/app/tabs/trezorTx.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TAB } from '../../../config/config';
3 | import Main from '../../index';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 | import { createRoot } from 'react-dom/client';
6 | import { Box, useColorModeValue, Image, useToast } from '@chakra-ui/react';
7 | import { AnalyticsProvider } from '../../../features/analytics/provider';
8 | import { EventTracker } from '../../../features/analytics/event-tracker';
9 | import { ExtensionViews } from '../../../features/analytics/types';
10 |
11 | // assets
12 | import LogoOriginal from '../../../assets/img/logo.svg';
13 | import LogoWhite from '../../../assets/img/logoWhite.svg';
14 | import { getCurrentAccount, indexToHw, initHW } from '../../../api/extension';
15 | import { signAndSubmitHW } from '../../../api/extension/wallet';
16 | import Loader from '../../../api/loader';
17 | import { useStoreActions } from 'easy-peasy';
18 |
19 | const App = () => {
20 | const Logo = useColorModeValue(LogoOriginal, LogoWhite);
21 | const backgroundColor = useColorModeValue('gray.200', 'inherit');
22 | const toast = useToast();
23 |
24 | const setRoute = useStoreActions(
25 | (actions) => actions.globalModel.routeStore.setRoute
26 | );
27 | const resetSend = useStoreActions(
28 | (actions) => actions.globalModel.sendStore.reset
29 | );
30 |
31 | const init = async () => {
32 | await Loader.load();
33 |
34 | const account = await getCurrentAccount();
35 | const params = new URLSearchParams(window.location.search);
36 | const tx = params.get('tx');
37 | const hw = indexToHw(account.index);
38 |
39 | const txDes = Loader.Cardano.Transaction.from_cbor_bytes(Buffer.from(tx, 'hex'));
40 | await initHW({ device: hw.device, id: hw.id });
41 | try {
42 | await signAndSubmitHW(txDes, {
43 | keyHashes: [account.paymentKeyHash],
44 | account,
45 | hw,
46 | });
47 | toast({
48 | title: 'Transaction submitted',
49 | status: 'success',
50 | duration: 3000,
51 | });
52 | } catch (_e) {
53 | toast({
54 | title: 'Transaction failed',
55 | status: 'error',
56 | duration: 3000,
57 | });
58 | }
59 | resetSend();
60 | setRoute('/wallet');
61 | setTimeout(() => window.close(), 3000);
62 | };
63 |
64 | React.useEffect(() => init(), []);
65 |
66 | return (
67 |
76 | {/* Logo */}
77 |
78 |
79 |
80 |
81 |
87 | Waiting for Trezor...
88 |
89 |
90 | );
91 | };
92 |
93 | const root = createRoot(window.document.querySelector(`#${TAB.trezorTx}`));
94 | root.render(
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 |
105 | if (module.hot) module.hot.accept();
106 |
--------------------------------------------------------------------------------
/src/ui/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { POPUP, POPUP_WINDOW, TAB } from '../config/config';
3 | import { Scrollbars } from './app/components/scrollbar';
4 | import './app/components/styles.css';
5 | import Theme from './theme';
6 | import StoreProvider from './store';
7 | import { Box, IconButton } from '@chakra-ui/react';
8 | import { ChevronUpIcon } from '@chakra-ui/icons';
9 |
10 | const isMain = window.document.querySelector(`#${POPUP.main}`);
11 | const isTab = window.document.querySelector(`#${TAB.hw}`);
12 |
13 | const Main = ({ children }) => {
14 | const [scroll, setScroll] = React.useState({ el: null, y: 0 });
15 |
16 | React.useEffect(() => {
17 | window.document.body.addEventListener(
18 | 'keydown',
19 | (e) => e.key === 'Escape' && e.preventDefault()
20 | );
21 | // Windows is somehow not opening the popup with the right size. Dynamically changing it, fixes it for now:
22 | if (navigator.userAgent.indexOf('Win') != -1 && !isMain && !isTab) {
23 | const width =
24 | POPUP_WINDOW.width + (window.outerWidth - window.innerWidth);
25 | const height =
26 | POPUP_WINDOW.height + (window.outerHeight - window.innerHeight);
27 | window.resizeTo(width, height);
28 | }
29 | }, []);
30 | return (
31 |
35 |
36 |
37 | {
42 | setScroll({ el: e.target, y: e.target.scrollTop });
43 | }}
44 | >
45 | {children}
46 | {scroll.y > 1200 && (
47 | {
49 | scroll.el.scrollTo({ behavior: 'smooth', top: 0 });
50 | }}
51 | position="fixed"
52 | bottom="15px"
53 | right="15px"
54 | size="sm"
55 | rounded="xl"
56 | colorScheme="teal"
57 | opacity={0.85}
58 | icon={}
59 | >
60 | )}
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default Main;
69 |
--------------------------------------------------------------------------------
/src/ui/indexInternal.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * indexInternal is the entry point for the popup windows (e.g. data signing, tx signing)
3 | */
4 |
5 | import { Box, Spinner } from '@chakra-ui/react';
6 | import React from 'react';
7 | import { createRoot } from 'react-dom/client';
8 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
9 | import { useNavigate } from 'react-router-dom';
10 | import { getAccounts } from '../api/extension';
11 | import { Messaging } from '../api/messaging';
12 | import { AnalyticsProvider } from '../features/analytics/provider';
13 | import { EventTracker } from '../features/analytics/event-tracker';
14 | import { ExtensionViews } from '../features/analytics/types';
15 |
16 | import { METHOD, POPUP } from '../config/config';
17 | import Enable from './app/pages/enable';
18 | import NoWallet from './app/pages/noWallet';
19 | import SignData from './app/pages/signData';
20 | import SignTx from './app/pages/signTx';
21 | import Main from './index';
22 |
23 | const App = () => {
24 | const controller = Messaging.createInternalController();
25 | const navigate = useNavigate();
26 | const [request, setRequest] = React.useState(null);
27 |
28 | const init = async () => {
29 | const request = await controller.requestData();
30 | const hasWallet = await getAccounts();
31 | setRequest(request);
32 | if (!hasWallet) navigate('/noWallet');
33 | else if (request.method === METHOD.enable) navigate('/enable');
34 | else if (request.method === METHOD.signData) navigate('/signData');
35 | else if (request.method === METHOD.signTx) navigate('/signTx');
36 | };
37 |
38 | React.useEffect(() => {
39 | init();
40 | }, []);
41 |
42 | return !request ? (
43 |
50 |
51 |
52 | ) : (
53 |
54 |
55 | }
58 | />
59 | }
62 | />
63 | }
66 | />
67 | } />
68 |
69 |
70 | );
71 | };
72 |
73 | const root = createRoot(window.document.querySelector(`#${POPUP.internal}`));
74 | root.render(
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 |
85 | if (module.hot) module.hot.accept();
86 |
--------------------------------------------------------------------------------
/src/ui/indexMain.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * indexMain is the entry point for the extension panel you open at the top right in the browser
3 | */
4 | import React from 'react';
5 | import { createRoot } from 'react-dom/client';
6 | import { POPUP } from '../config/config';
7 | import { EventTracker } from '../features/analytics/event-tracker';
8 | import { ExtensionViews } from '../features/analytics/types';
9 | import { AppWithMigration } from './lace-migration/components/migration.component';
10 | import Theme from './theme';
11 | import { BrowserRouter as Router } from 'react-router-dom';
12 | import Main from './index';
13 | import { AnalyticsProvider } from '../features/analytics/provider';
14 |
15 | const root = createRoot(window.document.querySelector(`#${POPUP.main}`));
16 | root.render(
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | if (module.hot) module.hot.accept();
30 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/checkmark.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/clock.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/assets/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/all-done/all-done.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useColorMode, Box } from '@chakra-ui/react';
3 | import { Slide } from '../slide.component';
4 | import { ReactComponent as Arrow } from '../../assets/arrow.svg';
5 | import { ReactComponent as DoneDark } from '../../assets/done-dark.svg';
6 | import { ReactComponent as DoneWhite } from '../../assets/done-white.svg';
7 |
8 | export const AllDone = ({ isLaceInstalled, onAction }) => {
9 | const { colorMode } = useColorMode();
10 | return (
11 |
15 | {colorMode === 'light' ? (
16 |
17 | ) : (
18 |
19 | )}
20 |
21 | }
22 | description="Your Nami wallet is now part of the Lace family."
23 | buttonText={isLaceInstalled ? 'Open Lace' : 'Download Lace'}
24 | buttonIcon={Arrow}
25 | onButtonClick={onAction}
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/all-done/all-done.stories.js:
--------------------------------------------------------------------------------
1 | import { AllDone } from './all-done.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/AllDone',
5 | component: AllDone,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | onAction: () => {},
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | export const Primary = {};
17 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/almost-there/almost-there.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useColorMode, Box } from '@chakra-ui/react';
3 | import { Slide } from '../slide.component';
4 | import { ReactComponent as Download } from '../../assets/download.svg';
5 | import { ReactComponent as Arrow } from '../../assets/arrow.svg';
6 | import { ReactComponent as PendingDark } from '../../assets/pending-dark-mode.svg';
7 | import { ReactComponent as PendingWhite } from '../../assets/pending-white-mode.svg';
8 | import { useCaptureEvent } from '../../../../features/analytics/hooks';
9 | import { Events } from '../../../../features/analytics/events';
10 |
11 | export const AlmostThere = ({
12 | isLaceInstalled,
13 | onAction,
14 | isDismissable,
15 | dismissibleSeconds,
16 | }) => {
17 | const { colorMode } = useColorMode();
18 | const captureEvent = useCaptureEvent();
19 | return (
20 |
27 | {colorMode === 'light' ? (
28 |
29 | ) : (
30 |
31 | )}
32 |
33 | }
34 | description={
35 | isLaceInstalled
36 | ? 'Your Nami wallet is now part of the Lace family.'
37 | : 'Download the Lace extension to begin.'
38 | }
39 | buttonText={isLaceInstalled ? 'Open Lace' : 'Download Lace'}
40 | buttonIcon={isLaceInstalled ? Arrow : Download}
41 | onButtonClick={async () => {
42 | await captureEvent(Events.NamiMigrationOpenLaceOrOpenChromeStore);
43 | onAction();
44 | }}
45 | isDismissable={isDismissable}
46 | buttonOrientation="column"
47 | dismissibleSeconds={dismissibleSeconds}
48 | onDismiss={() => captureEvent(Events.NamiMigrationDismissedInProgress)}
49 | />
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/almost-there/almost-there.stories.js:
--------------------------------------------------------------------------------
1 | import { AlmostThere } from './almost-there.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/AlmostThere',
5 | component: AlmostThere,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | isLaceInstalled: true,
11 | onAction: () => {},
12 | },
13 | };
14 |
15 | export default meta;
16 |
17 | export const Primary = {};
18 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/carousel.component.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, Button, useColorModeValue, Flex } from '@chakra-ui/react';
3 | import { ReactComponent as Left } from '../../assets/chevron-left.svg';
4 | import { ReactComponent as Right } from '../../assets/chevron-right.svg';
5 | const CarouselButton = ({ children, ...rest }) => (
6 |
20 | );
21 |
22 | export const Carousel = ({ children, onSlideSwitched }) => {
23 | const [currentIndex, setCurrentIndex] = useState(0);
24 |
25 | const prevSlide = () => {
26 | setCurrentIndex((prevIndex) => {
27 | const nextIndex = prevIndex === 0 ? children.length - 1 : prevIndex - 1;
28 | onSlideSwitched?.(nextIndex);
29 | return nextIndex;
30 | });
31 | };
32 |
33 | const nextSlide = () => {
34 | setCurrentIndex((prevIndex) => {
35 | const nextIndex = prevIndex === children.length - 1 ? 0 : prevIndex + 1;
36 | onSlideSwitched?.(nextIndex);
37 | return nextIndex;
38 | });
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {children[currentIndex]}
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/carousel.stories.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Carousel } from './carousel.component';
3 | import { Slide1 } from './slides/Slide1.component';
4 | import { Slide2 } from './slides/Slide2.component';
5 | import { Slide3 } from './slides/Slide3.component';
6 | import { AlmostThere } from '../almost-there/almost-there.component';
7 | import { AllDone } from '../all-done/all-done.component';
8 |
9 | const meta = {
10 | title: 'Nami Migration/Screens/Carousel',
11 | component: Carousel,
12 | parameters: {
13 | layout: 'centered',
14 | },
15 | args: {
16 | children: [
17 | {}} />,
18 | {}} />,
19 | {}} />,
20 | {}} />,
21 | {}} />,
22 | ],
23 | },
24 | };
25 |
26 | export default meta;
27 |
28 | export const Primary = {};
29 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide1.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@chakra-ui/react';
3 | import { Slide } from '../../slide.component';
4 | import { ReactComponent as Arrow } from '../../../assets/arrow.svg';
5 | import { ReactComponent as BackpackImg } from '../../../assets/backpack.svg';
6 | import { useCaptureEvent } from '../../../../../features/analytics/hooks';
7 | import { Events } from '../../../../../features/analytics/events';
8 |
9 | export const Slide1 = ({ onAction, isDismissable, dismissibleSeconds }) => {
10 | const captureEvent = useCaptureEvent();
11 | return (
12 |
17 |
18 |
19 | }
20 | description="The Nami Wallet is now integrated into Lace. Click 'Upgrade your wallet' to begin the process."
21 | buttonText="Upgrade your wallet"
22 | buttonIcon={Arrow}
23 | onButtonClick={async () => await onAction()}
24 | isDismissable={isDismissable}
25 | dismissibleSeconds={dismissibleSeconds}
26 | buttonOrientation={isDismissable ? 'column' : 'row'}
27 | onDismiss={async () =>
28 | captureEvent(Events.NamiMigrationDismissedNotStarted)
29 | }
30 | />
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide1.stories.js:
--------------------------------------------------------------------------------
1 | import { Slide1 } from './Slide1.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/Carousel/Slide 1',
5 | component: Slide1,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | onAction: () => {},
11 | isDismissable: true,
12 | },
13 | };
14 |
15 | export default meta;
16 |
17 | export const Primary = {};
18 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide2.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Slide } from '../../slide.component';
3 | import { ReactComponent as Arrow } from '../../../assets/arrow.svg';
4 | import { ReactComponent as SeamlessDark } from '../../../assets/grouped-dark-mode.svg';
5 | import { ReactComponent as SeamlessWhite } from '../../../assets/grouped-white-mode.svg';
6 | import { useColorMode, Box } from '@chakra-ui/react';
7 | import { useCaptureEvent } from '../../../../../features/analytics/hooks';
8 | import { Events } from '../../../../../features/analytics/events';
9 |
10 | export const Slide2 = ({ onAction, isDismissable, dismissibleSeconds }) => {
11 | const { colorMode } = useColorMode();
12 | const captureEvent = useCaptureEvent();
13 | return (
14 |
19 | {colorMode === 'light' ? (
20 |
21 | ) : (
22 |
23 | )}
24 |
25 | }
26 | description="On the surface, Nami is the same. But now, with Lace's advanced technology supporting it."
27 | buttonText="Upgrade your wallet"
28 | buttonIcon={Arrow}
29 | onButtonClick={onAction}
30 | isDismissable={isDismissable}
31 | dismissibleSeconds={dismissibleSeconds}
32 | buttonOrientation={isDismissable ? 'column' : 'row'}
33 | onDismiss={async () =>
34 | captureEvent(Events.NamiMigrationDismissedNotStarted)
35 | }
36 | />
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide2.stories.js:
--------------------------------------------------------------------------------
1 | import { Slide2 } from './Slide2.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/Carousel/Slide 2',
5 | component: Slide2,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | onAction: () => {},
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | export const Primary = {};
17 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide3.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Slide } from '../../slide.component';
3 | import { ReactComponent as Arrow } from '../../../assets/arrow.svg';
4 | import { Box } from '@chakra-ui/react';
5 | import { ReactComponent as FeaturesImg } from '../../../assets/features.svg';
6 | import { useCaptureEvent } from '../../../../../features/analytics/hooks';
7 | import { Events } from '../../../../../features/analytics/events';
8 |
9 | export const Slide3 = ({ onAction, isDismissable, dismissibleSeconds }) => {
10 | const captureEvent = useCaptureEvent();
11 | return (
12 |
18 |
19 |
20 | }
21 | buttonText="Upgrade your wallet"
22 | buttonIcon={Arrow}
23 | onButtonClick={onAction}
24 | isDismissable={isDismissable}
25 | dismissibleSeconds={dismissibleSeconds}
26 | buttonOrientation={isDismissable ? 'column' : 'row'}
27 | onDismiss={async () =>
28 | captureEvent(Events.NamiMigrationDismissedNotStarted)
29 | }
30 | />
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/carousel/slides/Slide3.stories.js:
--------------------------------------------------------------------------------
1 | import { Slide3 } from './Slide3.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/Carousel/Slide 3',
5 | component: Slide3,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | onAction: () => {},
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | export const Primary = {};
17 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/dismiss-btn.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Flex, useColorModeValue } from '@chakra-ui/react';
3 | import { Text } from './text.component';
4 | import { dismissMigration } from '../../../api/migration-tool/cross-extension-messaging/nami-migration-client.extension';
5 | import { ReactComponent as PendingDark } from '../assets/clock.svg';
6 |
7 | export const DismissBtn = ({
8 | dismissableIntervalSeconds,
9 | hasIcon,
10 | onDismiss,
11 | }) => {
12 | const futureDate = new Date();
13 | const futureTime = futureDate.setTime(
14 | futureDate.getTime() + dismissableIntervalSeconds * 1000
15 | );
16 | const textColor = useColorModeValue('#6F7786', '#FFFFFF');
17 | const Icon = !!hasIcon && ;
18 |
19 | return (
20 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/get-color.js:
--------------------------------------------------------------------------------
1 | const primary = {
2 | default: '#3D3B39',
3 | _dark: '#FFFFFF',
4 | };
5 |
6 | const secondary = { default: '#FFFFFF', _dark: '#3D3B39' };
7 |
8 | export const getColor = (color = 'primary', colorMode) => {
9 | const isLight = colorMode === 'light';
10 | switch (color) {
11 | case 'secondary':
12 | return isLight ? secondary.default : secondary._dark;
13 | case 'primary':
14 | return isLight ? primary.default : primary._dark;
15 | default:
16 | return color;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/index.js:
--------------------------------------------------------------------------------
1 | export { AllDone } from './all-done/all-done.component';
2 | export { AlmostThere } from './almost-there/almost-there.component';
3 | export { Carousel } from './carousel/carousel.component';
4 | export { Slide1 } from './carousel/slides/Slide1.component';
5 | export { Slide3 } from './carousel/slides/Slide3.component';
6 | export { SeamlessUpgrade } from './seamless-upgrade/seamless-upgrade.component';
7 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/migration-view/migration-view.stories.js:
--------------------------------------------------------------------------------
1 | import { MigrationView } from './migration-view.component';
2 |
3 | export default {
4 | title: 'Nami Migration/State Flow',
5 | component: MigrationView,
6 | parameters: {
7 | layout: 'centered',
8 | actions: { argTypesRegex: '^on.*' },
9 | },
10 | };
11 |
12 | export const None = {
13 | args: {
14 | migrationState: 'none',
15 | hasWallet: true,
16 | },
17 | };
18 |
19 | export const WaitingForLace = {
20 | args: {
21 | migrationState: 'in-progress',
22 | isLaceInstalled: false,
23 | hasWallet: true,
24 | },
25 | };
26 |
27 | export const InProgress = {
28 | args: {
29 | migrationState: 'in-progress',
30 | isLaceInstalled: true,
31 | hasWallet: true,
32 | },
33 | };
34 |
35 | export const Completed = {
36 | args: {
37 | migrationState: 'completed',
38 | isLaceInstalled: true,
39 | hasWallet: true,
40 | },
41 | };
42 |
43 | export const NoWallet = {
44 | args: {
45 | migrationState: 'none',
46 | hasWallet: false,
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/no-wallet/no-wallet.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Slide } from '../slide.component';
3 | import { ReactComponent as LaceIcon } from '../../assets/lace-icon.svg';
4 | import { ReactComponent as BackpackImg } from '../../assets/backpack.svg';
5 | import { Box } from '@chakra-ui/react';
6 | import { useCaptureEvent } from '../../../../features/analytics/hooks';
7 | import { Events } from '../../../../features/analytics/events';
8 |
9 | export const NoWallet = ({ onAction, isDismissable, dismissibleSeconds }) => {
10 | const captureEvent = useCaptureEvent();
11 |
12 | return (
13 |
17 |
18 |
19 | }
20 | description="To create or import a wallet, proceed using the Lace extension."
21 | buttonText="Get started with Lace"
22 | buttonIcon={LaceIcon}
23 | onButtonClick={onAction}
24 | isDismissable={isDismissable}
25 | buttonOrientation={isDismissable ? 'column' : 'row'}
26 | dismissibleSeconds={dismissibleSeconds}
27 | showTerms={false}
28 | showFindOutMore
29 | onDismiss={async () =>
30 | captureEvent(Events.NamiMigrationDismissedNoWallet)
31 | }
32 | />
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/no-wallet/no-wallet.stories.js:
--------------------------------------------------------------------------------
1 | import { NoWallet } from './no-wallet.component';
2 |
3 | const meta = {
4 | title: 'Nami Migration/Screens/NoWallet',
5 | component: NoWallet,
6 | parameters: {
7 | layout: 'centered',
8 | },
9 | args: {
10 | isLaceInstalled: true,
11 | onAction: () => {},
12 | },
13 | };
14 |
15 | export default meta;
16 |
17 | export const Primary = {};
18 |
--------------------------------------------------------------------------------
/src/ui/lace-migration/components/text.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text as ChakraText, useColorMode } from '@chakra-ui/react';
3 | import { getColor } from './get-color';
4 |
5 | export const Text = ({ children, color, ...rest }) => {
6 | const { colorMode } = useColorMode();
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/ui/theme.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ChakraProvider, extendTheme } from '@chakra-ui/react';
3 | import './app/components/styles.css';
4 | import '@fontsource/ubuntu/latin.css';
5 |
6 | const colorMode = localStorage['chakra-ui-color-mode'];
7 |
8 | const inputSizes = {
9 | sm: {
10 | borderRadius: 'lg',
11 | },
12 | md: {
13 | borderRadius: 'lg',
14 | },
15 | };
16 |
17 | const Input = {
18 | sizes: {
19 | sm: {
20 | field: inputSizes.sm,
21 | addon: inputSizes.sm,
22 | },
23 | md: {
24 | field: inputSizes.md,
25 | addon: inputSizes.md,
26 | },
27 | },
28 | defaultProps: {
29 | focusBorderColor: 'teal.500',
30 | },
31 | };
32 |
33 | const Checkbox = {
34 | defaultProps: {
35 | colorScheme: 'teal',
36 | },
37 | };
38 |
39 | const Select = {
40 | defaultProps: {
41 | focusBorderColor: 'teal.500',
42 | },
43 | };
44 |
45 | const Button = {
46 | baseStyle: {
47 | borderRadius: 'lg',
48 | },
49 | };
50 |
51 | const Switch = {
52 | baseStyle: {
53 | track: {
54 | _focus: {
55 | boxShadow: 'none',
56 | },
57 | },
58 | },
59 | defaultProps: {
60 | colorScheme: 'teal',
61 | },
62 | };
63 |
64 | const theme = extendTheme({
65 | components: {
66 | Checkbox,
67 | Input,
68 | Select,
69 | Button,
70 | Switch,
71 | },
72 | config: {
73 | useSystemColorMode: colorMode ? false : true,
74 | },
75 | styles: {
76 | global: {
77 | body: {
78 | // width: POPUP_WINDOW.width + 'px',
79 | // height: POPUP_WINDOW.height + 'px',
80 | overflow: 'hidden',
81 | },
82 | },
83 | },
84 | fonts: {
85 | body: 'Ubuntu, sans-serif',
86 | },
87 | });
88 |
89 | const Theme = ({ children }) => {
90 | return {children};
91 | };
92 |
93 | export default Theme;
94 |
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/.gitignore:
--------------------------------------------------------------------------------
1 | target/
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "cardano-message-signing"
3 | version = "1.0.1"
4 | edition = "2018"
5 | authors = ["EMURGO"]
6 |
7 | [lib]
8 | crate-type = ["cdylib", "rlib"]
9 |
10 | [dependencies]
11 | base64-url = "1.4.8"
12 | byteorder = "1.4.3"
13 | cbor_event = "2.1.3"
14 | cryptoxide = "0.3.2"
15 | #curve25519-dalek = { "path" = "curve25519-dalek" }
16 | linked-hash-map = "0.5.3"
17 | hex = "0.4.0"
18 | pruefung = "0.2.1"
19 |
20 | # non-wasm
21 | [target.'cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))'.dependencies]
22 | noop_proc_macro = "0.3.0"
23 |
24 | # wasm
25 | [target.'cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))'.dependencies]
26 | wasm-bindgen = { version = "0.2.84", features=["serde-serialize"] }
27 |
28 | [profile.release]
29 | # Tell `rustc` to optimize for small code size.
30 | codegen-units = 1
31 | opt-level = "s"
32 | incremental = true
33 | lto = true
34 |
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 EMURGO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/cardano_message_signing_bg.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/wasm/cardano_message_signing/cardano_message_signing_bg.wasm
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/src/crypto.rs:
--------------------------------------------------------------------------------
1 | //use curve25519_dalek::FieldElement;
2 |
3 | // pub fn get_ed25519_x(xpub: &ed25519_bip32::XPub) -> Option<()> {
4 |
5 | // // let compressed_y = CompressedEdwardsY::from_slice(&bytes[..]);
6 | // // let edwards_point = compressed_y.decompress()?;
7 | // // let proj_point = edwards_point.to_projective();
8 | // //let Y = FieldElement::from_bytes(bytes);
9 | // None
10 | // }
11 |
12 | use cryptoxide::blake2b::Blake2b;
13 | use pruefung::fnv::fnv32::Fnv32a;
14 |
15 | pub (crate) fn blake2b224(data: &[u8]) -> [u8; 28] {
16 | let mut out = [0; 28];
17 | Blake2b::blake2b(&mut out, data, &[]);
18 | out
19 | }
20 |
21 | pub (crate) fn fnv32a(data: &[u8]) -> u32 {
22 | use core::hash::Hasher;
23 | let mut hasher = Fnv32a::default();
24 | hasher.write(data);
25 | hasher.finish() as u32
26 | }
27 |
28 | // #[cfg(test)]
29 | // mod tests {
30 | // use super::*;
31 |
32 | // #[test]
33 | // fn x_coord() {
34 | // let pub_bytes = [
35 | // 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a,
36 | // 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a
37 | // ];
38 | // let compr_y = curve25519_dalek::edwards::CompressedEdwardsY::from_slice(&pub_bytes[..]);
39 | // let x_computed = compr_y.compute_x_as_bytes().unwrap();
40 | // let x_expected = base64_url::decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo").unwrap();
41 | // println!("pub_bytes = {:?}", pub_bytes);
42 | // println!("x_expected = {:?}", x_expected);
43 | // println!("x_computed = {:?}", x_computed);
44 | // assert_eq!(pub_bytes, x_expected.as_ref());
45 | // }
46 | // }
--------------------------------------------------------------------------------
/src/wasm/cardano_message_signing/src/tests.rs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/input-output-hk/nami/e52e0bdb02eb1ec224db26f48b0192685a30f99e/src/wasm/cardano_message_signing/src/tests.rs
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node20/tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./build/",
5 | "sourceMap": true,
6 | "noImplicitAny": true,
7 | "module": "commonjs",
8 | "target": "es5",
9 | "jsx": "react",
10 | "allowJs": true,
11 | "moduleResolution": "node"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/utils/build.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'production';
3 | process.env.NODE_ENV = 'production';
4 | process.env.ASSET_PATH = '/';
5 |
6 | var webpack = require('webpack'),
7 | config = require('../webpack.config');
8 |
9 | delete config.chromeExtensionBoilerplate;
10 |
11 | config.mode = 'production';
12 |
13 | webpack(config, function (err, stats) {
14 | if (stats.hasErrors()) {
15 | console.log(
16 | stats.toString({
17 | colors: true,
18 | })
19 | );
20 | }
21 |
22 | if (err) {
23 | throw err;
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/utils/env.js:
--------------------------------------------------------------------------------
1 | // tiny wrapper with default env vars
2 | module.exports = {
3 | NODE_ENV: process.env.NODE_ENV || 'development',
4 | PORT: process.env.PORT || 3000,
5 | };
6 |
--------------------------------------------------------------------------------
/utils/webserver.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'development';
3 | process.env.NODE_ENV = 'development';
4 | process.env.ASSET_PATH = '/';
5 |
6 | var ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
7 | var WebpackDevServer = require('webpack-dev-server'),
8 | webpack = require('webpack'),
9 | config = require('../webpack.config'),
10 | env = require('./env'),
11 | path = require('path');
12 |
13 | var options = config.chromeExtensionBoilerplate || {};
14 | var excludeEntriesToHotReload = options.notHotReload || [];
15 |
16 | for (var entryName in config.entry) {
17 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) {
18 | config.entry[entryName] = [
19 | 'webpack-dev-server/client?http://localhost:' + env.PORT,
20 | 'webpack/hot/dev-server',
21 | ].concat(config.entry[entryName]);
22 | }
23 | }
24 |
25 | config.plugins = [
26 | new webpack.HotModuleReplacementPlugin(),
27 | new ReactRefreshWebpackPlugin(),
28 | ].concat(config.plugins || []);
29 |
30 | delete config.chromeExtensionBoilerplate;
31 |
32 | var compiler = webpack(config);
33 |
34 | var server = new WebpackDevServer(compiler, {
35 | https: false,
36 | hot: true,
37 | client: {
38 | overlay: false,
39 | },
40 | devMiddleware: {
41 | publicPath: `http://localhost:${env.PORT}/`,
42 | writeToDisk: true,
43 | },
44 | liveReload: false,
45 | port: env.PORT,
46 | static: {
47 | directory: path.join(__dirname, '../build'),
48 | },
49 | headers: {
50 | 'Access-Control-Allow-Origin': '*',
51 | },
52 | allowedHosts: 'all',
53 | });
54 |
55 | if (process.env.NODE_ENV === 'development' && module.hot) {
56 | module.hot.accept();
57 | }
58 |
59 | server.listen(env.PORT);
60 |
--------------------------------------------------------------------------------