├── src ├── common │ ├── utils │ │ ├── tx │ │ │ └── genDummyTransaction.ts │ │ ├── axios.d.ts │ │ ├── message │ │ │ └── constants.ts │ │ ├── ckb.ts │ │ ├── fee │ │ │ ├── calculateFee.test.ts │ │ │ └── calculateFee.ts │ │ ├── locale.ts │ │ ├── tests │ │ │ ├── lock.test.ts │ │ │ ├── token.test.ts │ │ │ ├── locale.test.ts │ │ │ ├── address.test.ts │ │ │ ├── formatters │ │ │ │ ├── currencyFormatter │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── fixtures.ts │ │ │ │ ├── CKBToShannonFormatter │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── fixtures.ts │ │ │ │ └── shannonToCKBFormatter │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── fixtures.ts │ │ │ ├── index.test.ts │ │ │ ├── deps.test.ts │ │ │ └── wallet.test.ts │ │ ├── lock.ts │ │ ├── index.ts │ │ ├── token.ts │ │ ├── transaction.ts │ │ ├── deps.ts │ │ ├── constants │ │ │ ├── networks.ts │ │ │ ├── typesInfo.ts │ │ │ └── locksInfo.ts │ │ ├── __mocks__ │ │ │ └── apis.ts │ │ └── wallet.ts │ ├── messageManager │ │ ├── IMessageManager.ts │ │ ├── index.ts │ │ ├── browserMessageManager.ts │ │ └── index.test.ts │ ├── contactManager │ │ ├── fixtures │ │ │ └── contacts.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── popup │ │ ├── index.ts │ │ └── popup.ts │ ├── publicKey │ │ ├── index.ts │ │ └── publicKey.test.ts │ └── networkManager │ │ ├── fixtures │ │ └── networks.ts │ │ └── index.test.ts ├── background │ ├── address │ │ ├── index.ts │ │ ├── init.ts │ │ └── tests │ │ │ └── init.test.ts │ ├── currentWallet │ │ ├── ICurrentWalletManager.ts │ │ ├── index.ts │ │ ├── currentWalletManager.ts │ │ ├── index.test.ts │ │ └── currentWalletHandler.ts │ ├── keyper │ │ ├── locks │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ └── lockWithSign.ts │ │ │ ├── secp256k1.ts │ │ │ └── tests │ │ │ │ ├── anypay.test.ts │ │ │ │ ├── secp256k1.test.ts │ │ │ │ └── keccak256.test.ts │ │ ├── tests │ │ │ ├── containerFactory.test.ts │ │ │ ├── walletManager.test.ts │ │ │ ├── setupKeyper.test.ts │ │ │ └── fixtures │ │ │ │ └── lockScript.ts │ │ ├── signProviders │ │ │ ├── secp256k1WithPrivateKey.test.ts │ │ │ ├── __mocks__ │ │ │ │ └── secp256k1WithPrivateKey.ts │ │ │ ├── secp256k1WithPrivateKey.ts │ │ │ ├── secp256k1.ts │ │ │ └── secp256k1.test.ts │ │ ├── containerFactory.ts │ │ ├── walletManager.ts │ │ ├── containerManager.ts │ │ └── setupKeyper.ts │ ├── messageHandlers │ │ ├── sendToPopup.ts │ │ ├── tests │ │ │ ├── getAddressInfo.test.ts │ │ │ ├── fixtures │ │ │ │ └── currentWallet.ts │ │ │ └── getLiveCells.test.ts │ │ ├── proxy.ts │ │ ├── getLiveCells.ts │ │ ├── index.ts │ │ └── getAddressInfo.ts │ ├── wallet │ │ ├── __mocks__ │ │ │ └── passwordEncryptor.ts │ │ ├── transaction │ │ │ ├── tests │ │ │ │ ├── formatters.test.ts │ │ │ │ ├── getLockTypeByCodeHash.test.ts │ │ │ │ ├── fixtures │ │ │ │ │ └── cells.ts │ │ │ │ └── secp256k1.test.ts │ │ │ └── getLockTypeByCodeHash.ts │ │ ├── passwordEncryptor.ts │ │ ├── tests │ │ │ ├── passwordEncryptor.test.ts │ │ │ ├── mnemonic │ │ │ │ └── index.test.ts │ │ │ ├── address.test.ts │ │ │ └── keystore.test.ts │ │ ├── fixtures │ │ │ └── wallets.ts │ │ └── address.ts │ └── transaction.ts ├── ui │ ├── public │ │ └── assets │ │ │ ├── ckb-128.png │ │ │ ├── ckb-32.png │ │ │ ├── logo-32.png │ │ │ └── logo-128.png │ ├── utils │ │ └── context.ts │ ├── styles │ │ ├── palette.scss │ │ ├── theme.ts │ │ ├── _var.scss │ │ ├── _mixin.scss │ │ ├── global.scss │ │ └── type.tsx │ ├── Components │ │ ├── TokenList │ │ │ ├── __mocks__ │ │ │ │ └── index.tsx │ │ │ ├── fixtures │ │ │ │ ├── udts.ts │ │ │ │ └── currentWallet.ts │ │ │ ├── component.test.tsx │ │ │ ├── index.test.tsx │ │ │ └── component.tsx │ │ ├── TxDetail │ │ │ ├── fixture.ts │ │ │ └── index.test.tsx │ │ ├── PrettyPrintJson │ │ │ ├── TXPreviewer.tsx │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── Title │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── TokenListItem │ │ │ ├── fixtures │ │ │ │ └── tokenInfo.ts │ │ │ └── index.test.tsx │ │ ├── AddressListItem │ │ │ ├── fixtures │ │ │ │ └── addressInfo.ts │ │ │ └── index.test.tsx │ │ ├── Modal │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── AppBar │ │ │ └── index.test.tsx │ │ ├── PageNav │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── TxList │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── NetworkSelector │ │ │ └── index.test.tsx │ │ ├── LanguageSelector │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ └── AddressList │ │ │ └── index.test.tsx │ ├── pages │ │ ├── Transaction │ │ │ └── fixtures │ │ │ │ ├── contacts.ts │ │ │ │ └── currentWallet.ts │ │ ├── Address │ │ │ ├── fixtures │ │ │ │ └── currentWallet.ts │ │ │ ├── index.tsx │ │ │ └── address.test.tsx │ │ ├── Sign │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── ManageUDTs │ │ │ ├── index.tsx │ │ │ ├── tests │ │ │ │ ├── index.test.tsx │ │ │ │ ├── list.test.tsx │ │ │ │ ├── create.test.tsx │ │ │ │ └── edit.test.tsx │ │ │ ├── Create.tsx │ │ │ ├── List.tsx │ │ │ ├── Edit.tsx │ │ │ └── Form.tsx │ │ ├── ImportPrivateKey │ │ │ ├── UploadFile.test.tsx │ │ │ └── UploadFile.tsx │ │ ├── ExportMnemonicSecond │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── MnemonicSetting │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ └── ExportPrivateKeySecond │ │ │ └── index.test.tsx │ └── index.tsx ├── background.html ├── popup.html ├── notification.html ├── config.test.ts ├── config.ts ├── tests │ ├── mnemonic.test.ts │ ├── addressScript.test.ts │ ├── keystore.test.ts │ ├── wallet.test.ts │ └── address.test.ts ├── manifest.json ├── types │ └── RPCMessage.d.ts └── contentScript │ └── contentScript.ts ├── codecov.yml.example ├── __mocks__ ├── browser-passworder.ts └── @nervosnetwork │ └── ckb-sdk-core.ts ├── .prettierrc.js ├── webpack.prod.js ├── .eslintignore ├── webpack.dev.js ├── .env.example ├── scripts ├── build-and-compress.sh └── release.sh ├── .vscode ├── settings.json └── launch.json ├── jest.e2e.config.js ├── config └── jest │ ├── cssTransform.js │ └── jest.setup.js ├── tsconfig.json ├── .eslintrc.js ├── jest.config.js ├── LICENSE ├── .travis.yml ├── README.md ├── .gitignore ├── e2e └── importMnemonic.test.ts └── webpack.common.js /src/common/utils/tx/genDummyTransaction.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codecov.yml.example: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: LONG_TOKEN_YOU_CAN_NOT_GUESS 3 | -------------------------------------------------------------------------------- /src/background/address/index.ts: -------------------------------------------------------------------------------- 1 | import init from './init'; 2 | 3 | export default { init }; 4 | -------------------------------------------------------------------------------- /src/ui/public/assets/ckb-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebase-network/synapse-extension/HEAD/src/ui/public/assets/ckb-128.png -------------------------------------------------------------------------------- /src/ui/public/assets/ckb-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebase-network/synapse-extension/HEAD/src/ui/public/assets/ckb-32.png -------------------------------------------------------------------------------- /src/ui/public/assets/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebase-network/synapse-extension/HEAD/src/ui/public/assets/logo-32.png -------------------------------------------------------------------------------- /src/ui/public/assets/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebase-network/synapse-extension/HEAD/src/ui/public/assets/logo-128.png -------------------------------------------------------------------------------- /src/ui/utils/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const AppContext = React.createContext({ network: 'testnet' }); 4 | -------------------------------------------------------------------------------- /src/ui/styles/palette.scss: -------------------------------------------------------------------------------- 1 | $primary: black; 2 | $link: black; 3 | $info: #8152fb; 4 | $success: #00cd85; 5 | $warning: #ffb736; 6 | $danger: #fd396a; 7 | -------------------------------------------------------------------------------- /__mocks__/browser-passworder.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | encrypt: () => Promise.resolve('encrypt'), 3 | decrypt: () => Promise.resolve('decrypt'), 4 | }; 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | singleQuote: true, 4 | arrowParens: "always", 5 | printWidth: 100, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /src/background/currentWallet/ICurrentWalletManager.ts: -------------------------------------------------------------------------------- 1 | export default interface ICurrentWalletManager { 2 | setCurrentWallet(publicKey: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/common/messageManager/IMessageManager.ts: -------------------------------------------------------------------------------- 1 | export default interface IMessageManager { 2 | addListener(listener: any): void; 3 | 4 | removeListener(listener: any): void; 5 | } 6 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production" 6 | }); 7 | -------------------------------------------------------------------------------- /src/common/messageManager/index.ts: -------------------------------------------------------------------------------- 1 | import BrowserMessageManager from './browserMessageManager'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export { BrowserMessageManager }; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage -------------------------------------------------------------------------------- /src/common/utils/axios.d.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | declare module 'axios' { 4 | export interface AxiosResponse extends Promise { 5 | errCode; 6 | errMsg; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/common/utils/message/constants.ts: -------------------------------------------------------------------------------- 1 | export const BACKGROUND_PORT = 'background'; 2 | export const POPUP_PORT = 'popup'; 3 | export const WEB_PAGE = 'injectedScript'; 4 | export const CONTENT_SCRIPT = 'contentScript'; 5 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | devtool: "inline-source-map" 7 | }); 8 | -------------------------------------------------------------------------------- /src/background/currentWallet/index.ts: -------------------------------------------------------------------------------- 1 | import CurrentWalletHandler from './currentWalletHandler'; 2 | import CurrentWalletManager from './currentWalletManager'; 3 | 4 | export { CurrentWalletHandler, CurrentWalletManager }; 5 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/__mocks__/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | const Component = (): ReactElement => { 4 | return
Token list mock
; 5 | }; 6 | 7 | export default Component; 8 | -------------------------------------------------------------------------------- /src/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Synapse extension background 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/fixtures/udts.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | decimal: '3', 4 | name: 'aaa', 5 | symbol: 'TLT', 6 | typeHash: '0x1c5f32c5efb08ac256bd9413c30eef7420ae54f30ff11075967b89570326c295', 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/ui/pages/Transaction/fixtures/contacts.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', name: 'Alice' }, 3 | { address: 'ckt1qyqxpmg9n822t3nl6ff8wfp6cy4ely230dss74qff7', name: 'Bob' }, 4 | ]; 5 | -------------------------------------------------------------------------------- /src/ui/Components/TxDetail/fixture.ts: -------------------------------------------------------------------------------- 1 | export const tx = { 2 | status: 'Confirmed', 3 | amount: 100, 4 | fee: 0.001, 5 | inputs: [], 6 | outputs: [], 7 | hash: '0x123', 8 | blockNum: 11, 9 | timestamp: undefined, 10 | }; 11 | -------------------------------------------------------------------------------- /src/background/keyper/locks/index.ts: -------------------------------------------------------------------------------- 1 | import Keccak256LockScript from './keccak256'; 2 | import AnypayLockScript from './anypay'; 3 | import Secp256k1LockScript from './secp256k1'; 4 | 5 | export { Keccak256LockScript, AnypayLockScript, Secp256k1LockScript }; 6 | -------------------------------------------------------------------------------- /src/ui/styles/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | 3 | const theme = createMuiTheme({ 4 | typography: { 5 | button: { 6 | textTransform: 'none', 7 | }, 8 | }, 9 | }); 10 | 11 | export default theme; 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CKB_RPC_ENDPOINT=https://ckb-testnet.rebase.network/rpc 2 | CACHE_LAYER_ENDPOINT=https://testnet.getsynapse.io/api 3 | CKB_RPC_ENDPOINT_MAINNET=https://ckb-mainnet.rebase.network/rpc 4 | CACHE_LAYER_ENDPOINT_MAINNET=https://mainnet.getsynapse.io/api 5 | MODE=DEV 6 | -------------------------------------------------------------------------------- /src/background/messageHandlers/sendToPopup.ts: -------------------------------------------------------------------------------- 1 | import createPopup from '@common/popup'; 2 | 3 | export default async (port, message) => { 4 | const popup = await createPopup(); 5 | 6 | await popup.waitTabLoaded(); 7 | 8 | browser.runtime.sendMessage(message); 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/contactManager/fixtures/contacts.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'Alice', 4 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 5 | }, 6 | { 7 | name: 'Bob', 8 | address: 'ckt1qyqxpmg9n822t3nl6ff8wfp6cy4ely230dss74qff7', 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/ui/styles/_var.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: "Nunito Sans", 2 | sans-serif; 3 | $body-color: black; 4 | 5 | $popup-size-width: 360px; 6 | $popup-size-height: 600px; 7 | 8 | $header-height: 64px; 9 | 10 | $layout-padding: 12px; 11 | $layout-margin: 12px; 12 | 13 | $main-card-padding: 16px; -------------------------------------------------------------------------------- /src/ui/styles/_mixin.scss: -------------------------------------------------------------------------------- 1 | @import "var"; 2 | 3 | @mixin main-card { 4 | padding: $main-card-padding; 5 | margin-bottom: $layout-padding; 6 | box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.03), 0 2px 15px 0 rgba(0, 0, 0, 0.08); 7 | background-color: white; 8 | border-radius: 16px; 9 | } 10 | -------------------------------------------------------------------------------- /src/background/keyper/tests/containerFactory.test.ts: -------------------------------------------------------------------------------- 1 | import ContainerFactory from '../containerFactory'; 2 | 3 | describe('container factory', () => { 4 | it('create container', () => { 5 | const container = ContainerFactory.createContainer(); 6 | expect(container).not.toBeNull(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/utils/ckb.ts: -------------------------------------------------------------------------------- 1 | import CKB from '@nervosnetwork/ckb-sdk-core'; 2 | import NetworkManager from '@common/networkManager'; 3 | 4 | const getCKB = async () => { 5 | const { nodeURL } = await NetworkManager.getCurrentNetwork(); 6 | return new CKB(nodeURL); 7 | }; 8 | 9 | export default getCKB; 10 | -------------------------------------------------------------------------------- /scripts/build-and-compress.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | rm -rf dist synapse-extension 4 | 5 | yarn build 6 | 7 | mv dist synapse-extension 8 | 9 | zip -q -r synapse-extension.zip synapse-extension 10 | 11 | rm -rf dist 12 | 13 | # 文件的校验和 14 | shasum -a 256 synapse-extension.zip | tee synapse-extension.asc 15 | -------------------------------------------------------------------------------- /src/common/utils/fee/calculateFee.test.ts: -------------------------------------------------------------------------------- 1 | import { rawTx } from '@common/fixtures/tx'; 2 | import calculateTxFee from './calculateFee'; 3 | 4 | describe('calculateTxFee', () => { 5 | it('should calculate Tx Fee', () => { 6 | const result = calculateTxFee(rawTx); 7 | expect(result).toEqual('0x6db'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/utils/locale.ts: -------------------------------------------------------------------------------- 1 | export const getDefaultLanguage = (): string => { 2 | const languages = ['en', 'zh']; 3 | const language = localStorage.getItem('language') || navigator.language.split(/[-_]/)[0]; 4 | 5 | if (!languages.includes(language)) { 6 | return 'en'; // default english 7 | } 8 | 9 | return language; 10 | }; 11 | -------------------------------------------------------------------------------- /src/ui/Components/PrettyPrintJson/TXPreviewer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface AppProps { 4 | tx?: CKBComponents.RawTransactionToSign | any; 5 | } 6 | 7 | export default (props: AppProps) => { 8 | const { tx } = props; 9 | 10 | return
{JSON.stringify(tx, null, 2)}
; 11 | }; 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": false, 4 | }, 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "typescriptreact" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/common/utils/tests/lock.test.ts: -------------------------------------------------------------------------------- 1 | import lockUtils from '../lock'; 2 | import locksInfo from '../constants/locksInfo'; 3 | 4 | describe('lock utils', () => { 5 | it('should know if it is anypay script', () => { 6 | const result = lockUtils.isAnypay(locksInfo.mainnet.anypay.codeHash); 7 | expect(result).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "name": "Chrome", 5 | "type": "chrome", 6 | "request": "launch", 7 | "url": "http://localhost:3000", 8 | "webRoot": "${workspaceFolder}/src", 9 | "sourceMapPathOverrides": { 10 | "webpack:///src/*": "${webRoot}/*" 11 | } 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /jest.e2e.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line prettier/prettier 2 | const { 3 | defaults: tsPreset 4 | } = require('ts-jest/presets'); 5 | 6 | module.exports = { 7 | transform: { 8 | ...tsPreset.transform, 9 | }, 10 | testMatch: ['/e2e/**/*.test.ts'], 11 | setupFilesAfterEnv: ['expect-puppeteer'], 12 | preset: 'jest-puppeteer', 13 | }; -------------------------------------------------------------------------------- /src/common/utils/tests/token.test.ts: -------------------------------------------------------------------------------- 1 | import { aggregateUDT, UDTInfo } from '../token'; 2 | import { udtsLiveCells, udtsCapacity } from './fixtures/token'; 3 | 4 | describe('utils: token', () => { 5 | it('should aggregate udts correctly', () => { 6 | const result = aggregateUDT(udtsLiveCells as UDTInfo[]); 7 | expect(result).toEqual(udtsCapacity); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/background/wallet/__mocks__/passwordEncryptor.ts: -------------------------------------------------------------------------------- 1 | export async function encrypt(privKey: Buffer, password: string) { 2 | const result = 'encrypt'; 3 | return result; 4 | } 5 | 6 | export async function decrypt(input: string, password: string) { 7 | try { 8 | const result = 'decrypt'; 9 | return result; 10 | } catch (error) { 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Synapse extension 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Synapse extension notification 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/background/currentWallet/currentWalletManager.ts: -------------------------------------------------------------------------------- 1 | import { setCurrentWallet } from '@background/keyper/keyperwallet'; 2 | import ICurrentWalletManager from './ICurrentWalletManager'; 3 | 4 | export default class implements ICurrentWalletManager { 5 | // eslint-disable-next-line class-methods-use-this 6 | async setCurrentWallet(publicKey: string) { 7 | setCurrentWallet(publicKey); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/background/wallet/transaction/tests/formatters.test.ts: -------------------------------------------------------------------------------- 1 | import { ckbToshannon } from '@src/common/utils/formatters'; 2 | 3 | jest.unmock('@nervosnetwork/ckb-sdk-core'); 4 | 5 | describe('Formatter Test', () => { 6 | it('test capacity value', () => { 7 | const capacity = '0.0003'; 8 | const result = ckbToshannon(Number(capacity)); 9 | expect(result).toBe(BigInt(30000)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/background/keyper/signProviders/secp256k1WithPrivateKey.test.ts: -------------------------------------------------------------------------------- 1 | import { privateKey, message, signedMessageBeforeSerialize } from '@common/fixtures/tx'; 2 | import { sign } from './secp256k1WithPrivateKey'; 3 | 4 | describe('Sign tx with secp256k1 lock', () => { 5 | it('sign a tx hash', () => { 6 | const signedMsg = sign(privateKey, message); 7 | expect(signedMsg).toBe(signedMessageBeforeSerialize); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/messageManager/browserMessageManager.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import IMessageManager from './IMessageManager'; 3 | 4 | export default class implements IMessageManager { 5 | addListener(listener: any) { 6 | return browser.runtime.onMessage.addListener(listener); 7 | } 8 | 9 | removeListener(listener: any) { 10 | return browser.runtime.onMessage.removeListener(listener); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/utils/lock.ts: -------------------------------------------------------------------------------- 1 | import { NETWORK_TYPES } from '@common/utils/constants/networks'; 2 | import locksInfo from './constants/locksInfo'; 3 | 4 | const { testnet, mainnet } = NETWORK_TYPES; 5 | 6 | const isAnypay = (codeHash: string) => { 7 | return ( 8 | [locksInfo[testnet].anypay.codeHash, locksInfo[mainnet].anypay.codeHash].indexOf(codeHash) !== 9 | -1 10 | ); 11 | }; 12 | 13 | export default { 14 | isAnypay, 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/utils/tests/locale.test.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultLanguage } from '../locale'; 2 | 3 | describe('locale', () => { 4 | it('getDefaultLanguage', () => { 5 | const result = getDefaultLanguage(); 6 | expect(result).toEqual('en'); 7 | }); 8 | it('getDefaultLanguage return en', () => { 9 | localStorage.setItem('language', 'abc'); 10 | const result = getDefaultLanguage(); 11 | expect(result).toEqual('en'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/common/utils/tests/address.test.ts: -------------------------------------------------------------------------------- 1 | import { getAddressInfo } from '@src/common/utils/apis'; 2 | 3 | jest.mock('@common/utils/apis'); 4 | 5 | describe('address util test', () => { 6 | it('should return value for address balance', async () => { 7 | const address = 'ckt1qyq02llz4hpvl3sz9wkmt7qqh0397x2cdegsem7ykn'; 8 | const addressInfo = await getAddressInfo(address); 9 | expect(typeof addressInfo.capacity).toEqual('string'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/ui/Components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface AppProps { 4 | title: string; 5 | testId: string; 6 | } 7 | 8 | interface AppState {} 9 | 10 | export default class extends React.Component { 11 | constructor(props: AppProps, state: AppState) { 12 | super(props, state); 13 | } 14 | 15 | render() { 16 | return

{this.props.title}

; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config.test.ts: -------------------------------------------------------------------------------- 1 | import config from './config'; 2 | 3 | describe('config', () => { 4 | it('should have config', () => { 5 | expect(typeof config.CKB_RPC_ENDPOINT).toBe('string'); 6 | expect(typeof config.CACHE_LAYER_ENDPOINT).toBe('string'); 7 | expect(typeof config.CKB_RPC_ENDPOINT_MAINNET).toBe('string'); 8 | expect(typeof config.CACHE_LAYER_ENDPOINT_MAINNET).toBe('string'); 9 | expect(typeof config.isProduction).toBe('boolean'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/ui/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import 'palette'; 2 | @import 'var'; 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | html { 9 | min-width: $popup-size-width; 10 | height: $popup-size-height; 11 | margin: 0 auto; 12 | overflow: auto; 13 | } 14 | 15 | body { 16 | width: 100vw; 17 | height: 100vh; 18 | background-color: #efefef; 19 | margin: 0; 20 | } 21 | 22 | #popup { 23 | width: 100vw; 24 | height: 100vh; 25 | overflow-x: hidden; 26 | background: white; 27 | } -------------------------------------------------------------------------------- /src/background/keyper/containerFactory.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '@keyper/container'; 2 | import { SignatureAlgorithm } from '@keyper/specs'; 3 | import signProvider from './signProviders/secp256k1'; 4 | 5 | const containerFactory = { 6 | createContainer: () => { 7 | return new Container([ 8 | { 9 | algorithm: SignatureAlgorithm.secp256k1, 10 | provider: signProvider, 11 | }, 12 | ]); 13 | }, 14 | }; 15 | 16 | export default containerFactory; 17 | -------------------------------------------------------------------------------- /src/background/transaction.ts: -------------------------------------------------------------------------------- 1 | import { signTx } from '@background/keyper/keyperwallet'; 2 | 3 | export const signTxFromMsg = async (request) => { 4 | const { currentWallet } = await browser.storage.local.get(['currentWallet']); 5 | const { 6 | data: { tx: rawTx, meta }, 7 | password, 8 | } = request; 9 | const config = meta?.config || { index: 0, length: -1 }; 10 | const signedTx = await signTx(currentWallet?.lock, password.trim(), rawTx, config); 11 | return signedTx; 12 | }; 13 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/currencyFormatter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { currencyFormatter } from '@src/common/utils/formatters'; 2 | import fixtures from './fixtures'; 3 | 4 | const fixtureTable = fixtures.map(({ value, expected }) => [value, expected]); 5 | 6 | describe('Verify currency formatter', () => { 7 | test.each(fixtureTable)('%j => %s', (value: any, expected: string) => { 8 | expect(currencyFormatter(value.shannons, value.unit, value.exchange)).toBe(expected); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/background/address/init.ts: -------------------------------------------------------------------------------- 1 | import { getAddressList } from '@background/keyper/keyperwallet'; 2 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 3 | 4 | export default () => { 5 | browser.runtime.onMessage.addListener(async (request) => { 6 | if (request.type !== MESSAGE_TYPE.REQUEST_ADDRESS_LIST) return; 7 | const addressList = await getAddressList(); 8 | browser.runtime.sendMessage({ 9 | data: addressList, 10 | type: MESSAGE_TYPE.ADDRESS_LIST, 11 | }); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/ui/Components/TokenListItem/fixtures/tokenInfo.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | ckb: 28400000000, 4 | udt: 2923332, 5 | decimal: '3', 6 | name: 'Love Lina Token', 7 | symbol: 'TLT', 8 | typeHash: '0x1c5f32c5efb08ac256bd9413c30eef7420ae54f30ff11075967b89570326c295', 9 | }, 10 | { 11 | ckb: 14200000000, 12 | udt: 10000, 13 | typeHash: '0xa8b69151db2de6031203da9189c955ddb9c311fa7557f3f6e583d149b6681301', 14 | }, 15 | { ckb: 49400000000, udt: 0, typeHash: 'null' }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/ui/Components/AddressListItem/fixtures/addressInfo.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | lock: '0x7de181642f1d4e1cd0e1a8b5d00435af110f7fb8748c82c183f96c27f6d0f24f', 3 | script: { 4 | args: '0x60ed0599d4a5c67fd25277243ac12b9f91517b61', 5 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 6 | hashType: 'type', 7 | }, 8 | type: 'Secp256k1', 9 | publicKey: '0x0395d5fbcb6b1db6c4217f4758abad7d8d950d23d9eba27c5f3fb9432e4b3b508c', 10 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 11 | amount: 100, 12 | }; 13 | -------------------------------------------------------------------------------- /src/ui/styles/type.tsx: -------------------------------------------------------------------------------- 1 | export type Size = 'small' | 'default' | 'normal' | 'medium' | 'large'; 2 | 3 | export function getSizeClass(size?: Size): string { 4 | if (size === undefined || size === 'default') { 5 | return ''; 6 | } 7 | return 'is-' + size; 8 | } 9 | 10 | export type Color = 'primary' | 'default' | 'link' | 'info' | 'success' | 'warning' | 'danger'; 11 | 12 | export function getColorClass(color?: Color): string { 13 | if (color === undefined || color === 'default') { 14 | return ''; 15 | } 16 | return 'is-' + color; 17 | } 18 | -------------------------------------------------------------------------------- /src/background/messageHandlers/tests/getAddressInfo.test.ts: -------------------------------------------------------------------------------- 1 | import currentWallet from './fixtures/currentWallet'; 2 | import getAddressInfo from '../getAddressInfo'; 3 | 4 | jest.mock('@common/utils/apis'); 5 | 6 | describe('getAddressInfo', () => { 7 | it('should able to get address info', async () => { 8 | await browser.storage.local.set({ 9 | currentWallet, 10 | }); 11 | const port = { 12 | postMessage: (message) => { 13 | expect(message).not.toBeNull(); 14 | }, 15 | }; 16 | getAddressInfo(port); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | get CKB_RPC_ENDPOINT(): string { 3 | return process.env.CKB_RPC_ENDPOINT; 4 | }, 5 | 6 | get CACHE_LAYER_ENDPOINT(): string { 7 | return process.env.CACHE_LAYER_ENDPOINT; 8 | }, 9 | 10 | get CKB_RPC_ENDPOINT_MAINNET(): string { 11 | return process.env.CKB_RPC_ENDPOINT_MAINNET; 12 | }, 13 | 14 | get CACHE_LAYER_ENDPOINT_MAINNET(): string { 15 | return process.env.CACHE_LAYER_ENDPOINT_MAINNET; 16 | }, 17 | 18 | get isProduction(): boolean { 19 | return process.env.MODE !== 'DEV'; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/background/keyper/signProviders/__mocks__/secp256k1WithPrivateKey.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses } from '@src/tests/fixture/address'; 2 | 3 | export const padToEven = (value) => { 4 | let a = value; 5 | if (typeof a !== 'string') { 6 | throw new Error(`value must be string, is currently ${typeof a}, while padToEven.`); 7 | } 8 | if (a.length % 2) { 9 | a = `0${a}`; 10 | } 11 | return a; 12 | }; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | export const sign = (privateKey, message) => { 16 | return aliceAddresses.privateKey; 17 | }; 18 | -------------------------------------------------------------------------------- /src/background/wallet/passwordEncryptor.ts: -------------------------------------------------------------------------------- 1 | import passworder from 'browser-passworder'; 2 | 3 | export async function encrypt(privKey: Buffer, password: string) { 4 | let result; 5 | try { 6 | result = await passworder.encrypt(password, privKey); 7 | } catch (error) { 8 | // do nothing 9 | } 10 | return result; 11 | } 12 | 13 | export async function decrypt(input: string, password: string) { 14 | let result; 15 | try { 16 | result = await passworder.decrypt(password, input); 17 | } catch (error) { 18 | // do nothing 19 | } 20 | return result; 21 | } 22 | -------------------------------------------------------------------------------- /src/background/wallet/transaction/getLockTypeByCodeHash.ts: -------------------------------------------------------------------------------- 1 | import locksInfo, { NETWORKS } from '@common/utils/constants/locksInfo'; 2 | import { LockType } from '@common/utils/constants'; 3 | 4 | const mapping = new Map(); 5 | NETWORKS.forEach((networkName) => { 6 | mapping.set(locksInfo[networkName].secp256k1.codeHash, LockType.Secp256k1); 7 | mapping.set(locksInfo[networkName].keccak256.codeHash, LockType.Keccak256); 8 | mapping.set(locksInfo[networkName].anypay.codeHash, LockType.AnyPay); 9 | }); 10 | 11 | export default (codeHash: string): LockType => mapping.get(codeHash); 12 | -------------------------------------------------------------------------------- /src/background/keyper/locks/interfaces/lockWithSign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Script, 3 | RawTransaction, 4 | Config, 5 | SignProvider, 6 | SignContext, 7 | CellDep, 8 | SignatureAlgorithm, 9 | } from '@keyper/specs'; 10 | 11 | export default interface LockWithSign { 12 | deps: () => CellDep[]; 13 | signatureAlgorithm: () => SignatureAlgorithm; 14 | setProvider: (provider: SignProvider) => void; 15 | script: (publicKey: string) => Script; 16 | sign: ( 17 | context: SignContext, 18 | rawTxParam: RawTransaction, 19 | configParam: Config, 20 | ) => Promise; 21 | } 22 | -------------------------------------------------------------------------------- /src/background/messageHandlers/proxy.ts: -------------------------------------------------------------------------------- 1 | import { getLastWindowId } from '@common/popup'; 2 | 3 | export const sendToWebPage = (message) => { 4 | const sendToContentScript = async (tabs) => { 5 | // send back reponse to web page 6 | if (tabs[0]?.id) { 7 | browser.tabs.sendMessage(tabs[0]?.id, message); 8 | const windowId = await getLastWindowId(); 9 | browser.windows.remove(windowId); 10 | } 11 | }; 12 | browser.tabs 13 | .query({ currentWindow: false, active: true }) 14 | .then(sendToContentScript, console.error); 15 | }; 16 | 17 | export default sendToWebPage; 18 | -------------------------------------------------------------------------------- /src/ui/pages/Address/fixtures/currentWallet.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 3 | lock: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 4 | lockHash: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 5 | publicKey: '0x021b30b3047a645d8b6c10c513b767a3e08efa1a53df5f81bcb37af3c8c8358ae9', 6 | script: { 7 | args: '0x8eb4d75815e2207bd29a0b3651523e5a48e8fd34', 8 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 9 | hashType: 'type', 10 | }, 11 | type: 'Secp256k1', 12 | }; 13 | -------------------------------------------------------------------------------- /src/ui/pages/Transaction/fixtures/currentWallet.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 3 | lock: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 4 | lockHash: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 5 | publicKey: '0x021b30b3047a645d8b6c10c513b767a3e08efa1a53df5f81bcb37af3c8c8358ae9', 6 | script: { 7 | args: '0x8eb4d75815e2207bd29a0b3651523e5a48e8fd34', 8 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 9 | hashType: 'type', 10 | }, 11 | type: 'Secp256k1', 12 | }; 13 | -------------------------------------------------------------------------------- /src/background/wallet/tests/passwordEncryptor.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses, aliceWallet } from '@src/tests/fixture/address'; 2 | import { encrypt, decrypt } from '../passwordEncryptor'; 3 | 4 | describe('passwordEncryptor', () => { 5 | const { privateKey } = aliceAddresses; 6 | const pwd = '111111'; 7 | it('encrypt', async () => { 8 | const result = await encrypt(Buffer.from(privateKey), pwd); 9 | expect(result).toEqual('encrypt'); 10 | }); 11 | it('encrypt', async () => { 12 | const result = await decrypt(pwd, aliceWallet.keystore); 13 | expect(result).toEqual('decrypt'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/fixtures/currentWallet.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 3 | lock: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 4 | lockHash: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 5 | publicKey: '0x021b30b3047a645d8b6c10c513b767a3e08efa1a53df5f81bcb37af3c8c8358ae9', 6 | script: { 7 | args: '0x8eb4d75815e2207bd29a0b3651523e5a48e8fd34', 8 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 9 | hashType: 'type', 10 | }, 11 | type: 'Secp256k1', 12 | }; 13 | -------------------------------------------------------------------------------- /src/background/messageHandlers/tests/fixtures/currentWallet.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | address: 'ckt1qyqgadxhtq27ygrm62dqkdj32gl95j8gl56qum0yyn', 3 | lock: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 4 | lockHash: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 5 | publicKey: '0x021b30b3047a645d8b6c10c513b767a3e08efa1a53df5f81bcb37af3c8c8358ae9', 6 | script: { 7 | args: '0x8eb4d75815e2207bd29a0b3651523e5a48e8fd34', 8 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 9 | hashType: 'type', 10 | }, 11 | type: 'Secp256k1', 12 | }; 13 | -------------------------------------------------------------------------------- /src/background/messageHandlers/tests/getLiveCells.test.ts: -------------------------------------------------------------------------------- 1 | import currentWallet from './fixtures/currentWallet'; 2 | import getLiveCells from '../getLiveCells'; 3 | 4 | jest.mock('@common/utils/apis'); 5 | 6 | describe('getLiveCells', () => { 7 | it('should able to get live cells', async () => { 8 | await browser.storage.local.set({ 9 | currentWallet, 10 | }); 11 | const port = { 12 | postMessage: (message) => { 13 | expect(message).not.toBeNull(); 14 | }, 15 | }; 16 | const data = { 17 | lockHash: currentWallet.lock, 18 | }; 19 | getLiveCells(port, data); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/background/keyper/tests/walletManager.test.ts: -------------------------------------------------------------------------------- 1 | import wallets from '@src/background/wallet/fixtures/wallets'; 2 | import WalletManager from '../walletManager'; 3 | 4 | describe('wallet manager', () => { 5 | const manager = WalletManager.getInstance(); 6 | const publicKeys = wallets.map((wallet) => wallet.publicKey); 7 | 8 | it('should able to get all public keys', async () => { 9 | await browser.storage.local.set({ 10 | publicKeys, 11 | }); 12 | 13 | const itsPublicKeys = await manager.getAllPublicKeys(); 14 | expect(itsPublicKeys).toEqual(publicKeys); 15 | expect(itsPublicKeys).toHaveLength(3); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/background/keyper/walletManager.ts: -------------------------------------------------------------------------------- 1 | export default class Singleton { 2 | private static instance: Singleton; 3 | 4 | private wallets = []; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | private constructor() {} 8 | 9 | private thisStorage = browser.storage.local; 10 | 11 | static getInstance(): Singleton { 12 | if (!Singleton.instance) { 13 | Singleton.instance = new Singleton(); 14 | } 15 | return Singleton.instance; 16 | } 17 | 18 | async getAllPublicKeys() { 19 | const { publicKeys = [] } = await this.thisStorage.get('publicKeys'); 20 | return publicKeys; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ui/Components/Title/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import Title from './index'; 5 | 6 | describe('Title component', () => { 7 | let tree; 8 | beforeEach(() => { 9 | tree = render(); 10 | }); 11 | 12 | it('should render title', async () => { 13 | const { getByTestId, container } = tree; 14 | 15 | const title = getByTestId('test-title'); 16 | expect(container).toContainElement(title); 17 | expect(title).toHaveTextContent('test title'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/common/messageManager/index.test.ts: -------------------------------------------------------------------------------- 1 | import { BrowserMessageManager } from '@common/messageManager'; 2 | 3 | describe('message manager', () => { 4 | it('should add listener', () => { 5 | const manager = new BrowserMessageManager(); 6 | const listener = jest.fn(); 7 | manager.addListener(listener); 8 | expect(browser.runtime.onMessage.addListener).toHaveBeenCalledWith(listener); 9 | }); 10 | it('should remove listener', () => { 11 | const manager = new BrowserMessageManager(); 12 | const listener = jest.fn(); 13 | manager.removeListener(listener); 14 | expect(browser.runtime.onMessage.removeListener).toHaveBeenCalledWith(listener); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/tests/mnemonic.test.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | // import Address, { AddressType, AddressPrefix } from './address' 4 | // import Keychain, { privateToPublic } from './keychain' 5 | import { entropyToMnemonic } from '@background/wallet/mnemonic'; 6 | 7 | // Generate 12 words mnemonic code 8 | const generateMnemonic = () => { 9 | const entropySize = 16; 10 | const entropy = crypto.randomBytes(entropySize).toString('hex'); 11 | return entropyToMnemonic(entropy); 12 | }; 13 | 14 | describe('mnemonic test', () => { 15 | it('generate mnemonic', () => { 16 | const mnemonic = generateMnemonic(); 17 | expect(mnemonic.split(' ')).toHaveLength(12); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/common/popup/index.ts: -------------------------------------------------------------------------------- 1 | import Popup from './popup'; 2 | 3 | const defaultOptions = { 4 | url: 'notification.html', 5 | type: 'popup' as 'popup', 6 | width: 360, 7 | height: 590, 8 | }; 9 | 10 | const createPopup = async (options = {}) => { 11 | const finalParams = { ...defaultOptions, ...options }; 12 | const popupWindow = await browser.windows.create(finalParams); 13 | await browser.storage.local.set({ lastWindowId: popupWindow.id }); 14 | return new Popup(popupWindow); 15 | }; 16 | 17 | export const getLastWindowId = async () => { 18 | const { lastWindowId } = await browser.storage.local.get('lastWindowId'); 19 | return lastWindowId; 20 | }; 21 | 22 | export default createPopup; 23 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/CKBToShannonFormatter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { CapacityUnit } from '@src/common/utils/constants'; 2 | import { CKBToShannonFormatter } from '@src/common/utils/formatters'; 3 | import fixtures from './fixtures'; 4 | 5 | const fixtureTable: [ 6 | string, 7 | string | CapacityUnit, 8 | string, 9 | ][] = fixtures.map(({ ckb: { amount, unit }, expected }) => [amount, unit, expected]); 10 | 11 | describe('Verify CKB to Shannons formatter', () => { 12 | test.each(fixtureTable)( 13 | '%s %s => %s shannons', 14 | (amount: string, unit: CapacityUnit, expected: string) => { 15 | expect(CKBToShannonFormatter(amount, unit)).toBe(expected); 16 | }, 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "rootDir": ".", 7 | "outDir": "src/dist/js", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@src/*": ["src/*"], 11 | "@ui/*": ["src/ui/*"], 12 | "@background/*": ["src/background/*"], 13 | "@contentScript/*": ["src/contentScript/*"], 14 | "@common/*": ["src/common/*"], 15 | "@utils/*": ["src/common/utils/*"] 16 | }, 17 | "sourceMap": true, 18 | "jsx": "react", 19 | "lib": ["es2015", "dom"], 20 | "esModuleInterop": true, 21 | "experimentalDecorators": true, 22 | "allowSyntheticDefaultImports": true 23 | } 24 | } -------------------------------------------------------------------------------- /src/common/publicKey/index.ts: -------------------------------------------------------------------------------- 1 | import * as ckbUtils from '@nervosnetwork/ckb-sdk-utils'; 2 | import { Script } from '@keyper/specs'; 3 | 4 | export default class PublicKey { 5 | publicKey: string; 6 | 7 | constructor(publicKey: string) { 8 | this.publicKey = publicKey.startsWith('0x') ? publicKey.substr(2) : publicKey; 9 | } 10 | 11 | public getBlake160 = (): string => { 12 | return `0x${ckbUtils.blake160(`0x${this.publicKey}`, 'hex')}`; 13 | }; 14 | 15 | public publicKeyHash = (): string => { 16 | return this.getBlake160(); 17 | }; 18 | 19 | public getLockHash = (script: Script): string => { 20 | const lockHash = ckbUtils.scriptToHash(script); 21 | 22 | return lockHash; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { IntlProvider } from 'react-intl'; 4 | 5 | import zh from '@common/locales/zh'; 6 | import en from '@common/locales/en'; 7 | import App from '@ui/App'; 8 | import { getDefaultLanguage } from '@src/common/utils/locale'; 9 | 10 | const messages = { 11 | en, 12 | zh, 13 | }; 14 | 15 | declare global { 16 | interface Window { 17 | ckb: any; 18 | } 19 | } 20 | 21 | chrome.tabs.query({ active: true, currentWindow: true }, (tab) => { 22 | ReactDOM.render( 23 | <IntlProvider locale={getDefaultLanguage()} messages={messages[getDefaultLanguage()]}> 24 | <App /> 25 | </IntlProvider>, 26 | document.getElementById('popup'), 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { utf8ToBytes, bytesToHex, hexToBytes } from '@nervosnetwork/ckb-sdk-utils/lib'; 2 | 3 | export function textToHex(text) { 4 | let result = text.trim(); 5 | if (result.startsWith('0x')) { 6 | return result; 7 | } 8 | const bytes = utf8ToBytes(result); 9 | result = bytesToHex(bytes); 10 | return result; 11 | } 12 | 13 | export function textToBytesLength(text) { 14 | const textHex = textToHex(text); 15 | const result = hexToBytes(textHex); 16 | return result.length; 17 | } 18 | 19 | export const parseSUDT = (bigEndianHexStr: string) => { 20 | const littleEndianStr = bigEndianHexStr.replace('0x', '').match(/../g).reverse().join(''); 21 | const first128Bit = littleEndianStr.substr(16); 22 | return parseInt(`0x${first128Bit}`, 16); 23 | }; 24 | -------------------------------------------------------------------------------- /src/ui/Components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Modal from '@material-ui/core/Modal'; 4 | 5 | const useStyles = makeStyles({ 6 | paper: { 7 | background: 'white', 8 | height: '100%', 9 | }, 10 | }); 11 | 12 | export default function SimpleModal(props: any) { 13 | const { children, open, onClose } = props; 14 | const classes = useStyles(); 15 | const body = <div className={classes.paper}>{children}</div>; 16 | 17 | return ( 18 | <div> 19 | <Modal 20 | open={open} 21 | onClose={onClose} 22 | aria-labelledby="simple-modal-title" 23 | aria-describedby="simple-modal-description" 24 | > 25 | {body} 26 | </Modal> 27 | </div> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/shannonToCKBFormatter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { shannonToCKBFormatter } from '@src/common/utils/formatters'; 2 | import fixtures from './fixtures'; 3 | 4 | const fixtureTable = fixtures.map(({ shannons, expected }) => [shannons, expected]); 5 | 6 | describe('Verify shannon to CKB formatter', () => { 7 | test.each(fixtureTable)( 8 | '%s shannons => %s CKB without sign', 9 | (shannons: string, expected: string) => { 10 | expect(shannonToCKBFormatter(shannons)).toBe(expected); 11 | }, 12 | ); 13 | 14 | test.each(fixtureTable)( 15 | '%s shannons => %s CKB with sign', 16 | (shannons: string, expected: string) => { 17 | expect(shannonToCKBFormatter(shannons, true)).toBe(+shannons > 0 ? `+${expected}` : expected); 18 | }, 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/background/address/tests/init.test.ts: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 2 | import init from '../init'; 3 | 4 | describe('address list message handler', () => { 5 | it('should return address list', () => { 6 | init(); 7 | expect(browser.runtime.onMessage.addListener).toBeCalled(); 8 | }); 9 | 10 | it('should handle message', () => { 11 | browser.runtime.sendMessage({ 12 | type: MESSAGE_TYPE.REQUEST_ADDRESS_LIST, 13 | }); 14 | expect(browser.runtime.sendMessage).toBeCalled(); 15 | }); 16 | 17 | it('should handle message', () => { 18 | browser.runtime.sendMessage({ 19 | type: 'address list message handler unknown type', 20 | }); 21 | expect(browser.runtime.sendMessage).not.toBeCalledWith({ type: MESSAGE_TYPE.ADDRESS_LIST }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/common/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { parseSUDT } from '@src/common/utils/index'; 2 | import _ from 'lodash'; 3 | 4 | export interface UDTInfo { 5 | typeHash: string; 6 | capacity: string; 7 | outputdata: string; 8 | } 9 | 10 | export const aggregateUDT = (udtArr: UDTInfo[]) => { 11 | return udtArr.reduce((pre, cur) => { 12 | const result = pre; 13 | const shouldAddItsTypeHash = !_.has(pre, cur.typeHash); 14 | if (shouldAddItsTypeHash) { 15 | result[cur.typeHash] = { 16 | ckb: 0, 17 | udt: 0, 18 | }; 19 | } 20 | if (cur.typeHash) { 21 | result[cur.typeHash].udt += parseSUDT(cur.outputdata); 22 | } 23 | result[cur.typeHash].ckb += parseInt(cur.capacity, 10); 24 | return result; 25 | }, {}); 26 | }; 27 | 28 | export default { 29 | aggregateUDT, 30 | }; 31 | -------------------------------------------------------------------------------- /src/common/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import getCKB from '@src/common/utils/ckb'; 2 | 3 | export const getStatusByTxHash = async (txHash) => { 4 | const ckb = await getCKB(); 5 | const result = await ckb.rpc.getTransaction(txHash); 6 | return result.txStatus.status; 7 | }; 8 | 9 | export const getBlockNumberByTxHash = async (txHash) => { 10 | const ckb = await getCKB(); 11 | const result = await ckb.rpc.getTransaction(txHash); 12 | if (result.txStatus.blockHash == null) { 13 | return BigInt(0); 14 | } 15 | const depositBlockHeader = await ckb.rpc 16 | .getBlock(result.txStatus.blockHash) 17 | .then((b) => b.header); 18 | return BigInt(depositBlockHeader.number); 19 | }; 20 | 21 | export const sendSignedTx = async (signedTx) => { 22 | const ckb = await getCKB(); 23 | return ckb.rpc.sendTransaction(signedTx); 24 | }; 25 | -------------------------------------------------------------------------------- /src/background/messageHandlers/getLiveCells.ts: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 2 | import { WEB_PAGE } from '@src/common/utils/message/constants'; 3 | import { getUnspentCells } from '@src/common/utils/apis'; 4 | 5 | export default async (port, data) => { 6 | const { currentWallet } = await browser.storage.local.get(['currentWallet']); 7 | const { lockHash = currentWallet.lock } = data; 8 | 9 | let cells = []; 10 | 11 | if (currentWallet) { 12 | cells = await getUnspentCells(lockHash, data); 13 | } 14 | 15 | port.postMessage({ 16 | type: MESSAGE_TYPE.EXTERNAL_GET_LIVE_CELLS, 17 | requestId: 'getLiveCellsRequestId', 18 | success: true, 19 | message: currentWallet ? 'get live cells successfully' : 'do not have live cells', 20 | target: WEB_PAGE, 21 | data: cells, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/utils/deps.ts: -------------------------------------------------------------------------------- 1 | import LOCKS_INFO, { NETWORKS } from '@src/common/utils/constants/locksInfo'; 2 | 3 | type TLockType = 'Secp256k1' | 'Keccak256' | 'AnyPay'; 4 | 5 | export const getDepFromLockType = async (lockType: TLockType, NetworkManager) => { 6 | const { networkType } = await NetworkManager.getCurrentNetwork(); 7 | if (!networkType || !NETWORKS.includes(networkType)) { 8 | throw new Error('Network is not supported'); 9 | } 10 | const lockInfo = LOCKS_INFO[networkType.toLowerCase()][lockType.toLowerCase()]; 11 | if (!lockInfo) { 12 | throw new Error('No dep match'); 13 | } 14 | const { txHash, depType, index } = lockInfo; 15 | const result = { 16 | outPoint: { 17 | txHash, 18 | index, 19 | }, 20 | depType, 21 | }; 22 | return result; 23 | }; 24 | 25 | export default getDepFromLockType; 26 | -------------------------------------------------------------------------------- /src/ui/pages/Sign/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RawTxDetail from '@ui/Components/PrettyPrintJson'; 3 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 4 | import Component from './Component'; 5 | 6 | export default () => { 7 | const [message, setMessage] = React.useState({} as any); 8 | React.useEffect(() => { 9 | const listener = (msg) => { 10 | if ( 11 | msg.type === MESSAGE_TYPE.EXTERNAL_SEND || 12 | msg.type === MESSAGE_TYPE.EXTERNAL_SIGN || 13 | msg.type === MESSAGE_TYPE.EXTERNAL_SIGN_SEND 14 | ) { 15 | setMessage(msg); 16 | } 17 | }; 18 | browser.runtime.onMessage.addListener(listener); 19 | return () => browser.runtime.onMessage.removeListener(listener); 20 | }, []); 21 | 22 | return <Component RawTxDetail={RawTxDetail} message={message} />; 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/utils/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { textToHex, textToBytesLength, parseSUDT } from '../index'; 2 | 3 | describe('utils', () => { 4 | it('textToHex', () => { 5 | const result = textToHex('abc'); 6 | expect(result).toEqual('0x616263'); 7 | }); 8 | 9 | it('textToHex', () => { 10 | const result = textToHex('0x123'); 11 | expect(result).toEqual('0x123'); 12 | }); 13 | 14 | it('textToBytesLength', () => { 15 | const result = textToBytesLength('abc'); 16 | expect(result).toEqual(3); 17 | }); 18 | 19 | it('should be able to parse sUDT amount', () => { 20 | const outputData = '10270000000000000000000000000000'; 21 | const udtAmount = parseSUDT(outputData); 22 | const udtAmountSame = parseSUDT(`0x${outputData}`); 23 | expect(udtAmount).toBe(10000); 24 | expect(udtAmountSame).toBe(10000); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Refer to: https://github.com/iamturns/eslint-config-airbnb-typescript#i-use-eslint-config-airbnb-with-react-support 2 | 3 | module.exports = { 4 | plugins: [ 5 | 'prettier', 6 | 'testing-library', 7 | 'jest-dom', 8 | ], 9 | extends: [ 10 | 'airbnb-typescript', 11 | 'plugin:prettier/recommended', 12 | 'plugin:testing-library/recommended', 13 | "plugin:testing-library/react", 14 | 'plugin:jest-dom/recommended', 15 | "plugin:react-hooks/recommended", 16 | ], 17 | parserOptions: { 18 | project: './tsconfig.json', 19 | }, 20 | rules: { 21 | "spaced-comment": ["error", "always", { 22 | "markers": ["/"] 23 | }], 24 | 'import/no-extraneous-dependencies': ["warn", { 25 | devDependencies: true 26 | }], 27 | 'no-plusplus': ["warn", { 28 | allowForLoopAfterthoughts: true 29 | }] 30 | } 31 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line prettier/prettier 2 | const { 3 | pathsToModuleNameMapper 4 | } = require('ts-jest/utils'); 5 | // eslint-disable-next-line prettier/prettier 6 | const { 7 | compilerOptions 8 | } = require('./tsconfig'); 9 | 10 | module.exports = { 11 | roots: ['<rootDir>'], 12 | setupFilesAfterEnv: ['<rootDir>/config/jest/jest.setup.js'], 13 | transform: { 14 | '^.+\\.tsx?$': 'ts-jest', 15 | '^.+\\.scss$': '<rootDir>/config/jest/cssTransform.js', 16 | }, 17 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 18 | modulePathIgnorePatterns: ['e2e'], 19 | // watchPathIgnorePatterns: ['e2e'], 20 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 21 | preset: 'ts-jest', 22 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 23 | prefix: '<rootDir>/', 24 | }), 25 | collectCoverage: true, 26 | }; -------------------------------------------------------------------------------- /src/common/publicKey/publicKey.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses } from '@src/tests/fixture/address'; 2 | import LOCKS_INFO from '@common/utils/constants/locksInfo'; 3 | import PublicKey from '@common/publicKey'; 4 | 5 | describe('PublicKey', () => { 6 | it('PublicKey', () => { 7 | const pubKeyInstance = new PublicKey(aliceAddresses.publicKey); 8 | const blake160 = pubKeyInstance.getBlake160(); 9 | expect(blake160).toEqual(aliceAddresses.args); 10 | const publicKeyHash = pubKeyInstance.publicKeyHash(); 11 | expect(publicKeyHash).toEqual(aliceAddresses.args); 12 | const script = { 13 | args: blake160, 14 | codeHash: LOCKS_INFO.testnet.secp256k1.codeHash, 15 | hashType: LOCKS_INFO.testnet.secp256k1.hashType, 16 | }; 17 | const lockHash = pubKeyInstance.getLockHash(script); 18 | expect(lockHash).toEqual(aliceAddresses.secp256k1.lock); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/background/wallet/transaction/tests/getLockTypeByCodeHash.test.ts: -------------------------------------------------------------------------------- 1 | import { LockType } from '@common/utils/constants'; 2 | import locksInfo, { NETWORKS } from '@common/utils/constants/locksInfo'; 3 | import getLockTypeByCodeHash from '../getLockTypeByCodeHash'; 4 | 5 | describe('getLockTypeByCodeHash', () => { 6 | it('should get correct lock type', () => { 7 | NETWORKS.forEach((networkName) => { 8 | const Secp256k1LockType = getLockTypeByCodeHash(locksInfo[networkName].secp256k1.codeHash); 9 | expect(Secp256k1LockType).toEqual(LockType.Secp256k1); 10 | const Keccak256LockType = getLockTypeByCodeHash(locksInfo[networkName].keccak256.codeHash); 11 | expect(Keccak256LockType).toEqual(LockType.Keccak256); 12 | const AnyPayLockType = getLockTypeByCodeHash(locksInfo[networkName].anypay.codeHash); 13 | expect(AnyPayLockType).toEqual(LockType.AnyPay); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/common/utils/constants/networks.ts: -------------------------------------------------------------------------------- 1 | import configService from '@src/config'; 2 | 3 | export const NETWORK_TYPES = { 4 | testnet: 'testnet', 5 | mainnet: 'mainnet', 6 | local: 'local', 7 | }; 8 | 9 | export const networks = [ 10 | { 11 | title: 'Lina Mainnet', 12 | networkType: NETWORK_TYPES.mainnet, 13 | prefix: 'ckb', 14 | nodeURL: configService.CKB_RPC_ENDPOINT_MAINNET, 15 | cacheURL: configService.CACHE_LAYER_ENDPOINT_MAINNET, 16 | }, 17 | { 18 | title: 'Aggron Testnet', 19 | networkType: NETWORK_TYPES.testnet, 20 | prefix: 'ckt', 21 | nodeURL: configService.CKB_RPC_ENDPOINT, 22 | cacheURL: configService.CACHE_LAYER_ENDPOINT, 23 | }, 24 | { 25 | title: 'Local', 26 | networkType: NETWORK_TYPES.local, 27 | prefix: 'ckt', 28 | nodeURL: 'http://127.0.0.1:8114', 29 | cacheURL: 'http://127.0.0.1:3000', 30 | }, 31 | ]; 32 | export default networks; 33 | -------------------------------------------------------------------------------- /src/common/utils/__mocks__/apis.ts: -------------------------------------------------------------------------------- 1 | import { udtUnspentCells, getUDTsByLockHashFixture } from '../fixtures/apis'; 2 | 3 | // const unspentCells = await getUnspentCells(lockHash); 4 | export const getUnspentCells = async (lockHash, params) => { 5 | if (lockHash || params) return udtUnspentCells; 6 | const result = [ 7 | { 8 | capacity: 100, 9 | }, 10 | { 11 | capacity: 200, 12 | }, 13 | ]; 14 | return Promise.resolve(result); 15 | }; 16 | 17 | export const getAddressInfo = () => Promise.resolve({ capacity: '0x01' }); 18 | 19 | export const getTxHistories = () => Promise.resolve([]); 20 | 21 | export const getUDTsByLockHash = () => Promise.resolve(getUDTsByLockHashFixture); 22 | 23 | export const getUnspentCapacity = () => Promise.resolve(100 * 10 ** 8); 24 | 25 | export default { 26 | getAddressInfo, 27 | getUnspentCells, 28 | getTxHistories, 29 | getUDTsByLockHash, 30 | getUnspentCapacity, 31 | }; 32 | -------------------------------------------------------------------------------- /src/common/networkManager/fixtures/networks.ts: -------------------------------------------------------------------------------- 1 | export const deprecatedNetworks = [ 2 | { 3 | title: 'Testnet', 4 | networkType: 'testnet', 5 | prefix: 'ckb', 6 | nodeURL: 'https://testnet.getsynapse.io/rpc', 7 | cacheURL: 'https://testnet.getsynapse.io/api', 8 | }, 9 | { 10 | title: 'Lina Mainnet', 11 | networkType: 'mainnet', 12 | prefix: 'ckb', 13 | nodeURL: 'http://mainnet.getsynapse.io/rpc', 14 | cacheURL: 'http://mainnet.getsynapse.io/api', 15 | }, 16 | ]; 17 | 18 | export const networks = [ 19 | { 20 | title: 'Testnet', 21 | networkType: 'testnet', 22 | prefix: 'ckb', 23 | nodeURL: 'https://ckb-testnet.rebase.network/rpc', 24 | cacheURL: 'https://testnet.getsynapse.io/api', 25 | }, 26 | { 27 | title: 'Lina Mainnet', 28 | networkType: 'mainnet', 29 | prefix: 'ckb', 30 | nodeURL: 'http://ckb-mainnet.rebase.network/rpc', 31 | cacheURL: 'http://mainnet.getsynapse.io/api', 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/background/currentWallet/index.test.ts: -------------------------------------------------------------------------------- 1 | import { CurrentWalletHandler, CurrentWalletManager } from '@background/currentWallet'; 2 | import { BrowserMessageManager } from '@common/messageManager'; 3 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 4 | 5 | describe('currentWallet module', () => { 6 | const messageManager = new BrowserMessageManager(); 7 | const currentWalletManager = new CurrentWalletManager(); 8 | const currentWalletHandler = new CurrentWalletHandler( 9 | messageManager, 10 | browser.storage.local, 11 | currentWalletManager, 12 | ); 13 | it('should add listener', () => { 14 | currentWalletHandler.init(); 15 | expect(browser.runtime.onMessage.addListener).toHaveBeenCalled(); 16 | }); 17 | 18 | it('handle network change', () => { 19 | // currentWalletHandler.handleNetworkChange(); 20 | browser.runtime.sendMessage({ type: MESSAGE_TYPE.NETWORK_CHANGED }); 21 | expect(browser.storage.local.get).toHaveBeenCalled(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/ui/Components/AppBar/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import App from './index'; 7 | 8 | describe('AppBar component', () => { 9 | let tree; 10 | let container; 11 | let getByTestId; 12 | 13 | beforeEach(() => { 14 | tree = render( 15 | <IntlProvider locale="en" messages={en}> 16 | <App handleNetworkChange={null} /> 17 | </IntlProvider>, 18 | ); 19 | container = tree.container; 20 | getByTestId = tree.getByTestId; 21 | }); 22 | 23 | it('should render logo', () => { 24 | const elem = container.querySelector('img'); 25 | expect(container).toContainElement(elem); 26 | }); 27 | 28 | it('should render setting icon on left side', () => { 29 | const elem = getByTestId('setting-icon'); 30 | expect(elem).not.toBeNull(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/background/keyper/tests/setupKeyper.test.ts: -------------------------------------------------------------------------------- 1 | import wallets from '@src/background/wallet/fixtures/wallets'; 2 | import { NETWORKS } from '@src/common/utils/constants/locksInfo'; 3 | import setupKeyper from '../setupKeyper'; 4 | import ContainerManager from '../containerManager'; 5 | 6 | describe('setup keyper', () => { 7 | const publicKeys = wallets.map((wallet) => wallet.publicKey); 8 | it('should be able to setup keyper for mainnet and testnet', async () => { 9 | await browser.storage.local.set({ 10 | publicKeys, 11 | }); 12 | 13 | await setupKeyper(); 14 | 15 | const manager = ContainerManager.getInstance(); 16 | const containers = manager.getAllContainers(); 17 | const containerNames = manager.names; 18 | 19 | expect(containerNames).toEqual(NETWORKS); 20 | 21 | containerNames.forEach((network) => { 22 | expect(containers[network].lockScriptSize()).toEqual(3); 23 | expect(containers[network].publicKeySize()).toEqual(3); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/common/utils/wallet.ts: -------------------------------------------------------------------------------- 1 | import { scriptToAddress } from '@keyper/specs/lib/address'; 2 | import { bech32Address, AddressType, AddressPrefix } from '@nervosnetwork/ckb-sdk-utils'; 3 | import lockUtils from '@utils/lock'; 4 | import { AnyPayCodeHashIndex } from '@utils/constants/locksInfo'; 5 | 6 | export function findInWalletsByPublicKey(publicKey, wallets) { 7 | function findKeystore(wallet) { 8 | return wallet.publicKey === publicKey; 9 | } 10 | const wallet = wallets.find(findKeystore); 11 | return wallet; 12 | } 13 | 14 | export function showAddressHelper(networkPrefix: string, script: CKBComponents.Script) { 15 | // anypay short address 16 | if (lockUtils.isAnypay(script.codeHash)) { 17 | return bech32Address(script.args, { 18 | codeHashOrCodeHashIndex: AnyPayCodeHashIndex, 19 | prefix: networkPrefix as AddressPrefix, 20 | type: AddressType.HashIdx, 21 | }); 22 | } 23 | // other address 24 | return scriptToAddress(script, { networkPrefix, short: true }); 25 | } 26 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapse extension", 3 | "description": "Synapse extension is a wallet for Nervos CKB", 4 | "version": "0.4.0", 5 | "id": "dbmnckdibkgoeppfmploopnghhgnnnmf", 6 | "options_page": "", 7 | "background": { 8 | "scripts": ["js/browser-polyfill.min.js", "js/background.js"], 9 | "persistent": false 10 | }, 11 | "browser_action": { 12 | "default_popup": "popup.html", 13 | "default_icon": "logo-32.png" 14 | }, 15 | "icons": { 16 | "128": "logo-128.png" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": ["file://*/*", "http://*/*", "https://*/*"], 21 | "js": ["js/browser-polyfill.min.js", "js/contentScript.js"], 22 | "run_at": "document_start", 23 | "all_frames": true 24 | } 25 | ], 26 | "web_accessible_resources": ["js/injectedScript.js"], 27 | "permissions": ["storage", "notifications", "downloads"], 28 | "manifest_version": 2, 29 | "content_security_policy": "script-src 'self'; object-src 'self'" 30 | } 31 | -------------------------------------------------------------------------------- /src/tests/addressScript.test.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript } from '@keyper/specs/lib/address'; 2 | import { aliceAddresses } from '@src/tests/fixture/address'; 3 | import LOCKS_INFO from '@common/utils/constants/locksInfo'; 4 | 5 | describe('addressToScript', () => { 6 | test('secp256k1 decode', () => { 7 | const script = addressToScript(aliceAddresses.secp256k1.address); 8 | expect(script).toEqual( 9 | expect.objectContaining({ 10 | hashType: LOCKS_INFO.testnet.secp256k1.hashType, 11 | codeHash: LOCKS_INFO.testnet.secp256k1.codeHash, 12 | args: aliceAddresses.args, 13 | }), 14 | ); 15 | }); 16 | 17 | test('anyonepay decode', () => { 18 | const script = addressToScript(aliceAddresses.anyPay.address); 19 | expect(script).toEqual( 20 | expect.objectContaining({ 21 | hashType: LOCKS_INFO.testnet.anypay.hashType, 22 | codeHash: LOCKS_INFO.testnet.anypay.codeHash, 23 | args: aliceAddresses.args, 24 | }), 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/background/currentWallet/currentWalletHandler.ts: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 2 | import ICurrentWalletManager from './ICurrentWalletManager'; 3 | 4 | export default class { 5 | private storage; 6 | 7 | private messageManager; 8 | 9 | private currentWalletManager; 10 | 11 | constructor(messageManager, storage, currentWalletManager: ICurrentWalletManager) { 12 | this.storage = storage; 13 | this.messageManager = messageManager; 14 | this.currentWalletManager = currentWalletManager; 15 | } 16 | 17 | async init() { 18 | this.messageManager.addListener(async (request) => { 19 | if (request.type !== MESSAGE_TYPE.NETWORK_CHANGED) return; 20 | this.handleNetworkChange(); 21 | }); 22 | } 23 | 24 | // eslint-disable-next-line class-methods-use-this 25 | async handleNetworkChange() { 26 | const { currentWallet } = await this.storage.get('currentWallet'); 27 | this.currentWalletManager.setCurrentWallet(currentWallet.publicKey); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/utils/tests/deps.test.ts: -------------------------------------------------------------------------------- 1 | import NetworkManager from '@common/networkManager'; 2 | import { getDepFromLockType } from '../deps'; 3 | 4 | describe('deps', () => { 5 | it('getDepFromLockType', async () => { 6 | await NetworkManager.initNetworks(); 7 | const result = await getDepFromLockType('Secp256k1', NetworkManager); 8 | const expected = { 9 | depType: 'depGroup', 10 | outPoint: { 11 | index: '0x0', 12 | txHash: '0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c', 13 | }, 14 | }; 15 | expect(result).toEqual(expected); 16 | }); 17 | 18 | it('getDepFromLockType with error', async () => { 19 | const network = { 20 | networkType: 'notSupport', 21 | }; 22 | await browser.storage.local.set({ 23 | networks: [network], 24 | currentNetwork: network, 25 | }); 26 | expect(getDepFromLockType('Secp256k1', NetworkManager)).rejects.toEqual( 27 | new Error('Network is not supported'), 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import PageNav from '@ui/Components/PageNav'; 4 | import { Link } from 'react-router-dom'; 5 | import AddIcon from '@material-ui/icons/Add'; 6 | import UDTList from './List'; 7 | 8 | const useStyles = makeStyles({ 9 | container: { 10 | margin: 20, 11 | fontSize: 12, 12 | }, 13 | link: { 14 | color: '#666', 15 | marginRight: 16, 16 | }, 17 | right: { 18 | textAlign: 'right', 19 | }, 20 | }); 21 | 22 | export default function UDTHome() { 23 | const classes = useStyles(); 24 | const createBtn = ( 25 | <Link to="/udts/create" className={classes.link}> 26 | <AddIcon /> 27 | </Link> 28 | ); 29 | 30 | return ( 31 | <div> 32 | <PageNav to="/setting" title="Manage UDTs" /> 33 | <div className={classes.container}> 34 | <div className={classes.right}>{createBtn}</div> 35 | <UDTList /> 36 | </div> 37 | </div> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ `uname` == 'Darwin' ]]; then 4 | echo "Mac OS" 5 | sed -i "" 's/\"version\":.*/\"version\": \"'$1'\",/g' "package.json" 6 | sed -i "" 's/\"version\":.*/\"version\": \"'$1'\",/g' "src/manifest.json" 7 | fi 8 | 9 | if [[ `uname` == 'Linux' ]]; then 10 | echo "Linux" 11 | sed -i 's/\"version\":.*/\"version\": \"'$1'\",/g' "package.json" 12 | sed -i 's/\"version\":.*/\"version\": \"'$1'\",/g' "src/manifest.json" 13 | fi 14 | 15 | echo "bump version to $1" 16 | 17 | git add package.json src/manifest.json 18 | git commit -m "bump version to $1" 19 | 20 | git tag -a "v$1" -m "v$1" 21 | 22 | while true 23 | do 24 | read -r -p "Are You Push to Github? [Y/n]" input 25 | 26 | case $input in 27 | [yY][eE][sS]|[yY]) 28 | echo "Yes" 29 | git push origin master 30 | git push --tags 31 | break 32 | ;; 33 | [nN][oO]|[nN]) 34 | echo "No" 35 | break 36 | ;; 37 | *) 38 | echo "Invalid input..." 39 | ;; 40 | esac 41 | done 42 | 43 | # https://stackoverflow.com/a/50266574/1240067 -------------------------------------------------------------------------------- /src/background/wallet/tests/mnemonic/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | entropyToMnemonic, 3 | mnemonicToEntropy, 4 | mnemonicToSeed, 5 | mnemonicToSeedSync, 6 | validateMnemonic, 7 | } from '../../mnemonic'; 8 | 9 | const fixtures = require('./fixtures.json'); 10 | 11 | describe('mnemonic', () => { 12 | it('generate, validate mnemonic', () => { 13 | fixtures.vectors.map( 14 | async ({ entropy, mnemonic }: { entropy: string; mnemonic: string; seed: string }) => { 15 | expect(validateMnemonic(mnemonic)).toBe(true); 16 | expect(entropyToMnemonic(entropy)).toBe(mnemonic); 17 | expect(mnemonicToEntropy(mnemonic)).toBe(entropy); 18 | }, 19 | ); 20 | }); 21 | 22 | it('generate seed', () => { 23 | fixtures.vectors.map( 24 | async ({ mnemonic, seed }: { entropy: string; mnemonic: string; seed: string }) => { 25 | expect(await mnemonicToSeed(mnemonic).then((s) => s.toString('hex'))).toBe(seed); 26 | expect(mnemonicToSeedSync(mnemonic).toString('hex')).toBe(seed); 27 | }, 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/ui/pages/ImportPrivateKey/UploadFile.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import App from './UploadFile'; 7 | 8 | describe('Upload file', () => { 9 | beforeEach(() => { 10 | render( 11 | <IntlProvider locale="en" messages={en}> 12 | <App /> 13 | </IntlProvider>, 14 | ); 15 | }); 16 | it('upload file', async () => { 17 | const uploadBtn = await screen.findByRole('button', { name: 'Upload Keystore JSON File' }); 18 | expect(uploadBtn).toBeInTheDocument(); 19 | 20 | const file = new File(['hello'], 'hello.png', { type: 'image/png' }); 21 | const input = screen.getByLabelText('Upload Keystore JSON File') as HTMLInputElement; 22 | 23 | userEvent.upload(input, file); 24 | 25 | expect(input.files[0]).toStrictEqual(file); 26 | expect(input.files.item(0)).toStrictEqual(file); 27 | expect(input.files).toHaveLength(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/background/keyper/signProviders/secp256k1WithPrivateKey.ts: -------------------------------------------------------------------------------- 1 | import { hexToBytes } from '@nervosnetwork/ckb-sdk-utils/lib'; 2 | 3 | const EC = require('elliptic').ec; 4 | 5 | export const padToEven = (value) => { 6 | let a = value; 7 | if (typeof a !== 'string') { 8 | throw new Error(`value must be string, is currently ${typeof a}, while padToEven.`); 9 | } 10 | if (a.length % 2) { 11 | a = `0${a}`; 12 | } 13 | return a; 14 | }; 15 | 16 | export const sign = (privateKey, message) => { 17 | const ec = new EC('secp256k1'); 18 | const keypair = ec.keyFromPrivate(privateKey.replace('0x', '')); 19 | 20 | const msg = typeof message === 'string' ? hexToBytes(message) : message; 21 | const { r, s, recoveryParam } = keypair.sign(msg, { 22 | canonical: true, 23 | }); 24 | if (recoveryParam === null) { 25 | throw new Error('Fail to sign the message'); 26 | } 27 | const fmtR = r.toString(16).padStart(64, '0'); 28 | const fmtS = s.toString(16).padStart(64, '0'); 29 | const signature = `0x${fmtR}${fmtS}${padToEven(recoveryParam.toString(16))}`; 30 | return signature; 31 | }; 32 | -------------------------------------------------------------------------------- /config/jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | const windowCrypto = require('window-crypto'); 2 | const crypto = require('crypto'); 3 | 4 | require('jest-webextension-mock'); 5 | 6 | require('@testing-library/jest-dom/extend-expect'); 7 | 8 | require('dotenv').config({ 9 | path: './.env', 10 | }); 11 | 12 | global.document.createRange = () => ({ 13 | setStart: () => {}, 14 | setEnd: () => {}, 15 | commonAncestorContainer: { 16 | nodeName: 'BODY', 17 | ownerDocument: document, 18 | }, 19 | }); 20 | 21 | Object.assign(global.crypto, { 22 | ...windowCrypto, 23 | ...crypto, 24 | subtle: { 25 | encrypt: () => { 26 | console.warn('ERROR: It is a fake function. encrypt is not implemented in jsdom'); 27 | }, 28 | decrypt: () => { 29 | console.warn('ERROR: It is a fake function. decrypt is not implemented in jsdom'); 30 | }, 31 | importKey: () => { 32 | console.warn('ERROR: It is a fake function. importKey is not implemented in jsdom'); 33 | }, 34 | deriveKey: () => { 35 | console.warn('ERROR: It is a fake function. deriveKey is not implemented in jsdom'); 36 | }, 37 | }, 38 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rebase 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/background/messageHandlers/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 3 | import sendToPopup from './sendToPopup'; 4 | import getAddressInfo from './getAddressInfo'; 5 | import getLiveCells from './getLiveCells'; 6 | 7 | const handler = async (message, port) => { 8 | const isSendToPopup = 9 | [ 10 | MESSAGE_TYPE.EXTERNAL_SIGN, 11 | MESSAGE_TYPE.EXTERNAL_SEND, 12 | MESSAGE_TYPE.EXTERNAL_SIGN_SEND, 13 | ].indexOf(message.type) !== -1; 14 | 15 | if (isSendToPopup) { 16 | sendToPopup(port, message); 17 | } 18 | 19 | if (message.type === MESSAGE_TYPE.EXTERNAL_GET_ADDRESS_INFO) { 20 | getAddressInfo(port); 21 | } 22 | 23 | if (message.type === MESSAGE_TYPE.EXTERNAL_GET_LIVE_CELLS) { 24 | getLiveCells(port, message.data); 25 | } 26 | }; 27 | 28 | export default () => { 29 | browser.runtime.onConnect.addListener((port) => { 30 | port.onMessage.addListener(async (message: any) => { 31 | const messageHandled = _.has(message, 'success'); 32 | if (messageHandled) return; 33 | handler(message, port); 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: xenial # 操作系统是 ubuntu xenial 3 | node_js: 4 | - 10.19.0 5 | 6 | cache: 7 | - npm 8 | - yarn 9 | 10 | before_install: 11 | - npm install -g yarn 12 | 13 | install: 14 | - yarn install 15 | 16 | script: 17 | # - yarn lint 18 | - cp .env.example .env 19 | - yarn test 20 | - yarn build 21 | 22 | after_success: # after build succeeds 23 | - yarn test:cov 24 | 25 | notifications: 26 | email: 27 | recipients: 28 | - 2411mail@gmail.com 29 | - syuukawa@hotmail.com 30 | on_success: never # default: change 31 | on_failure: always # default: always 32 | 33 | before_deploy: 34 | # - yarn test:e2e # run end to end tests 35 | - mv dist synapse-extension 36 | - rm -f synapse-extension.zip synapse-extension.asc # 保持clean的env 37 | - zip -q -r synapse-extension.zip synapse-extension # 打包压缩 38 | - shasum -a 256 synapse-extension.zip | tee synapse-extension.asc # 校验值并输出结果到console 39 | 40 | deploy: 41 | provider: releases # 操作 releases 页面 42 | token: $GITHUB_TOKEN 43 | file: 44 | - synapse-extension.zip 45 | - synapse-extension.asc 46 | skip_cleanup: true 47 | on: # 只有打 tag 时才运行 deploy 阶段 48 | tags: true 49 | -------------------------------------------------------------------------------- /src/background/messageHandlers/getAddressInfo.ts: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 2 | import { WEB_PAGE } from '@src/common/utils/message/constants'; 3 | import { getAddressInfo } from '@src/common/utils/apis'; 4 | import { scriptToAddress } from '@keyper/specs/lib/address'; 5 | import NetworkManager from '@common/networkManager'; 6 | 7 | export default async (port) => { 8 | const { currentWallet } = await browser.storage.local.get(['currentWallet']); 9 | let capacity = '0'; 10 | if (currentWallet) { 11 | const addressInfo = await getAddressInfo(currentWallet?.lock); 12 | capacity = addressInfo.capacity; 13 | } 14 | const currentNetwork = await NetworkManager.getCurrentNetwork(); 15 | const address = scriptToAddress(currentWallet.script, { 16 | networkPrefix: currentNetwork.prefix, 17 | short: true, 18 | }); 19 | port.postMessage({ 20 | type: MESSAGE_TYPE.EXTERNAL_GET_ADDRESS_INFO, 21 | requestId: 'getAddressInfo', 22 | success: true, 23 | message: currentWallet ? 'get address info successfully' : 'do not have wallet info', 24 | target: WEB_PAGE, 25 | data: currentWallet ? { ...currentWallet, capacity, address } : undefined, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /__mocks__/@nervosnetwork/ckb-sdk-core.ts: -------------------------------------------------------------------------------- 1 | import configService from '@src/config'; 2 | 3 | const CKBOriginal = jest.requireActual('@nervosnetwork/ckb-sdk-core').default; 4 | 5 | const CKBOriginalInstance = new CKBOriginal(configService.CKB_RPC_ENDPOINT); 6 | 7 | class CKB { 8 | url: string = ''; 9 | 10 | constructor(url) { 11 | this.url = url; 12 | } 13 | 14 | rpc: any = { 15 | sendTransaction: (signedTx) => Promise.resolve('0x123'), 16 | getLiveCell: () => { 17 | return { 18 | cell: { 19 | data: { 20 | content: '0x73796e61707365', 21 | hash: '0xf276b360de7dc210833e8efb1f19927ecd8ff89e94c72d29dc20813fe8368564', 22 | }, 23 | output: { lock: '[Object]', type: null, capacity: '0x1954fc400' }, 24 | }, 25 | status: 'live', 26 | }; 27 | }, 28 | getHeaderByNumber: (blockNumber: string) => 29 | Promise.resolve({ timestamp: '100', blockNumber, isMock: true }), 30 | getTransaction: (txHash: string) => 31 | Promise.resolve({ isMock: true, transaction: { outputs: [] } }), 32 | }; 33 | 34 | signTransaction = CKBOriginalInstance.signTransaction; 35 | 36 | utils = CKBOriginalInstance.utils; 37 | } 38 | 39 | export default CKB; 40 | -------------------------------------------------------------------------------- /src/ui/Components/PageNav/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from './index'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('PageNav component', () => { 28 | beforeEach(() => { 29 | render( 30 | <IntlProvider locale="en" messages={en}> 31 | <Router> 32 | <App title="PageNav Title" /> 33 | </Router> 34 | </IntlProvider>, 35 | ); 36 | }); 37 | 38 | it('should render title', async () => { 39 | const english = screen.getByText('PageNav Title'); 40 | expect(english).toBeInTheDocument(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/background/wallet/transaction/tests/fixtures/cells.ts: -------------------------------------------------------------------------------- 1 | import { TypesInfo } from '@common/utils/constants/typesInfo'; 2 | 3 | const unspentCells = [ 4 | { 5 | blockHash: '0x49e71fde5904f67005b72712b49ddf52a97a7abe10bd5183c31f8b3a5d936121', 6 | lock: { 7 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 8 | hashType: 'type', 9 | args: '0x8eb4d75815e2207bd29a0b3651523e5a48e8fd34', 10 | }, 11 | lockHash: '0x111823010653d32d36b18c9a257fe13158ca012e22b9b82f0640be187f10904b', 12 | outPoint: { 13 | txHash: '0x37063697f29aaa185cb971ce658872eebdee419f3389ab746674c65c9b8a96b6', 14 | index: '0x1', 15 | }, 16 | outputData: '0x449b2c00000000000000000000000000', 17 | outputDataLen: '0x10', 18 | capacity: '0x34e62ce00', 19 | type: { 20 | hashType: TypesInfo.testnet.simpleudt.hashType, 21 | codeHash: TypesInfo.testnet.simpleudt.codeHash, 22 | args: '0x6e842ebb7d7fca88495c5f2edb05070198f6f8c798d7b8f1a48226f8f060c693', 23 | }, 24 | typeHash: '0x1c5f32c5efb08ac256bd9413c30eef7420ae54f30ff11075967b89570326c295', 25 | dataHash: '0x85b991d8b373274224b781301765cddd366959acf294f58f56bb56ed5b695516', 26 | status: 'live', 27 | }, 28 | ]; 29 | 30 | export default unspentCells; 31 | -------------------------------------------------------------------------------- /src/ui/Components/TxList/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { render, screen, waitFor } from '@testing-library/react'; 4 | import '@testing-library/jest-dom/extend-expect'; 5 | import userEvent from '@testing-library/user-event'; 6 | import { IntlProvider } from 'react-intl'; 7 | import en from '@common/locales/en'; 8 | import { explorerUrl } from '@src/common/utils/tests/fixtures/token'; 9 | import App from './index'; 10 | import txList from './fixtures/txList'; 11 | 12 | describe('TxList', () => { 13 | beforeEach(() => { 14 | render( 15 | <IntlProvider locale="en" messages={en}> 16 | <Router> 17 | <App txList={txList} explorerUrl={explorerUrl} /> 18 | </Router> 19 | </IntlProvider>, 20 | ); 21 | }); 22 | 23 | it('should render tx list', () => { 24 | const list = screen.getAllByRole('list'); 25 | expect(list).toHaveLength(txList.length); 26 | }); 27 | 28 | it('should render tx list', async () => { 29 | const list = screen.getAllByRole('list'); 30 | userEvent.click(list[0]); 31 | await waitFor(() => { 32 | const hash = screen.getByText(txList[0].hash); 33 | 34 | expect(hash).toBeInTheDocument(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/common/utils/fee/calculateFee.ts: -------------------------------------------------------------------------------- 1 | import { 2 | calculateSerializedTxSizeInBlock, 3 | calculateTransactionFee, 4 | } from '@nervosnetwork/ckb-sdk-utils'; 5 | import { BN } from 'bn.js'; 6 | import { MIN_FEE_RATE, EMPTY_WITNESS } from '@src/common/utils/constants'; 7 | 8 | export interface CalculateTxFeeResult { 9 | tx: CKBComponents.RawTransaction; 10 | fee: string; 11 | } 12 | 13 | const calculateTxFee = (transaction, feeRate = BigInt(1000)): String => { 14 | const { version, cellDeps, headerDeps, inputs, outputs, outputsData } = transaction; 15 | 16 | const calculateTx = { 17 | version, 18 | cellDeps, 19 | headerDeps, 20 | inputs, 21 | outputs, 22 | outputsData, 23 | witnesses: [EMPTY_WITNESS], 24 | }; 25 | 26 | const transactionSize = calculateSerializedTxSizeInBlock(calculateTx); 27 | let txFee = calculateTransactionFee(BigInt(transactionSize), feeRate); 28 | 29 | if (BigInt(txFee) < BigInt(MIN_FEE_RATE)) { 30 | txFee = MIN_FEE_RATE; 31 | } 32 | 33 | const chargeOutput = outputs[1]; 34 | const chargeCapacity = BigInt(chargeOutput.capacity) - BigInt(txFee); 35 | chargeOutput.capacity = `0x${new BN(chargeCapacity).toString(16)}`; 36 | outputs[1] = chargeOutput; 37 | 38 | return txFee; 39 | }; 40 | 41 | export default calculateTxFee; 42 | -------------------------------------------------------------------------------- /src/ui/Components/Modal/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from './index'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('modal component', () => { 28 | beforeEach(() => { 29 | render( 30 | <IntlProvider locale="en" messages={en}> 31 | <Router> 32 | <App open> 33 | <div>Modal Title</div> 34 | </App> 35 | </Router> 36 | </IntlProvider>, 37 | ); 38 | }); 39 | 40 | it('should render title', async () => { 41 | const english = screen.getByText('Modal Title'); 42 | expect(english).toBeInTheDocument(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/ui/Components/NetworkSelector/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { act, render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import { IntlProvider } from 'react-intl'; 7 | import en from '@common/locales/en'; 8 | import NetworkManager from '@common/networkManager'; 9 | import App from './index'; 10 | 11 | describe('NetworkSelector component', () => { 12 | beforeEach(async () => { 13 | await NetworkManager.initNetworks(); 14 | await act(async () => { 15 | render( 16 | <IntlProvider locale="en" messages={en}> 17 | <Router> 18 | <App handleNetworkChange={jest.fn()} /> 19 | </Router> 20 | </IntlProvider>, 21 | ); 22 | }); 23 | }); 24 | 25 | it('should render network', () => { 26 | const lina = screen.getByText('Lina Mainnet'); 27 | expect(lina).toBeInTheDocument(); 28 | 29 | userEvent.click(lina); 30 | const aggron = screen.getByText('Aggron Testnet'); 31 | expect(aggron).toBeInTheDocument(); 32 | userEvent.click(aggron); 33 | const aggronAfter = screen.getAllByText('Aggron Testnet'); 34 | expect(aggronAfter).toHaveLength(2); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/ui/Components/PrettyPrintJson/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import { rawTx } from '@common/fixtures/tx'; 8 | import App from './index'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('PrettyPrintJson component', () => { 29 | beforeEach(() => { 30 | render( 31 | <IntlProvider locale="en" messages={en}> 32 | <Router> 33 | <App tx={rawTx} /> 34 | </Router> 35 | </IntlProvider>, 36 | ); 37 | }); 38 | 39 | it('should render title', async () => { 40 | const english = screen.getByText('Message to be signed'); 41 | expect(english).toBeInTheDocument(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/ui/pages/ExportMnemonicSecond/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 5 | import PageNav from '@ui/Components/PageNav'; 6 | 7 | const useStyles = makeStyles({ 8 | container: { 9 | margin: 20, 10 | }, 11 | mnemonic: { 12 | border: '1px solid #eee', 13 | padding: '.8em', 14 | 'font-size': '1.3em', 15 | background: '#fff', 16 | }, 17 | }); 18 | 19 | export default () => { 20 | const classes = useStyles(); 21 | const [mnemonic, setMnemonic] = React.useState([]); 22 | 23 | React.useEffect(() => { 24 | const listener = (request) => { 25 | if (request.type === MESSAGE_TYPE.EXPORT_MNEONIC_SECOND_RESULT) { 26 | setMnemonic(request.mnemonic); 27 | } 28 | }; 29 | browser.runtime.onMessage.addListener(listener); 30 | return () => browser.runtime.onMessage.removeListener(listener); 31 | }, []); 32 | 33 | return ( 34 | <div> 35 | <PageNav to="/export-mnemonic" title={<FormattedMessage id="Export Mnemonic" />} /> 36 | <div className={classes.container} data-testid="container"> 37 | <div className={classes.mnemonic} data-testid="mnemonic-id"> 38 | {mnemonic} 39 | </div> 40 | <br /> 41 | </div> 42 | </div> 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen } from '@testing-library/react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import App from '../index'; 7 | 8 | const mockFunc = jest.fn(); 9 | 10 | jest.mock('react-router-dom', () => { 11 | // Require the original module to not be mocked... 12 | const originalModule = jest.requireActual('react-router-dom'); 13 | 14 | return { 15 | __esModule: true, 16 | ...originalModule, 17 | // add your noops here 18 | useParams: jest.fn(), 19 | useHistory: () => { 20 | return { push: mockFunc }; 21 | }, 22 | Link: 'a', 23 | }; 24 | }); 25 | 26 | describe('Manage UDTs page', () => { 27 | beforeEach(async () => { 28 | await act(async () => { 29 | render( 30 | <IntlProvider locale="en" messages={en}> 31 | <Router> 32 | <App /> 33 | </Router> 34 | </IntlProvider>, 35 | ); 36 | }); 37 | }); 38 | 39 | it('should render title', async () => { 40 | const title = screen.getByText(/Manage UDTs/i); 41 | expect(title).toBeInTheDocument(); 42 | }); 43 | it('should render add button', async () => { 44 | const button = screen.getByRole('button'); 45 | expect(button).toBeInTheDocument(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/ui/pages/Address/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import TxList from '@ui/Components/TxList'; 4 | import TokenList from '@ui/Components/TokenList'; 5 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 6 | import Address from './Address'; 7 | 8 | interface AppProps { 9 | match?: any; 10 | } 11 | 12 | export default (props: AppProps) => { 13 | const history = useHistory(); 14 | const { match } = props; 15 | const isLogin = localStorage.getItem('IS_LOGIN') === 'YES'; 16 | const [loading, setLoading] = React.useState(true); 17 | const [txs, setTxs] = React.useState([]); 18 | React.useEffect(() => { 19 | setTxs([]); // clean tx data 20 | 21 | setLoading(true); 22 | 23 | browser.runtime.sendMessage({ 24 | type: MESSAGE_TYPE.GET_TX_HISTORY, 25 | }); 26 | 27 | const listener = (message) => { 28 | if (message.type === MESSAGE_TYPE.SEND_TX_HISTORY && message.txs) { 29 | setTxs(message.txs); 30 | setLoading(false); 31 | } 32 | }; 33 | browser.runtime.onMessage.addListener(listener); 34 | return () => browser.runtime.onMessage.removeListener(listener); 35 | }, []); 36 | 37 | if (!isLogin) { 38 | history.push('./mnemonic-setting'); 39 | return null; 40 | } 41 | 42 | return ( 43 | <Address match={match} TxList={TxList} TokenList={TokenList} txs={txs} loading={loading} /> 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/ui/Components/TxDetail/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import TxDetail from './index'; 8 | import { tx } from './fixture'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('txDetail componnet', () => { 29 | beforeEach(() => { 30 | render( 31 | <IntlProvider locale="en" messages={en}> 32 | <Router> 33 | <TxDetail data={tx} /> 34 | </Router> 35 | </IntlProvider>, 36 | ); 37 | }); 38 | 39 | it('should render amount', async () => { 40 | const amount = screen.getByText('0.000001 CKB'); 41 | expect(amount).toBeInTheDocument(); 42 | }); 43 | 44 | it('should render TxHash', async () => { 45 | const txHash = screen.getByText('Tx Hash'); 46 | expect(txHash).toBeInTheDocument(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/currencyFormatter/fixtures.ts: -------------------------------------------------------------------------------- 1 | const fixtures = [ 2 | { 3 | value: { 4 | shannons: '1234567890', 5 | unit: 'Unknown', 6 | exchange: '0.000000001', 7 | }, 8 | expected: '1.23456789 Unknown', 9 | }, 10 | { 11 | value: { 12 | shannons: '1234567890', 13 | unit: 'CKB', 14 | exchange: '0.000000001', 15 | }, 16 | expected: '1.23456789 CKB', 17 | }, 18 | { 19 | value: { 20 | shannons: '1234567890', 21 | unit: 'CKB', 22 | exchange: '0.00065', 23 | }, 24 | expected: '802,469.1285 CKB', 25 | }, 26 | { 27 | value: { 28 | shannons: '1234567890', 29 | unit: 'CNY', 30 | exchange: '0.00065', 31 | }, 32 | expected: '802,469.1285 CNY', 33 | }, 34 | { 35 | value: { 36 | shannons: '1234567890123456789012345678901234567890123456789012345678901234567890', 37 | unit: 'CNY', 38 | exchange: '0.65', 39 | }, 40 | expected: 41 | '802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128.5 CNY', 42 | }, 43 | { 44 | value: { 45 | shannons: '12345678901234567890123456789012345678901234567890123456789012345678901234', 46 | unit: 'CNY', 47 | exchange: '0.65', 48 | }, 49 | expected: 50 | '8,024,691,285,802,469,128,580,246,912,858,024,691,285,802,469,128,580,246,912,858,024,691,285,802.1 CNY', 51 | }, 52 | ]; 53 | export default fixtures; 54 | -------------------------------------------------------------------------------- /src/common/utils/tests/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses, aliceWallet } from '@src/tests/fixture/address'; 2 | import { findInWalletsByPublicKey, showAddressHelper } from '../wallet'; 3 | 4 | const { publicKey } = aliceAddresses; 5 | 6 | describe('wallet utils', () => { 7 | beforeAll(async () => { 8 | await browser.storage.local.set({ 9 | publicKeys: [publicKey], 10 | wallets: [aliceWallet], 11 | }); 12 | }); 13 | it('findInWalletsByPublicKey', () => { 14 | const result = findInWalletsByPublicKey(publicKey, [aliceWallet]); 15 | expect(result).not.toBeNull(); 16 | }); 17 | 18 | it('should return address', () => { 19 | const script = { 20 | args: '0x60ed0599d4a5c67fd25277243ac12b9f91517b61', 21 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 22 | hashType: 'type', 23 | }; 24 | const result = showAddressHelper('ckb', script as CKBComponents.Script); 25 | expect(result).toEqual('ckb1qyqxpmg9n822t3nl6ff8wfp6cy4ely230dssrs7k9z'); 26 | }); 27 | 28 | it('should return anypay short address', () => { 29 | const script = { 30 | args: '0x81312ae06eeb0504b737e6bcfa5397be35a928de', 31 | codeHash: '0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354', 32 | hashType: 'type', 33 | }; 34 | const result = showAddressHelper('ckb', script as CKBComponents.Script); 35 | expect(result).toEqual('ckb1qypgzvf2uphwkpgykum7d0862wtmuddf9r0qw88kle'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/tests/keystore.test.ts: -------------------------------------------------------------------------------- 1 | import * as Keystore from '@background/wallet/keystore'; 2 | 3 | describe('encrypt checkpassword decrypt test', () => { 4 | const privateKey = 'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35'; 5 | const password = '123456'; 6 | const keystoreString = { 7 | version: 3, 8 | id: '47a83b49-13fe-4bc8-9bf9-f3039fba2bec', 9 | crypto: { 10 | ciphertext: 'f7ddbe74497221d28a3a8d6ba0d9f76ef8f2be1d76b7146906034cbee787a28b', 11 | cipherparams: { iv: 'a2b79c36237e00ce7b6fa6109fce5bfc' }, 12 | cipher: 'aes-128-ctr', 13 | kdf: 'scrypt', 14 | kdfparams: { 15 | dklen: 32, 16 | salt: '237225cf33080c219303d4f07c20b02dbdc48e923f7ec6c800e8669761b692d6', 17 | n: 262144, 18 | r: 8, 19 | p: 1, 20 | }, 21 | mac: '35107480c8c167e8deb145a308890fdd45de0ea34ebc11fa5a5ee24e4445cba2', 22 | }, 23 | }; 24 | 25 | let keystore; 26 | let privateKeyDecrypt; 27 | beforeAll(() => { 28 | keystore = keystoreString; 29 | privateKeyDecrypt = privateKey; 30 | // disable the real test due to it's too time comsuming 31 | // keystore = Keystore.encrypt(Buffer.from(privateKey, 'hex'), password); 32 | // privateKeyDecrypt = Keystore.decrypt(keystoreString, password); 33 | }); 34 | 35 | it('decrypt', () => { 36 | expect(privateKeyDecrypt).toEqual(privateKey); 37 | }); 38 | 39 | it('checks correct password', async () => { 40 | expect(Keystore.checkPasswd(keystore, password)).toBe(true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/background/wallet/transaction/tests/secp256k1.test.ts: -------------------------------------------------------------------------------- 1 | import CKB from '@nervosnetwork/ckb-sdk-core'; 2 | import configService from '@src/config'; 3 | import { rawTx } from '@common/fixtures/tx'; 4 | import { bobAddresses } from '@src/tests/fixture/address'; 5 | import { Secp256k1LockScript as Secp256k1LockScriptOriginal } from '@keyper/container/lib/locks/secp256k1'; 6 | import signProvider from '@background/keyper/signProviders/secp256k1'; 7 | import { LockScript } from '@keyper/specs'; 8 | 9 | jest.mock('@common/utils/apis'); 10 | 11 | const resultWitnesses = [ 12 | '0x5500000010000000550000005500000041000000ffac752f9a4da6fc3069dd8bea3caf8e8b687040e28d2ed1d71f0f32713e87b0380902cabdad79caaf7931d48ae2ee7db370e456c0546633a6d887e2f16d402d00', 13 | '0x10000000100000001000000010000000', 14 | '0x10000000100000001000000010000000', 15 | '0x10000000100000001000000010000000', 16 | '0x10000000100000001000000010000000', 17 | ]; 18 | 19 | describe('Transaction test: secp256k1', () => { 20 | const ckb = new CKB(configService.CKB_RPC_ENDPOINT); 21 | 22 | it('should be able to sign tx with secp256k1', async () => { 23 | const { privateKey } = bobAddresses; 24 | const lockScript: LockScript = new Secp256k1LockScriptOriginal(); 25 | lockScript.setProvider(signProvider); 26 | 27 | const expectedTx = { 28 | ...rawTx, 29 | witnesses: resultWitnesses, 30 | }; 31 | 32 | const signedTx = ckb.signTransaction(privateKey)(rawTx, []); 33 | expect(signedTx.witnesses).toEqual(resultWitnesses); 34 | expect(signedTx).toEqual(expectedTx); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/ui/Components/PrettyPrintJson/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; 3 | import Accordion from '@material-ui/core/Accordion'; 4 | import AccordionSummary from '@material-ui/core/AccordionSummary'; 5 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import TXPreviewer from './TXPreviewer'; 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | root: { 14 | width: '100%', 15 | fontSize: '0.85rem', 16 | }, 17 | heading: { 18 | fontSize: theme.typography.pxToRem(15), 19 | fontWeight: theme.typography.fontWeightRegular, 20 | }, 21 | }), 22 | ); 23 | 24 | export default function SimpleAccordion(props: any) { 25 | const classes = useStyles(); 26 | const { tx } = props; 27 | 28 | return ( 29 | <div className={classes.root}> 30 | <Accordion> 31 | <AccordionSummary 32 | expandIcon={<ExpandMoreIcon />} 33 | aria-controls="panel1a-content" 34 | id="panel1a-header" 35 | > 36 | <Typography className={classes.heading}> 37 | <FormattedMessage id="Message to be signed" /> 38 | </Typography> 39 | </AccordionSummary> 40 | <AccordionDetails> 41 | <TXPreviewer tx={tx} /> 42 | </AccordionDetails> 43 | </Accordion> 44 | </div> 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/background/keyper/tests/fixtures/lockScript.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SignatureAlgorithm, 3 | LockScript, 4 | ScriptHashType, 5 | Script, 6 | CellDep, 7 | RawTransaction, 8 | Config, 9 | SignProvider, 10 | DepType, 11 | SignContext, 12 | } from '@keyper/specs'; 13 | 14 | export default class TestLockScript implements LockScript { 15 | name = 'TestLockScript'; 16 | 17 | codeHash = '0x0000000000000000000000000000000000000000000000000000000000000100'; 18 | 19 | hashType = 'type' as ScriptHashType; 20 | 21 | provider: SignProvider; 22 | 23 | depsArr = [ 24 | { 25 | outPoint: { 26 | txHash: '0x0000000000000000000000000000000000000000000000000000000000000200', 27 | index: '0x0', 28 | }, 29 | depType: 'dev_group' as DepType, 30 | }, 31 | ]; 32 | 33 | algo = SignatureAlgorithm.secp256k1; 34 | 35 | constructor(name: string, codeHash: string) { 36 | this.name = name; 37 | this.codeHash = codeHash; 38 | } 39 | 40 | script(publicKey: string): Script { 41 | return { 42 | args: publicKey, 43 | codeHash: this.codeHash, 44 | hashType: this.hashType, 45 | }; 46 | } 47 | 48 | deps(): CellDep[] { 49 | return this.depsArr; 50 | } 51 | 52 | signatureAlgorithm(): SignatureAlgorithm { 53 | return this.algo; 54 | } 55 | 56 | setProvider(provider: SignProvider): void { 57 | this.provider = provider; 58 | } 59 | 60 | async sign( 61 | _context: SignContext, 62 | rawTx: RawTransaction, 63 | _config: Config, 64 | ): Promise<RawTransaction> { 65 | return rawTx; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ui/Components/LanguageSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | import FormControl from '@material-ui/core/FormControl'; 5 | import Select from '@material-ui/core/Select'; 6 | import { getDefaultLanguage } from '@src/common/utils/locale'; 7 | 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | formControl: { 11 | minWidth: 120, 12 | marginTop: '1rem', 13 | }, 14 | selectEmpty: { 15 | marginTop: theme.spacing(2), 16 | }, 17 | select: { 18 | color: 'blake', 19 | }, 20 | }), 21 | ); 22 | 23 | export default () => { 24 | const classes = useStyles(); 25 | const defaultLanguage = getDefaultLanguage(); 26 | const [language, setLanguage] = React.useState(defaultLanguage); 27 | 28 | const handleChange = (event: React.ChangeEvent<{ value: string }>) => { 29 | setLanguage(event.target.value); 30 | localStorage.setItem('language', event.target.value); 31 | location.replace('/popup.html'); 32 | }; 33 | 34 | return ( 35 | <div> 36 | <FormControl className={classes.formControl}> 37 | <Select 38 | labelId="language-select-label" 39 | id="language-select" 40 | value={language} 41 | onChange={handleChange} 42 | className={classes.select} 43 | > 44 | <MenuItem value="en">English</MenuItem> 45 | <MenuItem value="zh">中文</MenuItem> 46 | </Select> 47 | </FormControl> 48 | </div> 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/ui/pages/ExportMnemonicSecond/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { IntlProvider } from 'react-intl'; 5 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 6 | import en from '@common/locales/en'; 7 | import App from './index'; 8 | 9 | jest.mock('react-router-dom', () => { 10 | // Require the original module to not be mocked... 11 | const originalModule = jest.requireActual('react-router-dom'); 12 | 13 | return { 14 | __esModule: true, 15 | ...originalModule, 16 | // add your noops here 17 | useParams: jest.fn(), 18 | useHistory: () => { 19 | return { push: jest.fn() }; 20 | }, 21 | }; 22 | }); 23 | 24 | describe('export mnemonic page', () => { 25 | beforeEach(() => { 26 | render( 27 | <IntlProvider locale="en" messages={en}> 28 | <Router> 29 | <App /> 30 | </Router> 31 | </IntlProvider>, 32 | ); 33 | }); 34 | it('should render title', () => { 35 | const result = screen.getByText('Export Mnemonic'); 36 | expect(result).toBeInTheDocument(); 37 | }); 38 | 39 | it('send message', async () => { 40 | await waitFor(() => { 41 | browser.runtime.sendMessage({ 42 | type: MESSAGE_TYPE.EXPORT_MNEONIC_SECOND_RESULT, 43 | mnemonic: 'abc bcd', 44 | }); 45 | expect(browser.runtime.sendMessage).toBeCalled(); 46 | 47 | const result = screen.getByText('abc bcd'); 48 | expect(result).toBeInTheDocument(); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/ui/Components/AddressListItem/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router, useHistory } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import addressInfo from './fixtures/addressInfo'; 8 | import App from './index'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('address list component', () => { 29 | const history = useHistory(); 30 | beforeEach(async () => { 31 | const onSelectAddress = jest.fn(); 32 | await act(async () => { 33 | render( 34 | <IntlProvider locale="en" messages={en}> 35 | <Router> 36 | <App onSelectAddress={onSelectAddress} addressInfo={addressInfo} /> 37 | </Router> 38 | </IntlProvider>, 39 | ); 40 | }); 41 | }); 42 | 43 | it('should render address info', async () => { 44 | const elem = screen.getByText('Secp256k1'); 45 | expect(elem).toBeInTheDocument(); 46 | 47 | userEvent.click(elem); 48 | expect(browser.storage.local.set).toBeCalled(); 49 | expect(history.push).toBeCalled(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/ui/Components/LanguageSelector/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from './index'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('address list component', () => { 28 | beforeEach(() => { 29 | render( 30 | <IntlProvider locale="en" messages={en}> 31 | <Router> 32 | <App /> 33 | </Router> 34 | </IntlProvider>, 35 | ); 36 | }); 37 | 38 | it('should render menu', async () => { 39 | delete (window as any).location; 40 | (window as any).location = { replace: jest.fn() }; 41 | const english = screen.getByText('English'); 42 | expect(english).toBeInTheDocument(); 43 | 44 | userEvent.click(english); 45 | const cn = screen.getByText('中文'); 46 | expect(cn).toBeInTheDocument(); 47 | 48 | userEvent.click(cn); 49 | const englishAfter = screen.getByText('English'); 50 | expect(englishAfter).toBeInTheDocument(); 51 | 52 | const cnAfter = screen.getAllByText('中文'); 53 | expect(cnAfter).toHaveLength(2); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/common/contactManager/index.test.ts: -------------------------------------------------------------------------------- 1 | import ContactManager from './index'; 2 | import contacts from './fixtures/contacts'; 3 | 4 | describe('contact manager', () => { 5 | beforeEach(async () => { 6 | await browser.storage.local.set({ 7 | contacts, 8 | }); 9 | }); 10 | afterEach(async () => { 11 | await browser.storage.local.clear(); 12 | }); 13 | 14 | it('should able to get contacts', async () => { 15 | const result = await ContactManager.getContactList(); 16 | expect(result).toHaveLength(contacts.length); 17 | }); 18 | 19 | it('should able to set current contact', async () => { 20 | const result = await ContactManager.getCurrentContact(); 21 | 22 | expect(result).not.toBeNull(); 23 | 24 | await ContactManager.setCurrentContact(contacts[0].address); 25 | expect(await ContactManager.getCurrentContact()).toBe(contacts[0]); 26 | 27 | await ContactManager.setCurrentContact(contacts[1].address); 28 | expect(await ContactManager.getCurrentContact()).toBe(contacts[1]); 29 | }); 30 | 31 | it('should able to create a contact', async () => { 32 | const result = await ContactManager.createContact({ 33 | name: 'Lina', 34 | address: 'lina_address', 35 | }); 36 | 37 | expect(result).toHaveLength(3); 38 | }); 39 | 40 | it('should able to get contact info', async () => { 41 | const result = await ContactManager.getContact(contacts[0].address); 42 | expect(result).toBe(contacts[0]); 43 | }); 44 | 45 | it('should able to remove a contact', async () => { 46 | await ContactManager.setCurrentContact('lina_address'); 47 | const result = await ContactManager.removeContact('lina_address'); 48 | expect(result).toHaveLength(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/component.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, useHistory } from 'react-router-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import { udtsCapacity, udtsMeta, explorerUrl } from '@src/common/utils/tests/fixtures/token'; 8 | import Component from './component'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('token list', () => { 29 | const history = useHistory(); 30 | beforeEach(() => { 31 | render( 32 | <IntlProvider locale="en" messages={en}> 33 | <Router> 34 | <Component udtsCapacity={udtsCapacity} udtsMeta={udtsMeta} explorerUrl={explorerUrl} /> 35 | </Router> 36 | </IntlProvider>, 37 | ); 38 | }); 39 | it('should have correct amount of Love Lina Token', () => { 40 | const elems = screen.getAllByLabelText('Token List'); 41 | expect(elems).toHaveLength(5); 42 | }); 43 | 44 | it('should able to go to send tx page', () => { 45 | const sendBtns = screen.getAllByText('Send'); 46 | expect(sendBtns).toHaveLength(4); 47 | userEvent.click(sendBtns[0]); 48 | expect(history.push).toBeCalled(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/common/utils/constants/typesInfo.ts: -------------------------------------------------------------------------------- 1 | import { ScriptHashType } from '@keyper/specs'; 2 | import { NETWORK_TYPES } from '@src/common/utils/constants/networks'; 3 | 4 | type TypeScript = 'simpleudt'; 5 | 6 | const { testnet, mainnet } = NETWORK_TYPES; 7 | export const NETWORKS = [testnet, mainnet]; 8 | 9 | // https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0024-ckb-system-script-list/0024-ckb-system-script-list.md 10 | export const TypesInfo = { 11 | [testnet]: { 12 | simpleudt: { 13 | codeHash: '0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4', 14 | hashType: 'type' as ScriptHashType, 15 | txHash: '0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769', 16 | depType: 'code', 17 | index: '0x0', 18 | }, 19 | }, 20 | [mainnet]: { 21 | simpleudt: { 22 | codeHash: '0x5e7a36a77e68eecc013dfa2fe6a23f3b6c344b04005808694ae6dd45eea4cfd5', 23 | hashType: 'type' as ScriptHashType, 24 | txHash: '0xc7813f6a415144643970c2e88e0bb6ca6a8edc5dd7c1022746f628284a9936d5', 25 | depType: 'code', 26 | index: '0x0', 27 | }, 28 | }, 29 | }; 30 | 31 | export const getDepFromType = async (type: TypeScript = 'simpleudt', NetworkManager) => { 32 | const { networkType } = await NetworkManager.getCurrentNetwork(); 33 | if (!networkType || !NETWORKS.includes(networkType)) { 34 | throw new Error('Network is not supported'); 35 | } 36 | const typeInfo = TypesInfo[networkType.toLowerCase()][type.toLowerCase()]; 37 | if (!typeInfo) { 38 | throw new Error('No dep match'); 39 | } 40 | const { txHash, depType, index } = typeInfo; 41 | const result = { 42 | outPoint: { 43 | txHash, 44 | index, 45 | }, 46 | depType, 47 | }; 48 | return result; 49 | }; 50 | -------------------------------------------------------------------------------- /src/ui/pages/ImportPrivateKey/UploadFile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import { Button } from '@material-ui/core'; 4 | import ArrowUpwardRounded from '@material-ui/icons/ArrowUpwardRounded'; 5 | import { FormattedMessage } from 'react-intl'; 6 | 7 | const useStyles = makeStyles({ 8 | input: { 9 | display: 'none', 10 | }, 11 | button: { 12 | width: '100%', 13 | marginTop: 24, 14 | marginBottom: 8, 15 | }, 16 | name: { 17 | color: 'rgba(0, 0, 0, 0.26)', 18 | fontSize: 12, 19 | fontWeight: 400, 20 | }, 21 | }); 22 | 23 | export default function UploadFile(props) { 24 | const classes = useStyles(); 25 | const [name, setName] = React.useState(''); 26 | 27 | const handleChange = (e) => { 28 | const file: any = e.target.files[0]; 29 | if (!file) return; 30 | setName(file.name); 31 | const reader = new FileReader(); 32 | reader.onload = (evt) => { 33 | const content = evt.target.result; 34 | props.onChange(content); 35 | }; 36 | reader.readAsText(file); 37 | e.target.value = null; 38 | }; 39 | 40 | return ( 41 | <div> 42 | <input 43 | accept=".json" 44 | className={classes.input} 45 | id="keystore-file" 46 | type="file" 47 | onChange={handleChange} 48 | /> 49 | <label htmlFor="keystore-file"> 50 | <Button 51 | className={classes.button} 52 | variant="outlined" 53 | component="span" 54 | endIcon={<ArrowUpwardRounded />} 55 | > 56 | <FormattedMessage id="Upload Keystore JSON File" /> 57 | </Button> 58 | </label> 59 | {name && <div className={classes.name}>{name}</div>} 60 | </div> 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/tests/list.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from '../List'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('List UDT', () => { 28 | beforeEach(async () => { 29 | await browser.storage.local.set({ 30 | udts: [ 31 | { 32 | decimal: '8', 33 | name: 'simpleUDT', 34 | typeHash: '0x123', 35 | symbol: 'UDT', 36 | }, 37 | ], 38 | }); 39 | await act(async () => { 40 | render( 41 | <IntlProvider locale="en" messages={en}> 42 | <Router> 43 | <App /> 44 | </Router> 45 | </IntlProvider>, 46 | ); 47 | }); 48 | }); 49 | 50 | it('should delete', async () => { 51 | const udtsElem = screen.getAllByText(/simpleUDT/i); 52 | expect(udtsElem).toHaveLength(1); 53 | const result = screen.getAllByLabelText('delete'); 54 | expect(result).toHaveLength(1); 55 | 56 | userEvent.click(result[0]); 57 | await waitFor(() => { 58 | expect(browser.storage.local.set).toBeCalled(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/common/contactManager/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | interface IContact { 4 | name: string; 5 | address: string; 6 | } 7 | 8 | const ContactManager = { 9 | async createContact(contact: IContact): Promise<IContact[]> { 10 | const contacts = await ContactManager.getContactList(); 11 | contacts.push(contact); 12 | await browser.storage.local.set({ contacts }); 13 | 14 | return ContactManager.getContactList(); 15 | }, 16 | async removeContact(address: string): Promise<IContact[]> { 17 | const contacts = await ContactManager.getContactList(); 18 | _.remove(contacts, { address }); 19 | await browser.storage.local.set({ contacts }); 20 | const currentContact = await ContactManager.getCurrentContact(); 21 | if (currentContact.address === address) { 22 | await ContactManager.setCurrentContact(contacts[0]?.address); 23 | } 24 | return ContactManager.getContactList(); 25 | }, 26 | async getContactList(): Promise<IContact[]> { 27 | const { contacts = [] } = await browser.storage.local.get('contacts'); 28 | return contacts; 29 | }, 30 | async getContact(address: string): Promise<IContact> { 31 | const contacts = await ContactManager.getContactList(); 32 | return _.find(contacts, { address }); 33 | }, 34 | async getCurrentContact(): Promise<IContact> { 35 | const contacts = await ContactManager.getContactList(); 36 | const { currentContact = contacts[0] } = await browser.storage.local.get('currentContact'); 37 | return currentContact; 38 | }, 39 | async setCurrentContact(address: string) { 40 | if (!address) return; 41 | const contact = await ContactManager.getContact(address); 42 | await browser.storage.local.set({ currentContact: contact }); 43 | }, 44 | }; 45 | 46 | export default ContactManager; 47 | -------------------------------------------------------------------------------- /src/common/popup/popup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | export default class Popup { 4 | protected _window = undefined; 5 | 6 | protected _windowId: number = undefined; 7 | 8 | protected _tabId: number = undefined; 9 | 10 | constructor(popupWindow) { 11 | this._window = popupWindow; 12 | this._windowId = popupWindow?.id; 13 | this._tabId = popupWindow?.id; 14 | } 15 | 16 | get windowId() { 17 | return this._windowId; 18 | } 19 | 20 | get window() { 21 | return this._window; 22 | } 23 | 24 | get tabId() { 25 | return this._tabId; 26 | } 27 | 28 | async getTab() { 29 | const window = await browser.windows.get(this._windowId, { populate: true }); 30 | return window?.tabs?.[0]; 31 | } 32 | 33 | waitTabLoaded() { 34 | return new Promise((resolve, reject) => { 35 | let QUERY_LIMIT = 5; 36 | try { 37 | const intervalId = setInterval(async () => { 38 | QUERY_LIMIT -= 1; 39 | const tab = await this.getTab(); 40 | // status: [loading, complete] 41 | if (tab?.status === 'complete' || QUERY_LIMIT === 0) { 42 | clearInterval(intervalId); 43 | resolve(tab); 44 | } 45 | }, 2000); 46 | } catch (error) { 47 | reject(error); 48 | } 49 | }); 50 | } 51 | 52 | async update(updateInfo) { 53 | this._window = await browser.windows.update(this._windowId, updateInfo); 54 | } 55 | 56 | go(url) { 57 | browser.tabs.update(this._tabId, { active: true, url }); 58 | } 59 | 60 | close() { 61 | browser.windows.remove(this._windowId); 62 | this._reset(); 63 | } 64 | 65 | protected _reset() { 66 | this._windowId = undefined; 67 | this._window = undefined; 68 | this._tabId = undefined; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/tests/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { generateMnemonic } from '@background/wallet/key'; 2 | import Keychain from '@background/wallet/keychain'; 3 | import { mnemonicToSeedSync } from '@background/wallet/mnemonic'; 4 | 5 | const fixture = { 6 | entropy: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f', 7 | mnemonic: 'legal winner thank year wave sausage worth useful legal winner thank yellow', 8 | seed: 9 | '878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096', 10 | }; 11 | 12 | describe('wallet', () => { 13 | const longSeed = Buffer.from( 14 | 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542', 15 | 'hex', 16 | ); 17 | 18 | it('should mnemonic length is 12', () => { 19 | const words = generateMnemonic(); 20 | expect(words.split(' ').length).toEqual(12); 21 | }); 22 | 23 | it('should check seed', () => { 24 | const seed = mnemonicToSeedSync(fixture.mnemonic); 25 | expect(seed.toString('hex')).toEqual(fixture.seed); 26 | }); 27 | 28 | it('create master keychain from long seed', () => { 29 | const master = Keychain.fromSeed(longSeed); 30 | expect(master.privateKey.toString('hex')).toEqual( 31 | '4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e', 32 | ); 33 | expect(master.identifier.toString('hex')).toEqual('bd16bee53961a47d6ad888e29545434a89bdfe95'); 34 | expect(master.fingerprint).toEqual(3172384485); 35 | expect(master.chainCode.toString('hex')).toEqual( 36 | '60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689', 37 | ); 38 | expect(master.index).toEqual(0); 39 | expect(master.depth).toEqual(0); 40 | expect(master.parentFingerprint).toEqual(0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { act, render, screen } from '@testing-library/react'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import NetworkManager from '@common/networkManager'; 7 | import { networks } from '@src/common/utils/constants/networks'; 8 | import { explorerUrl } from '@common/utils/tests/fixtures/token'; 9 | import App from './index'; 10 | import udtsFixture from './fixtures/udts'; 11 | import currentWalletFixture from './fixtures/currentWallet'; 12 | 13 | jest.mock('@common/utils/apis'); 14 | 15 | const mockFunc = jest.fn(); 16 | jest.mock('react-router-dom', () => { 17 | // Require the original module to not be mocked... 18 | const originalModule = jest.requireActual('react-router-dom'); 19 | 20 | return { 21 | __esModule: true, 22 | ...originalModule, 23 | // add your noops here 24 | useParams: jest.fn(), 25 | useHistory: () => { 26 | return { push: mockFunc }; 27 | }, 28 | Link: 'a', 29 | }; 30 | }); 31 | 32 | describe('token list', () => { 33 | beforeEach(async () => { 34 | await browser.storage.local.set({ udts: udtsFixture, currentWallet: currentWalletFixture }); 35 | await NetworkManager.initNetworks(); 36 | await NetworkManager.setCurrentNetwork(networks[1].title); 37 | await act(async () => { 38 | render( 39 | <IntlProvider locale="en" messages={en}> 40 | <Router> 41 | <App explorerUrl={explorerUrl} /> 42 | </Router> 43 | </IntlProvider>, 44 | ); 45 | }); 46 | }); 47 | 48 | it('should render udt', async () => { 49 | const loading = screen.getByText(/2923.332 TLT/i); 50 | expect(loading).toBeInTheDocument(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/background/keyper/signProviders/secp256k1.ts: -------------------------------------------------------------------------------- 1 | import * as ckbUtils from '@nervosnetwork/ckb-sdk-utils'; 2 | import * as Keystore from '@src/background/wallet/passwordEncryptor'; 3 | import { sign as secp256k1WithPrivateKey } from './secp256k1WithPrivateKey'; 4 | 5 | export async function getWalletsInStorage() { 6 | const walletsObj = await browser.storage.local.get('wallets'); 7 | 8 | if (Array.isArray(walletsObj.wallets)) { 9 | return walletsObj.wallets; 10 | } 11 | return []; 12 | } 13 | 14 | // eslint-disable-next-line no-shadow 15 | function findKeystoreInWallets(wallets, publicKey) { 16 | function findKeystore(wallet) { 17 | return wallet.publicKey === publicKey; 18 | } 19 | const wallet = wallets.find(findKeystore); 20 | return wallet?.keystore; 21 | } 22 | 23 | async function getKeystoreFromWallets(publicKey) { 24 | let nPublicKey = publicKey; 25 | if (!publicKey.startsWith('0x')) { 26 | nPublicKey = `0x${publicKey}`; 27 | } 28 | const wallets = await getWalletsInStorage(); 29 | const ks = findKeystoreInWallets(wallets, nPublicKey); 30 | // keys[nPublicKey] 31 | return ks; 32 | } 33 | 34 | const getPrivateKey = async (context) => { 35 | if (context?.privateKey) return context.privateKey; 36 | 37 | const key = await getKeystoreFromWallets(context?.publicKey); 38 | if (!key) { 39 | throw new Error(`no key for address: ${context?.address}`); 40 | } 41 | const privateKeyBuffer = await Keystore.decrypt(key, context?.password); 42 | const Uint8ArrayPk = new Uint8Array(privateKeyBuffer?.data); 43 | const privateKey = ckbUtils.bytesToHex(Uint8ArrayPk); 44 | return privateKey; 45 | }; 46 | 47 | const sign = async (context, message) => { 48 | const privateKey = await getPrivateKey(context); 49 | const signature = secp256k1WithPrivateKey(privateKey, message); 50 | return signature; 51 | }; 52 | 53 | export default { 54 | sign, 55 | }; 56 | -------------------------------------------------------------------------------- /src/tests/address.test.ts: -------------------------------------------------------------------------------- 1 | import Address, { 2 | AddressType, 3 | publicKeyToAddress, 4 | AddressPrefix, 5 | } from '@background/wallet/address'; 6 | 7 | describe('address', () => { 8 | it('path from index', () => { 9 | expect(Address.pathFor(AddressType.Receiving, 0)).toEqual("m/44'/309'/0'/0/0"); 10 | expect(Address.pathFor(AddressType.Receiving, 1)).toEqual("m/44'/309'/0'/0/1"); 11 | expect(Address.pathFor(AddressType.Change, 0)).toEqual("m/44'/309'/0'/1/0"); 12 | expect(Address.pathFor(AddressType.Change, 1)).toEqual("m/44'/309'/0'/1/1"); 13 | 14 | expect(Address.pathForReceiving(0)).toEqual("m/44'/309'/0'/0/0"); 15 | expect(Address.pathForReceiving(1)).toEqual("m/44'/309'/0'/0/1"); 16 | expect(Address.pathForChange(0)).toEqual("m/44'/309'/0'/1/0"); 17 | expect(Address.pathForChange(1)).toEqual("m/44'/309'/0'/1/1"); 18 | }); 19 | 20 | it('from public key', () => { 21 | const publicKey = '0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ead3d901330bc01'; 22 | const path = "m/44'/309'/0'/0/0"; 23 | const address = Address.fromPublicKey(publicKey, "m/44'/309'/0'/0/0"); 24 | expect(address.address).toEqual('ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83'); 25 | expect(address.path).toEqual(path); 26 | }); 27 | 28 | it('Generate testnet address from public key', () => { 29 | const publicKey = '0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ead3d901330bc01'; 30 | const address = publicKeyToAddress(publicKey); 31 | expect(address).toEqual('ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83'); 32 | }); 33 | 34 | it('Generate mainnet address from public key', () => { 35 | const publicKey = '0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ead3d901330bc01'; 36 | const address = publicKeyToAddress(publicKey, AddressPrefix.Mainnet); 37 | expect(address).toEqual('ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/background/keyper/signProviders/secp256k1.test.ts: -------------------------------------------------------------------------------- 1 | import { wallets as walletsSample } from '@src/background/wallet/fixtures/wallets'; 2 | import { aliceAddresses, aliceWallet, aliceWalletPwd } from '@src/tests/fixture/address'; 3 | import signProvider, { getWalletsInStorage } from './secp256k1'; 4 | 5 | describe('secp256k1 sign provider', () => { 6 | it('should be able to set wallets to storage', async () => { 7 | const { wallets } = await browser.storage.local.get('wallets'); 8 | 9 | expect(wallets).toBeUndefined(); 10 | 11 | const currWallets = await getWalletsInStorage(); 12 | 13 | expect(currWallets).toEqual([]); 14 | 15 | await browser.storage.local.set({ 16 | wallets: walletsSample, 17 | }); 18 | const newWallets = await getWalletsInStorage(); 19 | 20 | expect(newWallets).toEqual(walletsSample); 21 | }); 22 | 23 | it('should sign correctly', async () => { 24 | const context = { 25 | privateKey: aliceAddresses.privateKey, 26 | publicKey: aliceAddresses.publicKey, 27 | address: aliceAddresses.secp256k1.address, 28 | password: aliceWalletPwd, 29 | }; 30 | const result = await signProvider.sign(context, '0x123'); 31 | const expected = 32 | '0xaccccd1be484021413e886e78b24d6abd20b3d3f9c9a37252b709ad2114aac3a5c63439109349d14d9aec7d8d300476155d9c82375e9625083da9a58f6234fe501'; 33 | expect(result).toEqual(expected); 34 | }); 35 | 36 | it('should sign correctly with context of not having private key', async () => { 37 | await browser.storage.local.set({ 38 | wallets: [aliceWallet], 39 | }); 40 | 41 | const context = { 42 | publicKey: aliceAddresses.publicKey, 43 | address: aliceAddresses.secp256k1.address, 44 | password: aliceWalletPwd, 45 | }; 46 | try { 47 | await signProvider.sign(context, '0x123'); 48 | } catch (error) { 49 | // no action 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/types/RPCMessage.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace RPCMessage { 2 | /* Auth */ 3 | export interface AuthResponse { 4 | type: string; 5 | success: boolean; 6 | message: string; 7 | token: string; 8 | } 9 | 10 | export interface AuthRequest { 11 | type: string; 12 | origin: string; 13 | } 14 | 15 | /* Locks */ 16 | export interface LocksRequest { 17 | type: string; 18 | token: string; 19 | requestId: string; 20 | } 21 | 22 | export interface LocksResponse { 23 | type: string; 24 | requestId: string; 25 | success: boolean; 26 | message: string; 27 | data: LocksResponsePayload; 28 | } 29 | 30 | export interface LocksResponsePayload { 31 | locks: LockScript[]; 32 | } 33 | 34 | export interface LockScript { 35 | code_hash: string; 36 | hash_type: string; 37 | args: string; 38 | } 39 | 40 | /* Sign */ 41 | export interface KeyperConfig { 42 | index: number; 43 | length: number; 44 | } 45 | 46 | export interface SignResponsePayload { 47 | tx: CKBComponents.RawTransaction; 48 | } 49 | 50 | export interface SignRequest { 51 | type: string; 52 | token: string; 53 | requestId: string; 54 | data: SignRequestPayload; 55 | } 56 | 57 | export interface SignRequestPayload { 58 | target: string; 59 | tx: CKBComponents.RawTransaction; 60 | config: KeyperConfig; 61 | meta: string; 62 | } 63 | 64 | export interface SignResponse { 65 | type: string; 66 | requestId: string; 67 | success: boolean; 68 | message: string; 69 | data: SignResponsePayload; 70 | } 71 | 72 | /* Sign and send */ 73 | export interface SignSendResponsePayload { 74 | hash: string; 75 | } 76 | 77 | export interface SignSendResponse { 78 | type: string; 79 | requestId: string; 80 | success: boolean; 81 | message: string; 82 | data: SignSendResponsePayload; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/background/keyper/containerManager.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '@keyper/container'; 2 | import { SignatureAlgorithm, LockScript } from '@keyper/specs'; 3 | import NetworkManager from '@common/networkManager'; 4 | 5 | export interface INetworkContainer { 6 | name: string; 7 | container: Container; 8 | } 9 | 10 | interface IContainer { 11 | [name: string]: Container; 12 | } 13 | 14 | export default class Singleton { 15 | private static instance: Singleton; 16 | 17 | private containers: IContainer = {}; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-empty-function 20 | private constructor() {} 21 | 22 | static getInstance(): Singleton { 23 | if (!Singleton.instance) { 24 | Singleton.instance = new Singleton(); 25 | } 26 | return Singleton.instance; 27 | } 28 | 29 | public addContainer(container: INetworkContainer) { 30 | this.containers[container.name] = container.container; 31 | } 32 | 33 | public getContainer(name: string): Container { 34 | return this.containers[name]; 35 | } 36 | 37 | public async getCurrentContainer(): Promise<Container> { 38 | const currentNetwork = await NetworkManager.getCurrentNetwork(); 39 | return this.getContainer(currentNetwork.networkType); 40 | } 41 | 42 | public getAllContainers(): IContainer { 43 | return this.containers; 44 | } 45 | 46 | public get names(): string[] { 47 | return Object.keys(this.containers); 48 | } 49 | 50 | public addPublicKeyForAllContainers(publicKey: string) { 51 | this.names.forEach((name: string) => { 52 | this.containers[name].addPublicKey({ 53 | payload: publicKey, 54 | algorithm: SignatureAlgorithm.secp256k1, 55 | }); 56 | }); 57 | } 58 | 59 | public addLockScriptForAllContainers(lockScript: LockScript) { 60 | this.names.forEach((name: string) => { 61 | this.containers[name].addLockScript(lockScript); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/background/wallet/tests/address.test.ts: -------------------------------------------------------------------------------- 1 | import { bobAddresses } from '@src/tests/fixture/address'; 2 | import Address from '../address'; 3 | 4 | describe('address', () => { 5 | const { publicKey, privateKey } = bobAddresses; 6 | const addressInstance = Address.fromPublicKey(publicKey); 7 | it('should be able to get address from public key', () => { 8 | expect(addressInstance.address).toEqual(bobAddresses.secp256k1.address); 9 | }); 10 | 11 | it('should be able to get address from private key', () => { 12 | expect(Address.fromPrivateKey(privateKey.substring(2)).address).toEqual( 13 | bobAddresses.secp256k1.address, 14 | ); 15 | }); 16 | 17 | it('should be able to get address from private key with 0x prefix', () => { 18 | expect(Address.fromPrivateKey(privateKey).address).toEqual(bobAddresses.secp256k1.address); 19 | }); 20 | 21 | it('return correct path', () => { 22 | const path = Address.pathFor(0, 1); 23 | expect(path).toEqual("m/44'/309'/0'/0/1"); 24 | const pathForReceiving = Address.pathForReceiving(1); 25 | expect(pathForReceiving).toEqual("m/44'/309'/0'/0/1"); 26 | const pathForChange = Address.pathForChange(1); 27 | expect(pathForChange).toEqual("m/44'/309'/0'/1/1"); 28 | }); 29 | 30 | it('should get correct value from toBlake160', () => { 31 | const result = Address.toBlake160(publicKey); 32 | expect(result.toString()).toEqual( 33 | '155,132,136,122,178,234,23,9,152,207,249,137,86,117,220,210,156,210,109,77', 34 | ); 35 | }); 36 | it('should get correct value from getBlake160', () => { 37 | const result = addressInstance.getBlake160(); 38 | expect(result).toEqual('0x0cfa67e35069ac4923ee86a0a83de9a72e5da33c'); 39 | }); 40 | it('should get correct value from publicKeyHash', () => { 41 | const result = addressInstance.publicKeyHash(); 42 | expect(result).toEqual('0x0cfa67e35069ac4923ee86a0a83de9a72e5da33c'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/ui/Components/AddressList/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen, waitFor } from '@testing-library/react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 7 | import fixtures from './fixtures/addressesList'; 8 | import App from './index'; 9 | 10 | const AddressListItem = () => { 11 | return ( 12 | <> 13 | <nav>Address List</nav> 14 | </> 15 | ); 16 | }; 17 | 18 | const mockFunc = jest.fn(); 19 | 20 | jest.mock('react-router-dom', () => { 21 | // Require the original module to not be mocked... 22 | const originalModule = jest.requireActual('react-router-dom'); 23 | 24 | return { 25 | __esModule: true, 26 | ...originalModule, 27 | // add your noops here 28 | useParams: jest.fn(), 29 | useHistory: () => { 30 | return { push: mockFunc }; 31 | }, 32 | Link: 'a', 33 | }; 34 | }); 35 | 36 | describe('address list component', () => { 37 | beforeEach(async () => { 38 | const onSelectAddress = jest.fn(); 39 | await act(async () => { 40 | await browser.storage.local.set({ 41 | currentNetwork: fixtures.currentNetwork, 42 | }); 43 | render( 44 | <IntlProvider locale="en" messages={en}> 45 | <Router> 46 | <App onSelectAddress={onSelectAddress} AddressListItem={AddressListItem} /> 47 | </Router> 48 | </IntlProvider>, 49 | ); 50 | }); 51 | }); 52 | 53 | it('should render address list', async () => { 54 | await waitFor(() => { 55 | browser.runtime.sendMessage({ 56 | type: MESSAGE_TYPE.ADDRESS_LIST, 57 | data: fixtures.addressesList, 58 | }); 59 | }); 60 | const items = screen.getAllByRole('navigation', { name: 'Address List' }); 61 | expect(items).toHaveLength(4); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/ui/pages/MnemonicSetting/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router, useHistory } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from './index'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('Mnemonic Setting page', () => { 28 | const history = useHistory(); 29 | 30 | beforeEach(() => { 31 | render( 32 | <IntlProvider locale="en" messages={en}> 33 | <Router> 34 | <App /> 35 | </Router> 36 | </IntlProvider>, 37 | ); 38 | }); 39 | 40 | it('should render Import / Generate btn', () => { 41 | const importBtn = screen.getByRole('button', { name: 'Import Mnemonic' }); 42 | const generateBtn = screen.getByRole('button', { name: 'Generate Mnemonic' }); 43 | expect(importBtn).toBeInTheDocument(); 44 | expect(generateBtn).toBeInTheDocument(); 45 | }); 46 | 47 | it('should go to import mnenomic page', () => { 48 | const importBtn = screen.getByRole('button', { name: 'Import Mnemonic' }); 49 | userEvent.click(importBtn); 50 | expect(history.push).toBeCalled(); 51 | }); 52 | 53 | it('should go to import mnenomic page', () => { 54 | const generateBtn = screen.getByRole('button', { name: 'Generate Mnemonic' }); 55 | userEvent.click(generateBtn); 56 | expect(history.push).toBeCalled(); 57 | expect(browser.runtime.sendMessage).toBeCalled(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Synapse Extension 2 | 3 | An extension wallet for Nervos CKB. 4 | 5 | | Service | Master | 6 | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | Travis | [![Build Status](https://travis-ci.com/rebase-network/synapse-extension.svg?branch=master)](https://travis-ci.com/rebase-network/synapse-extension) | 8 | | Coverage | [![codecov](https://codecov.io/gh/rebase-network/synapse-extension/branch/master/graph/badge.svg)](https://codecov.io/gh/rebase-network/synapse-extension) | 9 | 10 | [![License](https://img.shields.io/github/license/rebase-network/synapse-extension)](./LICENSE) 11 | 12 | 13 | ## Install 14 | 15 | [Chrome web store](https://chrome.google.com/webstore/detail/synapse-extension/jlbbhddconaakgfiepgconapcaeofdef/) 16 | 17 | ## Development 18 | ### Start extension 19 | 1. `git clone git@github.com:rebase-network/synapse-extension.git` 20 | or 21 | `git clone https://github.com/rebase-network/synapse-extension.git` 22 | 2. `yarn` 23 | 3. `cp .env.example .env` 24 | 4. `yarn watch` to run the dev task in watch mode 25 | 26 | Optional: If you want to work with local CKB RPC node, you need to setup a local service to provide cell query service, checkout [ckb-cache-layer](https://github.com/rebase-network/ckb-cache-layer/blob/master/README.md) for the setup instructions. 27 | 28 | ### Install extension 29 | 30 | 1. Open Google Chrome and go to [_chrome://extensions_](chrome://extensions) 31 | 2. Enable `Developer mode` 32 | 3. Click `Load unpacked` button, select `synapse-extension/dist` folder 33 | 34 | ### Testing 35 | `yarn test` 36 | 37 | ### Lint 38 | 39 | We use eslint, Please install the following extensions in your vscode: 40 | 41 | - ESLint 42 | - Prettier 43 | - EditorConfig 44 | 45 | ## Production 46 | 47 | `yarn build` to build a production (minified) version 48 | 49 | -------------------------------------------------------------------------------- /src/background/keyper/locks/secp256k1.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScriptHashType, 3 | Script, 4 | RawTransaction, 5 | Config, 6 | SignProvider, 7 | SignContext, 8 | LockScript, 9 | CellDep, 10 | DepType, 11 | SignatureAlgorithm, 12 | } from '@keyper/specs'; 13 | import LockWithSignInterface from './interfaces/lockWithSign'; 14 | 15 | class Secp256k1LockScript implements LockWithSignInterface { 16 | public readonly name: string = 'Secp256k1'; 17 | 18 | protected codeHash: string; 19 | 20 | protected txHash: string; 21 | 22 | private secp256k1LockScriptInstance: LockScript; 23 | 24 | private depType: DepType = 'depGroup'; 25 | 26 | private index: string = '0x0'; 27 | 28 | private hashType: ScriptHashType = 'type'; 29 | 30 | private provider: SignProvider; 31 | 32 | private algo: SignatureAlgorithm = SignatureAlgorithm.secp256k1; 33 | 34 | constructor( 35 | codeHash: string, 36 | txHash: string, 37 | hashType: ScriptHashType = 'type', 38 | secp256k1LockScriptInstance: LockScript, 39 | ) { 40 | this.codeHash = codeHash; 41 | this.txHash = txHash; 42 | this.hashType = hashType; 43 | this.secp256k1LockScriptInstance = secp256k1LockScriptInstance; 44 | } 45 | 46 | public deps(): CellDep[] { 47 | return [ 48 | { 49 | outPoint: { 50 | txHash: this.txHash, 51 | index: this.index, 52 | }, 53 | depType: this.depType, 54 | }, 55 | ]; 56 | } 57 | 58 | public signatureAlgorithm(): SignatureAlgorithm { 59 | return this.algo; 60 | } 61 | 62 | public setProvider(provider: SignProvider) { 63 | this.provider = provider; 64 | } 65 | 66 | public script(publicKey: string): Script { 67 | return this.secp256k1LockScriptInstance.script(publicKey); 68 | } 69 | 70 | public async sign( 71 | context: SignContext, 72 | rawTx: RawTransaction, 73 | config: Config = { index: 0, length: -1 }, 74 | ): Promise<RawTransaction> { 75 | return this.secp256k1LockScriptInstance.sign(context, rawTx, config); 76 | } 77 | } 78 | 79 | export default Secp256k1LockScript; 80 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/tests/create.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from '../Create'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('Create UDT', () => { 28 | const routeProps = { 29 | location: { 30 | search: '?typeHash=0x123', 31 | }, 32 | }; 33 | 34 | beforeEach(() => { 35 | render( 36 | <IntlProvider locale="en" messages={en}> 37 | <Router> 38 | <App routeProps={routeProps} /> 39 | </Router> 40 | </IntlProvider>, 41 | ); 42 | }); 43 | 44 | it('should render form fields: submitbutton', async () => { 45 | const submitButton = screen.getByRole('button', { name: /Confirm/i }); 46 | expect(submitButton).toBeInTheDocument(); 47 | }); 48 | 49 | it('should create new udt', async () => { 50 | const name = screen.getByLabelText('UDT Name'); 51 | await userEvent.type(name, 'simpleUDT'); 52 | 53 | const symbol = screen.getByLabelText('Symbol'); 54 | await userEvent.type(symbol, 'UDT'); 55 | 56 | expect(screen.getByRole('form')).toHaveFormValues({ 57 | decimal: '8', 58 | name: 'simpleUDT', 59 | typeHash: '0x123', 60 | symbol: 'UDT', 61 | }); 62 | 63 | const submitBtn = screen.getByRole('button', { name: /Confirm/i }); 64 | userEvent.click(submitBtn); 65 | await waitFor(() => { 66 | expect(browser.storage.local.set).toBeCalled(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/CKBToShannonFormatter/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { CapacityUnit } from '@src/common/utils/constants'; 2 | 3 | const fixtures = [ 4 | { 5 | ckb: { 6 | amount: '1.234', 7 | unit: 'Unknown', 8 | }, 9 | expected: '1.234', 10 | }, 11 | { 12 | ckb: { 13 | amount: 'a', 14 | unit: CapacityUnit.CKB, 15 | }, 16 | expected: 'a ckb', 17 | }, 18 | { 19 | ckb: { 20 | amount: '1.234', 21 | unit: CapacityUnit.CKB, 22 | }, 23 | expected: '123400000', 24 | }, 25 | { 26 | ckb: { 27 | amount: '1.23456789', 28 | unit: CapacityUnit.CKB, 29 | }, 30 | expected: '123456789', 31 | }, 32 | { 33 | ckb: { 34 | amount: '1.0', 35 | unit: CapacityUnit.CKB, 36 | }, 37 | expected: '100000000', 38 | }, 39 | { 40 | ckb: { 41 | amount: '1.', 42 | unit: CapacityUnit.CKB, 43 | }, 44 | expected: '100000000', 45 | }, 46 | { 47 | ckb: { 48 | amount: '0.123', 49 | unit: CapacityUnit.CKB, 50 | }, 51 | expected: '12300000', 52 | }, 53 | { 54 | ckb: { 55 | amount: '.123', 56 | unit: CapacityUnit.CKB, 57 | }, 58 | expected: '12300000', 59 | }, 60 | { 61 | ckb: { 62 | amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', 63 | unit: CapacityUnit.CKB, 64 | }, 65 | expected: '1234567890123456789012345678901234567890123456789012345678901234567890123400000000', 66 | }, 67 | { 68 | ckb: { 69 | amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', 70 | unit: CapacityUnit.CKKB, 71 | }, 72 | expected: 73 | '1234567890123456789012345678901234567890123456789012345678901234567890123400000000000', 74 | }, 75 | { 76 | ckb: { 77 | amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', 78 | unit: CapacityUnit.CKGB, 79 | }, 80 | expected: 81 | '1234567890123456789012345678901234567890123456789012345678901234567890123400000000000000000', 82 | }, 83 | ]; 84 | 85 | export default fixtures; 86 | -------------------------------------------------------------------------------- /src/background/keyper/locks/tests/anypay.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses, aliceWalletPwd } from '@src/tests/fixture/address'; 2 | import { rawTx } from '@common/fixtures/tx'; 3 | import signProvider from '@background/keyper/signProviders/secp256k1'; 4 | import LOCKS_INFO from '@common/utils/constants/locksInfo'; 5 | import AnypayLockScript from '../anypay'; 6 | 7 | describe('anypay lockscript', () => { 8 | const { codeHash, txHash } = LOCKS_INFO.testnet.anypay; 9 | const index = '0x0'; 10 | const depType = 'depGroup'; 11 | 12 | it('basic', () => { 13 | const lock = new AnypayLockScript(codeHash, txHash); 14 | const script = lock.script( 15 | '0x020ea44dd70b0116ab44ade483609973adf5ce900d7365d988bc5f352b68abe50b', 16 | ); 17 | expect(script).toEqual( 18 | expect.objectContaining({ 19 | args: '0xedcda9513fa030ce4308e29245a22c022d0443bb', 20 | codeHash, 21 | hashType: 'type', 22 | }), 23 | ); 24 | }); 25 | 26 | it('deps', () => { 27 | const lock = new AnypayLockScript(codeHash, txHash); 28 | const deps = lock.deps(); 29 | 30 | expect(deps[0].depType).toEqual('depGroup'); 31 | 32 | expect(deps).toEqual([ 33 | { 34 | outPoint: { 35 | txHash, 36 | index, 37 | }, 38 | depType, 39 | }, 40 | ]); 41 | }); 42 | 43 | it('sign', async () => { 44 | const { 45 | privateKey, 46 | publicKey, 47 | secp256k1: { address }, 48 | } = aliceAddresses; 49 | const lock = new AnypayLockScript( 50 | LOCKS_INFO.testnet.anypay.codeHash, 51 | LOCKS_INFO.testnet.anypay.txHash, 52 | LOCKS_INFO.testnet.anypay.hashType, 53 | ); 54 | 55 | lock.setProvider(signProvider); 56 | 57 | const context = { privateKey, publicKey, address, aliceWalletPwd }; 58 | 59 | const result = await lock.sign(context, rawTx); 60 | const expected = 61 | '0x5500000010000000550000005500000041000000d1e172abccec16973df781ec6a4a19b0aa9930be7e8ef9b6b7b43d0bda95b9ac4a271ed96e28164c0d15136be5b0d47a1c087aac76b7ce5b8f36caa095f6794a00'; 62 | expect(result.witnesses[0]).toEqual(expected); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/ui/Components/TokenList/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import { List } from '@material-ui/core'; 5 | import { useHistory } from 'react-router-dom'; 6 | 7 | import { FormattedMessage } from 'react-intl'; 8 | import TokenListItem, { ITokenInfo } from '../TokenListItem'; 9 | 10 | const useStyles = makeStyles({ 11 | root: {}, 12 | loading: { 13 | height: 200, 14 | display: 'flex', 15 | alignItems: 'center', 16 | justifyContent: 'center', 17 | }, 18 | sendLink: { 19 | cursor: 'pointer', 20 | 'text-decoration': 'underline', 21 | }, 22 | }); 23 | 24 | interface AppProps { 25 | udtsCapacity: any; 26 | udtsMeta: any; 27 | explorerUrl: any; 28 | } 29 | 30 | export default (props: AppProps) => { 31 | const history = useHistory(); 32 | const classes = useStyles(); 33 | 34 | const { udtsCapacity, udtsMeta, explorerUrl } = props; 35 | 36 | const addressesElem = Object.keys(udtsCapacity).map((typeHash) => { 37 | const meta = _.find(udtsMeta, { typeHash }); 38 | const itemProps: ITokenInfo = { 39 | ...udtsCapacity[typeHash], 40 | ...meta, 41 | typeHash, 42 | }; 43 | 44 | const handleClick = async (event, itemPropsParams) => { 45 | const { name, typeHash: typeHashItem, udt } = itemPropsParams; 46 | let decimal = itemPropsParams?.decimal; 47 | if (decimal === undefined || decimal === null) { 48 | decimal = '8'; 49 | } 50 | history.push(`/send-tx?name=${name}&typeHash=${typeHashItem}&udt=${udt}&decimal=${decimal}`); 51 | }; 52 | 53 | const sendLink = typeHash !== 'null' && ( 54 | <span onClick={(event) => handleClick(event, itemProps)} className={classes.sendLink}> 55 | <FormattedMessage id="Send" /> 56 | </span> 57 | ); 58 | 59 | return ( 60 | <List component="nav" aria-label="Token List" key={`tokenInfo-${typeHash}`}> 61 | <TokenListItem tokenInfo={itemProps} sendLink={sendLink} explorerUrl={explorerUrl} /> 62 | </List> 63 | ); 64 | }); 65 | 66 | return <div className={classes.root}>{addressesElem}</div>; 67 | }; 68 | -------------------------------------------------------------------------------- /src/ui/Components/PageNav/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; 8 | import NavigateNextIcon from '@material-ui/icons/NavigateNext'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => 11 | createStyles({ 12 | root: { 13 | background: theme.palette.background.default, 14 | }, 15 | menuButton: { 16 | marginRight: theme.spacing(1), 17 | }, 18 | menuButtonRight: { 19 | marginRight: theme.spacing(2), 20 | position: 'absolute', 21 | right: 10, 22 | }, 23 | }), 24 | ); 25 | 26 | interface AppProps { 27 | title: React.ReactNode; 28 | to?: string; 29 | position?: string; 30 | onClickRight?: Function; 31 | } 32 | 33 | export default function PageNav(props: AppProps) { 34 | const classes = useStyles(); 35 | const history = useHistory(); 36 | const { position, to, title, onClickRight } = props; 37 | let navButtonBefore; 38 | let navButtonNext; 39 | if (position !== 'right') { 40 | navButtonBefore = !!to && ( 41 | <Link to={to}> 42 | <IconButton edge="start" className={classes.menuButton} aria-label="nav"> 43 | <NavigateBeforeIcon /> 44 | </IconButton> 45 | </Link> 46 | ); 47 | } else { 48 | navButtonNext = ( 49 | <IconButton 50 | edge="start" 51 | className={classes.menuButtonRight} 52 | aria-label="nav" 53 | onClick={() => { 54 | onClickRight('right', false); 55 | history.push(to); 56 | }} 57 | > 58 | <NavigateNextIcon /> 59 | </IconButton> 60 | ); 61 | } 62 | 63 | return ( 64 | <div className={classes.root}> 65 | <Toolbar> 66 | {navButtonBefore} 67 | <Typography variant="h6">{title}</Typography> 68 | {navButtonNext} 69 | </Toolbar> 70 | </div> 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/ui/pages/MnemonicSetting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@material-ui/core'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { makeStyles, withStyles } from '@material-ui/core/styles'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import { FormattedMessage } from 'react-intl'; 7 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 8 | 9 | const useStylesTheme = makeStyles({ 10 | root: { 11 | marginTop: 120, 12 | }, 13 | }); 14 | 15 | const BootstrapButton = withStyles({ 16 | root: { 17 | width: 208, 18 | boxShadow: 'none', 19 | fontSize: 16, 20 | padding: '8px 12px', 21 | border: '1px solid', 22 | backgroundColor: '#0063cc', 23 | borderColor: '#0063cc', 24 | }, 25 | })(Button); 26 | 27 | export default () => { 28 | const history = useHistory(); 29 | 30 | const onImport = () => { 31 | history.push('/import-mnemonic'); 32 | }; 33 | 34 | const onGenerate = () => { 35 | browser.runtime.sendMessage({ type: MESSAGE_TYPE.GEN_MNEMONIC }); 36 | history.push('/generate-mnemonic'); 37 | }; 38 | 39 | const classes = useStylesTheme(); 40 | 41 | return ( 42 | <div className={classes.root}> 43 | <Grid container direction="column" spacing={4}> 44 | <Grid item xs={12} container justify="center"> 45 | <BootstrapButton 46 | type="button" 47 | variant="contained" 48 | id="import-button" 49 | color="primary" 50 | onClick={onImport} 51 | data-testid="import-button" 52 | > 53 | <FormattedMessage id="Import Mnemonic" /> 54 | </BootstrapButton> 55 | </Grid> 56 | <Grid item xs={12} container justify="center"> 57 | <BootstrapButton 58 | type="button" 59 | variant="contained" 60 | id="generate-button" 61 | color="primary" 62 | onClick={onGenerate} 63 | data-testid="generate-button" 64 | > 65 | <FormattedMessage id="Generate Mnemonic" /> 66 | </BootstrapButton> 67 | </Grid> 68 | </Grid> 69 | </div> 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/ui/Components/TokenListItem/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import { explorerUrl } from '@src/common/utils/tests/fixtures/token'; 7 | import tokenInfo from './fixtures/tokenInfo'; 8 | import App from './index'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('token list item comopnent', () => { 29 | it('should render Love Lina Token', async () => { 30 | render( 31 | <IntlProvider locale="en" messages={en}> 32 | <Router> 33 | <App explorerUrl={explorerUrl} tokenInfo={tokenInfo[0]} sendLink="" /> 34 | </Router> 35 | </IntlProvider>, 36 | ); 37 | const loading = screen.getByText('Love Lina Token'); 38 | expect(loading).toBeInTheDocument(); 39 | }); 40 | 41 | it('should render unnamed token', async () => { 42 | render( 43 | <IntlProvider locale="en" messages={en}> 44 | <Router> 45 | <App explorerUrl={explorerUrl} tokenInfo={tokenInfo[1]} sendLink="" /> 46 | </Router> 47 | </IntlProvider>, 48 | ); 49 | const loading = screen.getByText(tokenInfo[1].typeHash.substr(0, 10)); 50 | expect(loading).toBeInTheDocument(); 51 | }); 52 | 53 | it('should render ckb token', async () => { 54 | render( 55 | <IntlProvider locale="en" messages={en}> 56 | <Router> 57 | <App explorerUrl={explorerUrl} tokenInfo={tokenInfo[2]} sendLink="" /> 58 | </Router> 59 | </IntlProvider>, 60 | ); 61 | const loading = screen.getByText('494 CKB'); 62 | expect(loading).toBeInTheDocument(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/contentScript/contentScript.ts: -------------------------------------------------------------------------------- 1 | import { BACKGROUND_PORT, WEB_PAGE, CONTENT_SCRIPT } from '@src/common/utils/message/constants'; 2 | import _ from 'lodash'; 3 | 4 | function injectCustomJs(jsPath) { 5 | const jsPathToInject = jsPath || 'js/injectedScript.js'; 6 | const temp = document.createElement('script'); 7 | const container = document.head || document.documentElement; 8 | temp.setAttribute('type', 'text/javascript'); 9 | // full url will be: chrome-extension://ggpbicboegkhkbnoifoljffhicfhgbjn/js/injectedScript.js 10 | temp.src = browser.extension.getURL(jsPathToInject); 11 | temp.onload = () => { 12 | temp.remove(); 13 | }; 14 | container.appendChild(temp); 15 | } 16 | 17 | try { 18 | injectCustomJs('js/injectedScript.js'); 19 | } catch (e) { 20 | console.error('Synapse injection failed.', e); 21 | } 22 | 23 | // Refer to chrome extension messaging: https://developer.chrome.com/extensions/messaging 24 | 25 | // post and listen message(long live) from background 26 | const port = browser.runtime.connect('', { name: 'knockknock' }); 27 | port.onMessage.addListener((message: any) => { 28 | const shouldHandleByMe = [WEB_PAGE, CONTENT_SCRIPT].indexOf(message.target) !== -1; 29 | const messageHandled = _.has(message, 'success'); 30 | if (shouldHandleByMe && messageHandled) { 31 | // send to web page(injected script) 32 | window.postMessage({ ...message, target: WEB_PAGE }, '*'); 33 | } 34 | }); 35 | 36 | // post and listen message(one time) from background 37 | // background can not send message with port, can only use one time message 38 | browser.runtime.onMessage.addListener((message) => { 39 | const messageHandled = _.has(message, 'success'); 40 | const sendToWebPage = message.target === WEB_PAGE; 41 | if (messageHandled && sendToWebPage) { 42 | window.postMessage(message, '*'); 43 | } 44 | }); 45 | 46 | // forward message from web page to background 47 | window.addEventListener( 48 | 'message', 49 | (e) => { 50 | const message = e.data; 51 | const isMessageValid = message.type; 52 | const sendToBG = message.target === BACKGROUND_PORT; 53 | if (isMessageValid && sendToBG) { 54 | port.postMessage(message); 55 | } 56 | }, 57 | false, 58 | ); 59 | -------------------------------------------------------------------------------- /src/background/keyper/locks/tests/secp256k1.test.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Secp256k1LockScript as Secp256k1LockScriptOriginal } from '@keyper/container/lib/locks/secp256k1'; 3 | import Secp256k1LockScript from '@background/keyper/locks/secp256k1'; 4 | import LOCKS_INFO from '@src/common/utils/constants/locksInfo'; 5 | import signProvider from '@background/keyper/signProviders/secp256k1'; 6 | import { privateKey, rawTx, signedMessage, config } from '@common/fixtures/tx'; 7 | 8 | describe('secp256k1 lockscript', () => { 9 | const original = new Secp256k1LockScriptOriginal(); 10 | original.setProvider(signProvider); 11 | 12 | const lockTestnet = new Secp256k1LockScript( 13 | LOCKS_INFO.testnet.secp256k1.codeHash, 14 | LOCKS_INFO.testnet.secp256k1.txHash, 15 | LOCKS_INFO.testnet.secp256k1.hashType, 16 | original, 17 | ); 18 | 19 | const lockMainnet = new Secp256k1LockScript( 20 | LOCKS_INFO.mainnet.secp256k1.codeHash, 21 | LOCKS_INFO.mainnet.secp256k1.txHash, 22 | LOCKS_INFO.mainnet.secp256k1.hashType, 23 | original, 24 | ); 25 | it('should be able get lock script', () => { 26 | const script = lockMainnet.script( 27 | '0x020ea44dd70b0116ab44ade483609973adf5ce900d7365d988bc5f352b68abe50b', 28 | ); 29 | expect(script).toEqual( 30 | expect.objectContaining({ 31 | args: '0xedcda9513fa030ce4308e29245a22c022d0443bb', 32 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 33 | hashType: 'type', 34 | }), 35 | ); 36 | }); 37 | 38 | it('should have correct depType', () => { 39 | const deps = lockTestnet.deps(); 40 | expect(deps[0].depType).toEqual('depGroup'); 41 | }); 42 | 43 | it('should be able to sign a tx hash', async () => { 44 | const rawTxCloned = _.cloneDeep(rawTx); 45 | const signedMsg = await lockTestnet.sign({ privateKey }, rawTxCloned, config); 46 | expect(signedMsg.witnesses[3]).toBe(signedMessage); 47 | }); 48 | 49 | it('should be able to sign a tx hash', async () => { 50 | const rawTxCloned = _.cloneDeep(rawTx); 51 | const signedMsg = await lockMainnet.sign({ privateKey }, rawTxCloned, config); 52 | expect(signedMsg.witnesses[3]).toBe(signedMessage); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/background/keyper/locks/tests/keccak256.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses, aliceWalletPwd } from '@src/tests/fixture/address'; 2 | import { rawTx } from '@common/fixtures/tx'; 3 | import signProvider from '@background/keyper/signProviders/secp256k1'; 4 | import LOCKS_INFO from '@common/utils/constants/locksInfo'; 5 | import Keccak256LockScript from '../keccak256'; 6 | 7 | describe('keccak lockscript', () => { 8 | const codeHash = '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8'; 9 | const txHash = '0x25635bf587adacf95c9ad302113648f89ecddc2acfe1ea358ea99f715219c4c5'; 10 | const index = '0x0'; 11 | const depType = 'code'; 12 | 13 | it('basic', () => { 14 | const lock = new Keccak256LockScript(codeHash, txHash); 15 | const script = lock.script( 16 | '0x020ea44dd70b0116ab44ade483609973adf5ce900d7365d988bc5f352b68abe50b', 17 | ); 18 | expect(script).toEqual( 19 | expect.objectContaining({ 20 | args: '0xf7f62b9be3a4aab818dc4e706b7d4fa29738d91b', 21 | codeHash, 22 | hashType: 'type', 23 | }), 24 | ); 25 | }); 26 | 27 | it('deps', () => { 28 | const lock = new Keccak256LockScript(codeHash, txHash); 29 | const deps = lock.deps(); 30 | 31 | expect(deps[0].depType).toEqual(depType); 32 | 33 | expect(deps).toEqual([ 34 | { 35 | outPoint: { 36 | txHash, 37 | index, 38 | }, 39 | depType, 40 | }, 41 | ]); 42 | }); 43 | 44 | it('sign', async () => { 45 | const { 46 | privateKey, 47 | publicKey, 48 | secp256k1: { address }, 49 | } = aliceAddresses; 50 | const lock = new Keccak256LockScript( 51 | LOCKS_INFO.testnet.keccak256.codeHash, 52 | LOCKS_INFO.testnet.keccak256.txHash, 53 | LOCKS_INFO.testnet.keccak256.hashType, 54 | ); 55 | 56 | lock.setProvider(signProvider); 57 | 58 | const context = { privateKey, publicKey, address, aliceWalletPwd }; 59 | 60 | const result = await lock.sign(context, rawTx); 61 | const expected = 62 | '0x5500000010000000550000005500000041000000b1998b1641c80d67acabbff4b776b215d10a0426289e073683c0f16d13bee5307ac6d447c41ac1af8984dc805011bee6ee673c025c1131e85ff6bf8c1cb023be00'; 63 | expect(result.witnesses[0]).toEqual(expected); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/ui/pages/Address/address.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen } from '@testing-library/react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { IntlProvider } from 'react-intl'; 5 | import en from '@common/locales/en'; 6 | import NetworkManager from '@common/networkManager'; 7 | import currentWallet from './fixtures/currentWallet'; 8 | import App from './Address'; 9 | 10 | jest.mock('react-router-dom', () => { 11 | // Require the original module to not be mocked... 12 | const originalModule = jest.requireActual('react-router-dom'); 13 | 14 | return { 15 | __esModule: true, 16 | ...originalModule, 17 | // add your noops here 18 | useParams: jest.fn(), 19 | useHistory: () => ({ 20 | push: jest.fn(), 21 | }), 22 | }; 23 | }); 24 | 25 | const TxList = () => <div>TxList</div>; 26 | const TokenList = () => <div>TokenList</div>; 27 | const txs = []; 28 | const loading = false; 29 | 30 | describe('Address page', () => { 31 | beforeEach(async () => { 32 | await browser.storage.local.set({ currentWallet }); 33 | await NetworkManager.initNetworks(); 34 | 35 | await act(async () => { 36 | render( 37 | <IntlProvider locale="en" messages={en}> 38 | <Router> 39 | <App TxList={TxList} TokenList={TokenList} txs={txs} loading={loading} /> 40 | </Router> 41 | </IntlProvider>, 42 | ); 43 | }); 44 | }); 45 | 46 | it('should render receive / send btn', async () => { 47 | const receiveBtn = screen.getByRole('button', { name: 'Receive' }); 48 | const sendBtn = screen.getByRole('button', { name: 'Send' }); 49 | expect(receiveBtn).toBeInTheDocument(); 50 | expect(sendBtn).toBeInTheDocument(); 51 | }); 52 | 53 | it('should render capacity refresh button', async () => { 54 | const result = screen.getByRole('button', { name: /ckb1qyqgad...l56qp73mg0/i }); 55 | expect(result).toBeInTheDocument(); 56 | }); 57 | 58 | it('should render tx list', async () => { 59 | const result = screen.getByText('Latest 20 Transactions'); 60 | expect(result).toBeInTheDocument(); 61 | }); 62 | 63 | it('should render Show UDT button', async () => { 64 | const result = screen.getByText('Show UDT'); 65 | expect(result).toBeInTheDocument(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | .DS_Store 4 | 5 | # Created by https://www.gitignore.io/api/node 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | / 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # next.js build output 80 | .next 81 | 82 | # nuxt.js build output 83 | .nuxt 84 | 85 | # Uncomment the public line if your project uses Gatsby 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 88 | # public 89 | 90 | # Storybook build outputs 91 | .out 92 | .storybook-out 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # Temporary folders 107 | tmp/ 108 | temp/ 109 | # End of https://www.gitignore.io/api/node 110 | 111 | .jsbeautifyrc 112 | 113 | synapse-extension* 114 | 115 | codecov.yml -------------------------------------------------------------------------------- /src/background/wallet/fixtures/wallets.ts: -------------------------------------------------------------------------------- 1 | export const wallets = [ 2 | { 3 | entropyKeystore: '', 4 | keystore: 5 | '{"data":"Eoi8blI4DkqV6gFboXf5fFrEEFT6Ruyati2jPmuSDIIQ0JUY564yj6tIn6ZeNeHyF07/50redPryklzb4+ik7lHIHk0iwqhoa2AXl6huQNF9djAqCUWlnxmmKrRtTp5lqBvuSl4ISxLyL+S1H6GBzu1bo9qTo7+3delmDAlYaIv9RQXaxYQxaRq7zodEZr4KGmkDHEGZRm8mIDt3bw==","iv":"haB4bkup1m6eEMeuL702Ww==","salt":"3Mxo4KRTrX7HgLzjy2ZHsKXoWnmq0MibKsahtA3CYpc="}', 6 | keystoreType: '3', 7 | publicKey: '0x02823d83ffca3ad1a9383cbb9dad7b72c7c3f6d547c69017e64f43cbea4563a511', 8 | rootKeystore: '', 9 | }, 10 | { 11 | entropyKeystore: 12 | '{"data":"ZPTUv6QOX7hRg4T9gRYXTXk4gDfqBwdcsY0UaFup8YP26oa8qX6lpeqckU1EOyqQ0POzZtw8HqyQ1yQ4nmRn7GXh/QJdVNi4FjGE9y6g34Gg37qPvaqtSicQByzQqsA=","iv":"l8h6u6znz/BUEleBNBEmMQ==","salt":"tBKFIWuOQ7vEjAKkAQuPfyMZLa3jJnPNGEbHFx94cMk="}', 13 | keystore: 14 | '{"data":"wfPxyPWO7z202yhwWZBmL2eUCjOKRUUNatMuuJ4mUs2Kz/OyuK6Phvd3wG99/RpbAIdo6QIBm0hgfIGXFyYi9lheZYNcc5hQ9aQ4lqt4gTDius1YxQPFCKf8USWzhw6wYgGNx0itb+pfoGQiesgxEACnpLGD2Lt6lOnR0PFdhBLVeJ2/4V5v/ICksRPsXD77twPr0yUSvtXEERo1W5IdgtooYw==","iv":"xUQWLqBpCSFzYmCcxupjVA==","salt":"NPyD2YdiAkUsc4l5hhijSK6whVFh03qbRY2Ul2qOrRQ="}', 15 | keystoreType: '3', 16 | publicKey: '0x021b30b3047a645d8b6c10c513b767a3e08efa1a53df5f81bcb37af3c8c8358ae9', 17 | rootKeystore: 18 | '{"data":"z0jQgUbx9Mf2n6vUpepgqSa9xnuoxakLa4LEwpYkCN/yc/08++y3n8ce1W8eHMFb1WYtYD6Hv7rrGH93AX4RWPrfumDCEzvTwgLX2+Xit7JVdtI9C0bzAGcsNzhcM64W/6I1orkze/JEisYdeHpcuU92W0hEUI4v6PlB9SuWsCmunkLMK24FtFGZxY/zRYhkLi64nifEE1I1S2fgPg6tQFC6OEJGVRAWriITYoNagqdplEzYSAlJhCNv789lni1njVyzFtgI5W1qCNQ8kXJop6ndlo65Hs4+L1f3LTxkW0Uw18nt5Ln2zP5DAGzqemQBuSNcH1ObMGjf79zha5PySOpBSNkS8JprZD2hK2yL","iv":"lG6FZ92rSV9CUsoZJMD2zQ==","salt":"JR4QmFQ48RuvfYrqxkmJM7qIcQu9I6NaMTAMTxyKKdM="}', 19 | }, 20 | { 21 | entropyKeystore: '', 22 | keystore: 23 | '{"data":"tKn7P2kil4TFs3PBkrIYkA8uDBA5lILh1BBNvpIMrtslOH/QzvqYF7CzzAK46ng8taC9/p/28Poq338eUiWHr4eiF0KGMusvtHjvXFKTsiErlPuHM1Jq+K2uww0Pq9EPuvOYYi9pvpwZehmJcKIoe0/jbroH+o0jQ4WJmFv9dJFSOhhyO0iiT6MofKdMur3+3WIxBYxPMvtDKciiccXm","iv":"c5LY1XJe8smRcfUXrMGqjg==","salt":"WIFviei/JGvbLIQp/rybWh+tkXI37CemnPVc9miVdKM="}', 24 | keystoreType: '3', 25 | publicKey: '0x03d3319a7a7b8b88747664ca9559ab21e746452e8ed5eddc2f4365a1a9157e9ca2', 26 | rootKeystore: '', 27 | }, 28 | ]; 29 | 30 | export default wallets; 31 | -------------------------------------------------------------------------------- /src/ui/Components/TxList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { ListItem, ListItemText, List, Link, Tooltip } from '@material-ui/core'; 5 | import CallMadeIcon from '@material-ui/icons/CallMade'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import Divider from '@material-ui/core/Divider'; 8 | import { shannonToCKBFormatter } from '@src/common/utils/formatters'; 9 | import Modal from '@ui/Components/Modal'; 10 | import TxDetail from '@ui/Components/TxDetail'; 11 | 12 | const useStyles = makeStyles({ 13 | list: { 14 | cursor: 'pointer', 15 | }, 16 | }); 17 | 18 | interface AppProps { 19 | txList: any; 20 | explorerUrl: string; 21 | } 22 | 23 | export default (props: AppProps) => { 24 | const classes = useStyles(); 25 | const [open, setOpen] = React.useState(false); 26 | const [selectedTxHash, setSelectedTxHash] = React.useState(''); 27 | 28 | const { txList, explorerUrl } = props; 29 | 30 | const toggleModal = () => { 31 | setOpen(!open); 32 | }; 33 | 34 | const closeModal = () => { 35 | setSelectedTxHash(''); 36 | }; 37 | 38 | const onSelectTx = (hash) => { 39 | toggleModal(); 40 | setSelectedTxHash(hash); 41 | }; 42 | 43 | const txListElem = txList.map((item) => ( 44 | <List onClick={() => onSelectTx(item.hash)} key={item.hash} className={classes.list}> 45 | <Divider /> 46 | <ListItem disableGutters> 47 | <ListItemText primary={`${shannonToCKBFormatter(item.amount.toString())} CKB`} /> 48 | {item.typeHash !== null ? <ListItemText primary={item.sudt} /> : null} 49 | <Link rel="noreferrer" target="_blank" href={`${explorerUrl}/transaction/${item.hash}`}> 50 | <Tooltip title={<FormattedMessage id="View on Explorer" />} placement="top"> 51 | <CallMadeIcon /> 52 | </Tooltip> 53 | </Link> 54 | </ListItem> 55 | <ListItem disableGutters> 56 | <ListItemText secondary={item.income ? 'Received' : 'Sent'} /> 57 | <ListItemText secondary={moment(item.timestamp).format('YYYY-MM-DD HH:mm:ss')} /> 58 | </ListItem> 59 | <Modal open={open && selectedTxHash === item.hash} onClose={closeModal}> 60 | <TxDetail data={item} /> 61 | </Modal> 62 | </List> 63 | )); 64 | 65 | return <div data-testid="container">{txListElem}</div>; 66 | }; 67 | -------------------------------------------------------------------------------- /src/common/utils/constants/locksInfo.ts: -------------------------------------------------------------------------------- 1 | import { ScriptHashType } from '@keyper/specs'; 2 | import { NETWORK_TYPES } from '@common/utils/constants/networks'; 3 | 4 | const { testnet, mainnet, local } = NETWORK_TYPES; 5 | export const NETWORKS = [mainnet, testnet, local]; 6 | 7 | // https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0024-ckb-system-script-list/0024-ckb-system-script-list.md 8 | const locksInfo = { 9 | [testnet]: { 10 | secp256k1: { 11 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 12 | hashType: 'type' as ScriptHashType, 13 | txHash: '0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37', 14 | depType: 'depGroup', 15 | index: '0x0', 16 | }, 17 | keccak256: { 18 | codeHash: '0x58c5f491aba6d61678b7cf7edf4910b1f5e00ec0cde2f42e0abb4fd9aff25a63', 19 | hashType: 'type' as ScriptHashType, 20 | txHash: '0x57a62003daeab9d54aa29b944fc3b451213a5ebdf2e232216a3cfed0dde61b38', 21 | depType: 'code', 22 | index: '0x0', 23 | }, 24 | anypay: { 25 | codeHash: '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', 26 | hashType: 'type' as ScriptHashType, 27 | txHash: '0xec26b0f85ed839ece5f11c4c4e837ec359f5adc4420410f6453b1f6b60fb96a6', 28 | depType: 'depGroup', 29 | index: '0x0', 30 | }, 31 | }, 32 | [mainnet]: { 33 | secp256k1: { 34 | codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', 35 | hashType: 'type' as ScriptHashType, 36 | txHash: '0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c', 37 | depType: 'depGroup', 38 | index: '0x0', 39 | }, 40 | keccak256: { 41 | codeHash: '0xbf43c3602455798c1a61a596e0d95278864c552fafe231c063b3fabf97a8febc', 42 | hashType: 'type' as ScriptHashType, 43 | txHash: '0x1d60cb8f4666e039f418ea94730b1a8c5aa0bf2f7781474406387462924d15d4', 44 | depType: 'code', 45 | index: '0x0', 46 | }, 47 | anypay: { 48 | codeHash: '0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354', 49 | hashType: 'type' as ScriptHashType, 50 | txHash: '0x4153a2014952d7cac45f285ce9a7c5c0c0e1b21f2d378b82ac1433cb11c25c4d', 51 | depType: 'depGroup', 52 | index: '0x0', 53 | }, 54 | }, 55 | }; 56 | 57 | locksInfo[local] = locksInfo[testnet]; 58 | 59 | export const AnyPayCodeHashIndex: string = '0x02'; 60 | 61 | export default locksInfo; 62 | -------------------------------------------------------------------------------- /src/ui/pages/ExportPrivateKeySecond/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 7 | import en from '@common/locales/en'; 8 | import App from './index'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | }; 25 | }); 26 | 27 | browser.downloads = { 28 | ...browser.downloads, 29 | download: mockFunc, 30 | }; 31 | 32 | describe('export mnemonic page', () => { 33 | beforeEach(() => { 34 | render( 35 | <IntlProvider locale="en" messages={en}> 36 | <Router> 37 | <App /> 38 | </Router> 39 | </IntlProvider>, 40 | ); 41 | }); 42 | 43 | it('should render title', () => { 44 | const result = screen.getByText('Export Private Key / Keystore'); 45 | expect(result).toBeInTheDocument(); 46 | }); 47 | 48 | it('should change radio form fields: private key', async () => { 49 | const radio = screen.getByLabelText('Private Key'); 50 | expect(radio).toBeInTheDocument(); 51 | 52 | userEvent.click(radio); 53 | expect(radio).toBeChecked(); 54 | }); 55 | 56 | it('should change radio form fields: Keystore', async () => { 57 | const radio = screen.getByLabelText('Keystore'); 58 | expect(radio).toBeInTheDocument(); 59 | 60 | userEvent.click(radio); 61 | expect(radio).toBeChecked(); 62 | 63 | await waitFor(() => { 64 | browser.runtime.sendMessage({ 65 | type: MESSAGE_TYPE.EXPORT_PRIVATE_KEY_SECOND_RESULT, 66 | keystore: 'keystore', 67 | }); 68 | expect(browser.runtime.sendMessage).toBeCalled(); 69 | }); 70 | 71 | const submitButton = screen.getByRole('button', { name: /Save Keystore/i }); 72 | expect(submitButton).toBeInTheDocument(); 73 | 74 | userEvent.click(submitButton); 75 | await waitFor(() => { 76 | expect(browser.downloads.download).toBeCalled(); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/Create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import * as Yup from 'yup'; 4 | import { useIntl } from 'react-intl'; 5 | import { Formik } from 'formik'; 6 | import PageNav from '@src/ui/Components/PageNav'; 7 | import { makeStyles } from '@material-ui/core'; 8 | import { useHistory } from 'react-router-dom'; 9 | import queryString from 'query-string'; 10 | import UDTForm from './Form'; 11 | 12 | const useStyles = makeStyles({ 13 | container: { 14 | margin: 20, 15 | fontSize: 12, 16 | }, 17 | }); 18 | 19 | export default function UDTCreate(props: any) { 20 | const classes = useStyles(); 21 | const intl = useIntl(); 22 | const history = useHistory(); 23 | const { routeProps } = props; 24 | const searchParams = queryString.parse(routeProps?.location?.search); 25 | 26 | const onSubmit = async (values, { resetForm }) => { 27 | const { name, typeHash, decimal, symbol } = values; 28 | const { udts = [] } = await browser.storage.local.get('udts'); 29 | const udtObj = { name, typeHash, decimal, symbol }; 30 | 31 | const udtInx = _.findIndex(udts, (udtItem) => { 32 | return udtItem.typeHash === typeHash; 33 | }); 34 | 35 | if (udtInx === -1) { 36 | udts.push(udtObj); 37 | } else { 38 | udts[udtInx] = udtObj; 39 | } 40 | 41 | await browser.storage.local.set({ udts }); 42 | resetForm({ values: { name: '', typeHash: '', decimal: '', symbol: '' } }); 43 | history.push('/udts'); 44 | }; 45 | 46 | const initialValues = { 47 | name: '', 48 | typeHash: searchParams.typeHash, 49 | decimal: '8', 50 | symbol: '', 51 | }; 52 | const formElem = ( 53 | <Formik 54 | initialValues={initialValues} 55 | onSubmit={onSubmit} 56 | validationSchema={Yup.object().shape({ 57 | name: Yup.string().required(intl.formatMessage({ id: 'Required' })), 58 | typeHash: Yup.string().required(intl.formatMessage({ id: 'Required' })), 59 | decimal: Yup.string() 60 | .required(intl.formatMessage({ id: 'Required' })) 61 | .matches(/^[1-9][0-9]*$|^0$/, intl.formatMessage({ id: 'Invalid number' })), 62 | symbol: Yup.string().required(intl.formatMessage({ id: 'Required' })), 63 | })} 64 | > 65 | {UDTForm} 66 | </Formik> 67 | ); 68 | 69 | return ( 70 | <div> 71 | <PageNav to="/udts" title="Add UDT" /> 72 | <div className={classes.container}>{formElem}</div> 73 | </div> 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 4 | import { ListItem, ListItemText, List } from '@material-ui/core'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import DeleteIcon from '@material-ui/icons/Delete'; 7 | import EditIcon from '@material-ui/icons/Edit'; 8 | import { truncateHash } from '@src/common/utils/formatters'; 9 | import { useHistory } from 'react-router-dom'; 10 | 11 | export default function UDTList() { 12 | const history = useHistory(); 13 | const [udtsItems, setUdtsItems] = React.useState([]); 14 | React.useEffect(() => { 15 | browser.storage.local.get('udts').then((result) => { 16 | if (Array.isArray(result.udts)) { 17 | setUdtsItems(result.udts); 18 | } 19 | }); 20 | }, []); 21 | 22 | const handleDelete = async (event, typeHash) => { 23 | let udtsObj = []; 24 | const udtsStorage = await browser.storage.local.get('udts'); 25 | if (Array.isArray(udtsStorage.udts)) { 26 | udtsObj = udtsStorage.udts; 27 | } 28 | _.remove(udtsObj, function removeItem(contact) { 29 | return contact.typeHash === typeHash; 30 | }); 31 | setUdtsItems(udtsObj); 32 | await browser.storage.local.set({ udts: udtsObj }); 33 | }; 34 | 35 | const handleEdit = async (event, typeHash) => { 36 | history.push(`/udts/edit/${typeHash}`); 37 | }; 38 | 39 | const udtsElem = udtsItems.map((item) => { 40 | const secondaryItem = `${item.name} - ${item.decimal} - ${item.symbol}`; 41 | return ( 42 | <List component="nav" aria-label="udts List" key={`item-${item.typeHash}`}> 43 | <ListItem> 44 | <ListItemText primary={truncateHash(item.typeHash)} secondary={secondaryItem} /> 45 | <ListItemSecondaryAction> 46 | <IconButton 47 | edge="end" 48 | aria-label="edit" 49 | onClick={(event) => handleEdit(event, item.typeHash)} 50 | > 51 | <EditIcon /> 52 | </IconButton> 53 | <IconButton 54 | edge="end" 55 | aria-label="delete" 56 | onClick={(event) => handleDelete(event, item.typeHash)} 57 | > 58 | <DeleteIcon /> 59 | </IconButton> 60 | </ListItemSecondaryAction> 61 | </ListItem> 62 | </List> 63 | ); 64 | }); 65 | 66 | return <>{udtsElem}</>; 67 | } 68 | -------------------------------------------------------------------------------- /src/ui/pages/Sign/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import { MESSAGE_TYPE } from '@src/common/utils/constants'; 8 | import App from './Component'; 9 | 10 | const mockFunc = jest.fn(); 11 | 12 | jest.mock('react-router-dom', () => { 13 | // Require the original module to not be mocked... 14 | const originalModule = jest.requireActual('react-router-dom'); 15 | 16 | return { 17 | __esModule: true, 18 | ...originalModule, 19 | // add your noops here 20 | useParams: jest.fn(), 21 | useHistory: () => { 22 | return { push: mockFunc }; 23 | }, 24 | Link: 'a', 25 | }; 26 | }); 27 | 28 | describe('sign/auth page', () => { 29 | const RawTxDetail = () => <div>RawTxDetail</div>; 30 | const message = { 31 | type: MESSAGE_TYPE.EXTERNAL_SIGN, 32 | data: { tx: { version: '0x0' } }, 33 | }; 34 | beforeEach(() => { 35 | render( 36 | <IntlProvider locale="en" messages={en}> 37 | <Router> 38 | <App RawTxDetail={RawTxDetail} message={message} /> 39 | </Router> 40 | </IntlProvider>, 41 | ); 42 | }); 43 | 44 | it('should render form fields: submitbutton', async () => { 45 | const submitButton = screen.getByRole('button', { name: /confirm/i }); 46 | expect(submitButton).toBeInTheDocument(); 47 | }); 48 | 49 | it('should change form fields: password', async () => { 50 | const password = screen.getByLabelText('Password'); 51 | 52 | expect(password).toBeInTheDocument(); 53 | expect(password).toBeEmpty(); 54 | 55 | await userEvent.type(password, 'test password'); 56 | 57 | expect(screen.getByRole('form')).toHaveFormValues({ 58 | password: 'test password', 59 | }); 60 | }); 61 | 62 | it('should submit', async () => { 63 | const password = screen.getByLabelText('Password'); 64 | await userEvent.type(password, 'password_1'); 65 | expect(screen.getByRole('form')).toHaveFormValues({ 66 | password: 'password_1', 67 | }); 68 | const confirmBtn = screen.getByText('Confirm'); 69 | expect(confirmBtn).toBeInTheDocument(); 70 | userEvent.click(confirmBtn); 71 | await waitFor(() => { 72 | expect(browser.runtime.sendMessage).toBeCalled(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/common/networkManager/index.test.ts: -------------------------------------------------------------------------------- 1 | import { networks } from '@src/common/utils/constants/networks'; 2 | import NetworkManager from './index'; 3 | import { deprecatedNetworks, networks as networksFixture } from './fixtures/networks'; 4 | 5 | describe('network manager', () => { 6 | beforeEach(async () => { 7 | await NetworkManager.initNetworks(); 8 | }); 9 | afterEach(async () => { 10 | await NetworkManager.reset(); 11 | }); 12 | 13 | it('should return initial networks', async () => { 14 | const result = await NetworkManager.getNetworkList(); 15 | expect(result).toHaveLength(networks.length); 16 | }); 17 | 18 | it('should be able to reset networks', async () => { 19 | await NetworkManager.reset(); 20 | const result = await NetworkManager.getNetworkList(); 21 | expect(result).toHaveLength(networks.length); 22 | }); 23 | 24 | it('should able to update deprecated networks', async () => { 25 | await browser.storage.local.set({ 26 | networks: deprecatedNetworks, 27 | }); 28 | 29 | await NetworkManager.initNetworks(); 30 | 31 | const result = await NetworkManager.getNetworkList(); 32 | expect(result).toHaveLength(deprecatedNetworks.length); 33 | }); 34 | 35 | it('should able to set current network', async () => { 36 | const result = await NetworkManager.getCurrentNetwork(); 37 | 38 | expect(result).toBe(networks[0]); 39 | 40 | await NetworkManager.setCurrentNetwork(networks[0].title); 41 | expect(await NetworkManager.getCurrentNetwork()).toBe(networks[0]); 42 | 43 | await NetworkManager.setCurrentNetwork(networks[1].title); 44 | expect(await NetworkManager.getCurrentNetwork()).toBe(networks[1]); 45 | }); 46 | 47 | it('should able to create a network', async () => { 48 | const result = await NetworkManager.createNetwork({ 49 | title: 'Lina Mainnet 2', 50 | networkType: 'mainnet', 51 | prefix: 'ckb', 52 | nodeURL: 'http://mainnet.getsynapse.io/rpc', 53 | cacheURL: 'http://mainnet.getsynapse.io/api', 54 | }); 55 | 56 | expect(result).toHaveLength(networks.length); 57 | }); 58 | 59 | it('should able to get network info', async () => { 60 | const result = await NetworkManager.getNetwork('Lina Mainnet'); 61 | expect(result).toBe(networks[0]); 62 | }); 63 | 64 | it('should able to remove a network', async () => { 65 | const result = await NetworkManager.removeNetwork('Lina Mainnet'); 66 | expect(result).toHaveLength(networks.length); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/background/wallet/tests/keystore.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceAddresses } from '@src/tests/fixture/address'; 2 | import * as Keystore from '../keystore'; 3 | 4 | const enum KDFFunctions { 5 | PBKDF = 'pbkdf2', 6 | Scrypt = 'scrypt', 7 | } 8 | 9 | describe('load and check password', () => { 10 | const password = 'hello~!23'; 11 | const keystore = Keystore.encrypt(Buffer.from(aliceAddresses.privateKey), password); 12 | const keystoreKDF = Keystore.encrypt(Buffer.from(aliceAddresses.privateKey), password, { 13 | kdf: KDFFunctions.PBKDF, 14 | }); 15 | 16 | it('decrypts', () => { 17 | expect(Keystore.decrypt(keystore, password)).toEqual( 18 | '307831346366616538346337313666383935393532363334633234306462383062613030623131303332653561633966643136363365336137323333633764383065', 19 | ); 20 | }); 21 | 22 | it('decrypts json', () => { 23 | expect(Keystore.decrypt(keystoreKDF, password)).toEqual( 24 | '307831346366616538346337313666383935393532363334633234306462383062613030623131303332653561633966643136363365336137323333633764383065', 25 | ); 26 | }); 27 | 28 | it('checks wrong password for scrypt', () => { 29 | expect(Keystore.checkPasswd(keystore, `oops${password}`)).toBe(false); 30 | }); 31 | 32 | it('checks correct password for scrypt', () => { 33 | expect(Keystore.checkPasswd(keystore, password)).toBe(true); 34 | }); 35 | 36 | it('checks wrong password for pbkdf2', () => { 37 | expect(Keystore.checkPasswd(keystoreKDF, `oops${password}`)).toBe(false); 38 | }); 39 | 40 | it('checks correct password for pbkdf2', () => { 41 | expect(Keystore.checkPasswd(keystoreKDF, password)).toBe(true); 42 | }); 43 | 44 | it('checks scrypt password for error', () => { 45 | const keystoreNothing = keystore; 46 | keystoreNothing.crypto.kdf = '123'; 47 | expect(() => Keystore.checkPasswd(keystoreNothing, password)).toThrow(); 48 | }); 49 | 50 | it('checks version', () => { 51 | const keystoreNothing = keystore; 52 | keystoreNothing.version = 2; 53 | expect(() => Keystore.checkPasswd(keystoreNothing, password)).toThrow(); 54 | }); 55 | 56 | it('checks kdf password for error', () => { 57 | const keystoreNothing = keystoreKDF; 58 | keystoreNothing.crypto.kdfparams = { 59 | dklen: 32, 60 | salt: '6c849a1d5040e8fa031eb5e4bcab6db00f18e00408afc02ac1464a237b879af0', 61 | c: 262144, 62 | prf: 'hmac-sha512', 63 | }; 64 | expect(() => Keystore.checkPasswd(keystoreNothing, password)).toThrow(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/common/utils/tests/formatters/shannonToCKBFormatter/fixtures.ts: -------------------------------------------------------------------------------- 1 | const fixtures = [ 2 | { 3 | shannons: 'a', 4 | expected: 'a', 5 | }, 6 | { 7 | shannons: null, 8 | expected: '0', 9 | }, 10 | { 11 | shannons: '123', 12 | expected: '0.00000123', 13 | }, 14 | { 15 | shannons: '12300000', 16 | expected: '0.123', 17 | }, 18 | { 19 | shannons: '123000000', 20 | expected: '1.23', 21 | }, 22 | { 23 | shannons: '000123000000', 24 | expected: '1.23', 25 | }, 26 | { 27 | shannons: '1234567890123456789012345678901234567890123456789012345678901234567890123400000000', 28 | expected: 29 | '12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234', 30 | }, 31 | { 32 | shannons: '12345678901234567890123456789012345678901234567890123456789012345678901234', 33 | expected: 34 | '123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234', 35 | }, 36 | { 37 | shannons: '1234567890123456789012345678901234567890123456789012345678901234567890123400', 38 | expected: 39 | '12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234', 40 | }, 41 | 42 | { 43 | shannons: '-123', 44 | expected: '-0.00000123', 45 | }, 46 | { 47 | shannons: '-12300000', 48 | expected: '-0.123', 49 | }, 50 | { 51 | shannons: '-123000000', 52 | expected: '-1.23', 53 | }, 54 | { 55 | shannons: '-000123000000', 56 | expected: '-1.23', 57 | }, 58 | { 59 | shannons: '-1234567890123456789012345678901234567890123456789012345678901234567890123400000000', 60 | expected: 61 | '-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234', 62 | }, 63 | { 64 | shannons: '-12345678901234567890123456789012345678901234567890123456789012345678901234', 65 | expected: 66 | '-123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456.78901234', 67 | }, 68 | { 69 | shannons: '-1234567890123456789012345678901234567890123456789012345678901234567890123400', 70 | expected: 71 | '-12,345,678,901,234,567,890,123,456,789,012,345,678,901,234,567,890,123,456,789,012,345,678.901234', 72 | }, 73 | { 74 | shannons: '0', 75 | expected: '0', 76 | }, 77 | { 78 | shannons: '-0', 79 | expected: '0', 80 | }, 81 | { 82 | shannons: '', 83 | expected: '0', 84 | }, 85 | ]; 86 | export default fixtures; 87 | -------------------------------------------------------------------------------- /e2e/importMnemonic.test.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import path from 'path'; 3 | 4 | jest.setTimeout(300000); 5 | 6 | // Path to the actual extension we want to be testing 7 | const pathToExtension = require('path').join(path.join(__dirname, '..', 'dist')); 8 | 9 | // Tell puppeteer we want to load the web extension 10 | const puppeteerArgs = [ 11 | `--disable-extensions-except=${pathToExtension}`, 12 | `--load-extension=${pathToExtension}`, 13 | '--show-component-extension-options', 14 | ]; 15 | 16 | const extensionId = 'dbmnckdibkgoeppfmploopnghhgnnnmf'; 17 | const chromeExtPath = `chrome-extension://${extensionId}/popup.html`; 18 | 19 | describe('Import Mnemonic', () => { 20 | let page: puppeteer.Page; 21 | let browser: puppeteer.Browser; 22 | 23 | beforeAll(async () => { 24 | browser = await puppeteer.launch({ 25 | // headless: false, 26 | headless: true, 27 | slowMo: 250, 28 | devtools: true, 29 | args: puppeteerArgs, 30 | }); 31 | 32 | // Creates a new tab 33 | page = await browser.newPage(); 34 | 35 | await page.goto(chromeExtPath, { waitUntil: 'domcontentloaded' }); 36 | }); 37 | 38 | afterAll(async () => { 39 | // Tear down the browser 40 | await browser.close(); 41 | }); 42 | 43 | it('should render initial page normally', async () => { 44 | const header = await page.$('h6'); 45 | expect(header).not.toBeNull(); 46 | // FIXME: why it does not work? 47 | // await expect(page).toMatch('Synapse'); 48 | await expect(page.title()).resolves.toMatch('Synapse extension'); 49 | const button = await page.$('#import-button'); 50 | expect(button).not.toBeNull(); 51 | }); 52 | 53 | it('should go to import mnemonic page', async () => { 54 | await page.click('#import-button'); 55 | const header = await page.$('h3'); 56 | expect(header).not.toBeNull(); 57 | }); 58 | 59 | it('should import mnemonic correctly', async () => { 60 | await page.type( 61 | '[name="mnemonic"]', 62 | 'gym cycle pool joke bamboo airport ridge choose vote raw perfect bus', 63 | ); 64 | await page.type('[name="password"]', '111111'); 65 | await page.type('[name="confirmPassword"]', '111111'); 66 | 67 | await page.click('#submit-button'); 68 | }); 69 | 70 | it('should go to address page', async () => { 71 | const addressElem = await page.$('[data-testid="address-info"]'); 72 | expect(addressElem).not.toBeNull(); 73 | // FIXME: why it does not work? 74 | // await expect(page).toMatch('ckt1qyqgad...l56qum0yyn'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/background/keyper/setupKeyper.ts: -------------------------------------------------------------------------------- 1 | import { Secp256k1LockScript as Secp256k1LockScriptOriginal } from '@keyper/container/lib/locks/secp256k1'; 2 | import { SignatureAlgorithm } from '@keyper/specs'; 3 | import LOCKS_INFO, { NETWORKS } from '@common/utils/constants/locksInfo'; 4 | import ContainerManager from './containerManager'; 5 | import WalletManager from './walletManager'; 6 | import { Keccak256LockScript, AnypayLockScript, Secp256k1LockScript } from './locks'; 7 | import containerFactory from './containerFactory'; 8 | import signProvider from './signProviders/secp256k1'; 9 | 10 | export default async () => { 11 | const walletManager = WalletManager.getInstance(); 12 | const containerManager = ContainerManager.getInstance(); 13 | const publicKeys = await walletManager.getAllPublicKeys(); 14 | 15 | // create keyper container for each network 16 | NETWORKS.forEach((networkName) => { 17 | containerManager.addContainer({ 18 | name: networkName, 19 | container: containerFactory.createContainer(), 20 | }); 21 | }); 22 | const containers = containerManager.getAllContainers(); 23 | 24 | // add lock script for all containers 25 | Object.keys(containers).forEach((networkName) => { 26 | const container = containers[networkName]; 27 | // add lock script 28 | const original = new Secp256k1LockScriptOriginal(); 29 | original.setProvider(signProvider); 30 | const secp256k1LockScript = new Secp256k1LockScript( 31 | LOCKS_INFO[networkName].secp256k1.codeHash, 32 | LOCKS_INFO[networkName].secp256k1.txHash, 33 | LOCKS_INFO[networkName].secp256k1.hashType, 34 | original, 35 | ); 36 | 37 | const keccak256LockScript = new Keccak256LockScript( 38 | LOCKS_INFO[networkName].keccak256.codeHash, 39 | LOCKS_INFO[networkName].keccak256.txHash, 40 | LOCKS_INFO[networkName].keccak256.hashType, 41 | ); 42 | 43 | const anypayLockScript = new AnypayLockScript( 44 | LOCKS_INFO[networkName].anypay.codeHash, 45 | LOCKS_INFO[networkName].anypay.txHash, 46 | LOCKS_INFO[networkName].anypay.hashType, 47 | ); 48 | 49 | // secp256k1LockScript.setProvider(signProvider); 50 | keccak256LockScript.setProvider(signProvider); 51 | anypayLockScript.setProvider(signProvider); 52 | 53 | container.addLockScript(secp256k1LockScript); 54 | container.addLockScript(keccak256LockScript); 55 | container.addLockScript(anypayLockScript); 56 | publicKeys.forEach((publicKey) => { 57 | container.addPublicKey({ 58 | payload: publicKey, 59 | algorithm: SignatureAlgorithm.secp256k1, 60 | }); 61 | }); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const Dotenv = require('dotenv-webpack'); 4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | popup: path.join(__dirname, 'src/ui/index.tsx'), 9 | background: path.join(__dirname, 'src/background/index.ts'), 10 | contentScript: path.join(__dirname, 'src/contentScript/contentScript.ts'), 11 | injectedScript: path.join(__dirname, 'src/contentScript/inject/injectedScript.ts'), 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'dist/js'), 15 | filename: '[name].js', 16 | }, 17 | plugins: [ 18 | new CopyPlugin([ 19 | // prettier-ignore 20 | { 21 | from: './src/manifest.json', 22 | to: path.join(__dirname, 'dist'), 23 | }, 24 | { 25 | from: 'node_modules/webextension-polyfill/dist/browser-polyfill.min.js', 26 | to: path.join(__dirname, 'dist/js'), 27 | }, 28 | { 29 | from: './src/popup.html', 30 | to: path.join(__dirname, 'dist'), 31 | }, 32 | { 33 | from: './src/notification.html', 34 | to: path.join(__dirname, 'dist'), 35 | }, 36 | { 37 | from: './src/background.html', 38 | to: path.join(__dirname, 'dist'), 39 | }, 40 | { 41 | from: './src/ui/public/assets/logo-32.png', 42 | to: path.join(__dirname, 'dist'), 43 | }, 44 | { 45 | from: './src/ui/public/assets/logo-32.svg', 46 | to: path.join(__dirname, 'dist'), 47 | }, 48 | { 49 | from: './src/ui/public/assets/logo-128.png', 50 | to: path.join(__dirname, 'dist'), 51 | }, 52 | ]), 53 | new Dotenv(), 54 | ], 55 | module: { 56 | rules: [ 57 | // prettier-ignore 58 | { 59 | exclude: /node_modules/, 60 | test: /\.tsx?$/, 61 | use: 'ts-loader', 62 | }, 63 | { 64 | exclude: /node_modules/, 65 | test: /\.scss$/, 66 | use: [ 67 | // prettier-ignore 68 | { 69 | loader: 'style-loader', // Creates style nodes from JS strings 70 | }, 71 | { 72 | loader: 'css-loader', // Translates CSS into CommonJS 73 | }, 74 | { 75 | loader: 'sass-loader', // Compiles Sass to CSS 76 | }, 77 | ], 78 | }, 79 | ], 80 | }, 81 | resolve: { 82 | extensions: ['.ts', '.tsx', '.js'], 83 | plugins: [ 84 | new TsconfigPathsPlugin({ 85 | configFile: './tsconfig.json', 86 | }), 87 | ], 88 | }, 89 | }; -------------------------------------------------------------------------------- /src/background/wallet/address.ts: -------------------------------------------------------------------------------- 1 | import { AddressPrefix, AddressType as Type, pubkeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; 2 | import * as ckbUtils from '@nervosnetwork/ckb-sdk-utils'; 3 | import { ckbAccountPath } from '@common/utils/constants'; 4 | 5 | export { AddressPrefix }; 6 | 7 | export enum AddressType { 8 | Receiving = 0, // External chain 9 | Change = 1, // Internal chain 10 | } 11 | 12 | export const publicKeyToAddress = (publicKey: string, prefix = AddressPrefix.Testnet) => { 13 | const pubkey = publicKey.startsWith('0x') ? publicKey : `0x${publicKey}`; 14 | return pubkeyToAddress(pubkey, { 15 | prefix, 16 | type: Type.HashIdx, 17 | codeHashOrCodeHashIndex: '0x00', 18 | }); 19 | }; 20 | 21 | export default class Address { 22 | publicKey?: string; 23 | 24 | address: string; 25 | 26 | path: string; // BIP44 path 27 | 28 | blake160: string; 29 | 30 | constructor(address: string, path: string = Address.pathForReceiving(0), blake160: string = '') { 31 | this.address = address; 32 | this.path = path; 33 | this.blake160 = blake160; 34 | } 35 | 36 | public static fromPublicKey = ( 37 | publicKey: string, 38 | path: string = Address.pathForReceiving(0), 39 | prefix: AddressPrefix = AddressPrefix.Testnet, 40 | ) => { 41 | const address = publicKeyToAddress(publicKey, prefix); 42 | const instance = new Address(address, path); 43 | instance.publicKey = publicKey; 44 | return instance; 45 | }; 46 | 47 | // path 默认可以不传 48 | // prefix 默认可以不传 49 | public static fromPrivateKey = ( 50 | privateKey: string, 51 | path: string = Address.pathForReceiving(0), 52 | prefix: AddressPrefix = AddressPrefix.Testnet, 53 | ) => { 54 | const publicKey = privateKey.startsWith('0x') 55 | ? ckbUtils.privateKeyToPublicKey(privateKey) 56 | : ckbUtils.privateKeyToPublicKey(`0x${privateKey}`); 57 | const instance = Address.fromPublicKey(publicKey, path, prefix); 58 | return instance; 59 | }; 60 | 61 | public static pathFor = (type: AddressType, index: number) => { 62 | return `${ckbAccountPath}/${type}/${index}`; 63 | }; 64 | 65 | public static pathForReceiving = (index: number) => { 66 | return Address.pathFor(AddressType.Receiving, index); 67 | }; 68 | 69 | public static pathForChange = (index: number) => { 70 | return Address.pathFor(AddressType.Change, index); 71 | }; 72 | 73 | public static toBlake160 = (publicKey: string) => { 74 | const val = ckbUtils.blake160(publicKey); 75 | return val; 76 | }; 77 | 78 | public getBlake160 = () => { 79 | return `0x${ckbUtils.blake160(`0x${this.publicKey}`, 'hex')}`; 80 | }; 81 | 82 | public publicKeyHash = () => { 83 | return this.getBlake160(); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/Edit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import * as Yup from 'yup'; 4 | import { useIntl } from 'react-intl'; 5 | import { Formik } from 'formik'; 6 | import PageNav from '@src/ui/Components/PageNav'; 7 | import { makeStyles } from '@material-ui/core'; 8 | import { useHistory } from 'react-router-dom'; 9 | import UDTForm from './Form'; 10 | 11 | const useStyles = makeStyles({ 12 | container: { 13 | margin: 20, 14 | fontSize: 12, 15 | }, 16 | link: {}, 17 | }); 18 | 19 | interface AppProps { 20 | match?: any; 21 | } 22 | 23 | export default function UDTEdit(props: AppProps) { 24 | const history = useHistory(); 25 | const classes = useStyles(); 26 | const intl = useIntl(); 27 | const typeHashPropsFromUrl = _.get(props, 'match.params.typeHash', ''); 28 | const [udt, setUdt] = React.useState(); 29 | 30 | React.useEffect(() => { 31 | const getUDT = async () => { 32 | const { udts = [] } = await browser.storage.local.get('udts'); 33 | const result = _.find(udts, (udtItem) => udtItem.typeHash === typeHashPropsFromUrl); 34 | setUdt(result); 35 | }; 36 | getUDT(); 37 | }, []); 38 | 39 | const onSubmit = async (values, { resetForm }) => { 40 | const { name, typeHash, decimal, symbol } = values; 41 | const { udts } = await browser.storage.local.get('udts'); 42 | const udtObj = { name, typeHash, decimal, symbol }; 43 | 44 | const udtInx = _.findIndex(udts, (udtItem) => { 45 | return udtItem.typeHash === typeHash; 46 | }); 47 | 48 | if (udtInx === -1) { 49 | udts.push(udtObj); 50 | } else { 51 | udts[udtInx] = udtObj; 52 | } 53 | 54 | await browser.storage.local.set({ udts }); 55 | resetForm({ values: { name: '', typeHash: '', decimal: '', symbol: '' } }); 56 | history.push('/udts'); 57 | }; 58 | 59 | if (!udt) return <div />; 60 | 61 | const initialValues = udt; 62 | const formElem = ( 63 | <Formik 64 | initialValues={initialValues} 65 | onSubmit={onSubmit} 66 | validationSchema={Yup.object().shape({ 67 | name: Yup.string().required(intl.formatMessage({ id: 'Required' })), 68 | typeHash: Yup.string().required(intl.formatMessage({ id: 'Required' })), 69 | decimal: Yup.string() 70 | .required(intl.formatMessage({ id: 'Required' })) 71 | .matches(/^[1-9][0-9]*$|^0$/, intl.formatMessage({ id: 'Invalid number' })), 72 | symbol: Yup.string().required(intl.formatMessage({ id: 'Required' })), 73 | })} 74 | > 75 | {UDTForm} 76 | </Formik> 77 | ); 78 | 79 | return ( 80 | <div> 81 | <PageNav to="/udts" title="Edit UDT" /> 82 | <div className={classes.container}>{formElem}</div> 83 | </div> 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/tests/edit.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { IntlProvider } from 'react-intl'; 6 | import en from '@common/locales/en'; 7 | import App from '../Edit'; 8 | 9 | const mockFunc = jest.fn(); 10 | 11 | jest.mock('react-router-dom', () => { 12 | // Require the original module to not be mocked... 13 | const originalModule = jest.requireActual('react-router-dom'); 14 | 15 | return { 16 | __esModule: true, 17 | ...originalModule, 18 | // add your noops here 19 | useParams: jest.fn(), 20 | useHistory: () => { 21 | return { push: mockFunc }; 22 | }, 23 | Link: 'a', 24 | }; 25 | }); 26 | 27 | describe('Edit UDT', () => { 28 | const match = { 29 | params: { 30 | typeHash: '0x123', 31 | }, 32 | }; 33 | beforeEach(async () => { 34 | await browser.storage.local.set({ 35 | udts: [ 36 | { 37 | decimal: '8', 38 | name: 'simpleUDT', 39 | typeHash: '0x123', 40 | symbol: 'UDT', 41 | }, 42 | ], 43 | }); 44 | await act(async () => { 45 | render( 46 | <IntlProvider locale="en" messages={en}> 47 | <Router> 48 | <App match={match} /> 49 | </Router> 50 | </IntlProvider>, 51 | ); 52 | }); 53 | }); 54 | 55 | it('should render form fields: submitbutton', async () => { 56 | const submitButton = screen.getByRole('button', { name: /Confirm/i }); 57 | expect(submitButton).toBeInTheDocument(); 58 | }); 59 | 60 | it('should create new udt', async () => { 61 | expect(screen.getByRole('form')).toHaveFormValues({ 62 | decimal: '8', 63 | name: 'simpleUDT', 64 | typeHash: '0x123', 65 | symbol: 'UDT', 66 | }); 67 | 68 | const name = screen.getByLabelText('UDT Name'); 69 | await userEvent.type(name, '123'); 70 | 71 | const typeHash = screen.getByLabelText('UDT Hash'); 72 | await userEvent.type(typeHash, '123'); 73 | 74 | const symbol = screen.getByLabelText('Symbol'); 75 | await userEvent.type(symbol, '123'); 76 | 77 | const decimal = screen.getByLabelText('Decimal'); 78 | await userEvent.type(decimal, '123'); 79 | 80 | expect(screen.getByRole('form')).toHaveFormValues({ 81 | decimal: '8123', 82 | name: 'simpleUDT123', 83 | typeHash: '0x123123', 84 | symbol: 'UDT123', 85 | }); 86 | 87 | const submitBtn = screen.getByRole('button', { name: /Confirm/i }); 88 | userEvent.click(submitBtn); 89 | await waitFor(() => { 90 | expect(browser.storage.local.set).toBeCalled(); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/ui/pages/ManageUDTs/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, useIntl } from 'react-intl'; 3 | import { Form } from 'formik'; 4 | import { Button, TextField } from '@material-ui/core'; 5 | 6 | const UDTForm = (props: any) => { 7 | const intl = useIntl(); 8 | 9 | const { values, touched, errors, handleChange, handleBlur, handleSubmit } = props; 10 | 11 | return ( 12 | <Form 13 | className="manage-contacts" 14 | id="manage-contacts" 15 | onSubmit={handleSubmit} 16 | aria-label="form" 17 | > 18 | <TextField 19 | size="small" 20 | label={intl.formatMessage({ id: 'UDT Name' })} 21 | id="name" 22 | name="name" 23 | type="text" 24 | fullWidth 25 | value={values.name} 26 | onChange={handleChange} 27 | onBlur={handleBlur} 28 | error={!!errors.name} 29 | helperText={errors.name && touched.name && errors.name} 30 | margin="normal" 31 | variant="outlined" 32 | data-testid="field-name" 33 | /> 34 | <TextField 35 | size="small" 36 | label={intl.formatMessage({ id: 'UDT Hash' })} 37 | id="typeHash" 38 | name="typeHash" 39 | type="text" 40 | fullWidth 41 | value={values.typeHash} 42 | onChange={handleChange} 43 | onBlur={handleBlur} 44 | error={!!errors.typeHash} 45 | helperText={errors.typeHash && touched.typeHash && errors.typeHash} 46 | margin="normal" 47 | variant="outlined" 48 | data-testid="field-typeHash" 49 | /> 50 | <TextField 51 | size="small" 52 | label={intl.formatMessage({ id: 'Decimal' })} 53 | id="decimal" 54 | name="decimal" 55 | type="text" 56 | fullWidth 57 | value={values.decimal} 58 | onChange={handleChange} 59 | onBlur={handleBlur} 60 | error={!!errors.decimal} 61 | helperText={errors.decimal && touched.decimal && errors.decimal} 62 | margin="normal" 63 | variant="outlined" 64 | data-testid="field-decimal" 65 | /> 66 | <TextField 67 | size="small" 68 | label={intl.formatMessage({ id: 'Symbol' })} 69 | id="symbol" 70 | name="symbol" 71 | type="text" 72 | fullWidth 73 | value={values.symbol} 74 | onChange={handleChange} 75 | onBlur={handleBlur} 76 | error={!!errors.symbol} 77 | helperText={errors.symbol && touched.symbol && errors.symbol} 78 | margin="normal" 79 | variant="outlined" 80 | data-testid="field-symbol" 81 | /> 82 | <Button 83 | type="submit" 84 | id="submit-button" 85 | color="primary" 86 | variant="contained" 87 | data-testid="submit-button" 88 | > 89 | <FormattedMessage id="Confirm" /> 90 | </Button> 91 | </Form> 92 | ); 93 | }; 94 | 95 | export default UDTForm; 96 | --------------------------------------------------------------------------------