diff --git a/lerna.json b/lerna.json index acd5b2ab0f..42c5fa1074 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index fca721b6fb..64ea576f71 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@nervosnetwork/neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index 87bfef97ce..131dbfc9d0 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "@nervosnetwork/neuron-ui", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "private": true, "author": { "name": "Nervos Core Dev", @@ -42,6 +42,7 @@ "@nervosnetwork/ckb-sdk-core": "0.15.1", "@uifabric/experiments": "7.4.2", "@uifabric/styling": "7.1.1", + "canvg": "2.0.0", "grommet-icons": "4.2.0", "i18next": "15.1.3", "office-ui-fabric-react": "7.6.1", diff --git a/packages/neuron-ui/src/components/Addresses/index.tsx b/packages/neuron-ui/src/components/Addresses/index.tsx index 885edd39a9..4e3c3986f3 100644 --- a/packages/neuron-ui/src/components/Addresses/index.tsx +++ b/packages/neuron-ui/src/components/Addresses/index.tsx @@ -17,6 +17,7 @@ import { appCalls } from 'services/UILayer' import { useLocalDescription } from 'utils/hooks' import { MIN_CELL_WIDTH, Routes } from 'utils/const' +import { shannonToCKBFormatter } from 'utils/formatters' const Addresses = ({ wallet: { id, addresses = [] }, @@ -77,6 +78,16 @@ const Addresses = ({ maxWidth: 450, isResizable: true, isCollapsible: false, + onRender: (item?: State.Address) => { + if (item) { + return ( + + {item.address} + + ) + } + return '-' + }, }, { name: 'addresses.description', @@ -118,6 +129,16 @@ const Addresses = ({ maxWidth: 250, isResizable: true, isCollapsible: false, + onRender: (item?: State.Address) => { + if (item) { + return ( + + {`${shannonToCKBFormatter(item.balance)} CKB`} + + ) + } + return '-' + }, }, { name: 'addresses.transactions', @@ -127,6 +148,12 @@ const Addresses = ({ maxWidth: 150, isResizable: true, isCollapsible: false, + onRender: (item?: State.Address) => { + if (item) { + return (+item.txCount).toLocaleString() + } + return '-' + }, }, ], [onDescriptionChange, localDescription, onDescriptionFieldBlur, onDescriptionPress, t, semanticColors] @@ -148,6 +175,10 @@ const Addresses = ({ display: 'flex', alignItems: 'center', }, + '.text-overflow': { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, }, }, }} diff --git a/packages/neuron-ui/src/components/NetworkSetting/index.tsx b/packages/neuron-ui/src/components/NetworkSetting/index.tsx index 1583955792..6bc3a74e6c 100644 --- a/packages/neuron-ui/src/components/NetworkSetting/index.tsx +++ b/packages/neuron-ui/src/components/NetworkSetting/index.tsx @@ -44,7 +44,6 @@ const NetworkSetting = ({ key: network.id, text: network.name, checked: chain.networkID === network.id, - disabled: chain.networkID === network.id, onRenderLabel: ({ text }: IChoiceGroupOption) => { return ( diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index b7cc195419..40215f9105 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -18,7 +18,7 @@ import { import { StateWithDispatch } from 'states/stateProvider/reducer' import actionCreators from 'states/stateProvider/actionCreators' -import { localNumberFormatter } from 'utils/formatters' +import { localNumberFormatter, shannonToCKBFormatter } from 'utils/formatters' import { PAGE_SIZE, MIN_CELL_WIDTH } from 'utils/const' const timeFormatter = new Intl.DateTimeFormat(undefined, { @@ -155,7 +155,14 @@ const Overview = ({ { key: 'value', name: t('overview.amount'), + title: 'value', minWidth: 2 * MIN_CELL_WIDTH, + onRender: (item?: State.Transaction) => { + if (item) { + return {`${shannonToCKBFormatter(item.value)} CKB`} + } + return '-' + }, }, ].map( (col): IColumn => ({ @@ -193,7 +200,10 @@ const Overview = ({ const balanceItems = useMemo( () => [ - { label: t('overview.amount'), value: balance }, + { + label: t('overview.amount'), + value: {`${shannonToCKBFormatter(balance)} CKB`}, + }, { label: t('overview.live-cells'), value: 'mock living cells' }, { label: t('overview.cell-types'), value: 'mock cell typ' }, ], @@ -212,8 +222,17 @@ const Overview = ({ ) return ( - - + + {t('overview.balance')} @@ -229,7 +248,14 @@ const Overview = ({ ) : null} - + {t('overview.recent-activities')} diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 3935bc7e4b..e5e27d3047 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -1,18 +1,12 @@ import React, { useState, useCallback, useMemo } from 'react' import { RouteComponentProps } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Stack, Text, TextField, TooltipHost, Modal } from 'office-ui-fabric-react' +import { Stack, Text, TextField, TooltipHost, Modal, FontSizes } from 'office-ui-fabric-react' import { StateWithDispatch } from 'states/stateProvider/reducer' import QRCode from 'widgets/QRCode' import { Copy } from 'grommet-icons' -declare global { - interface Window { - clipboard: any - } -} - const Receive = ({ wallet: { addresses = [] }, match: { params }, @@ -37,24 +31,35 @@ const Receive = ({ } return ( - - setShowLargeQRCode(true)} style={{ alignSelf: 'center' }}> - - - - - - - - - + <> + + + + + + + + + + + + setShowLargeQRCode(true)} size={256} exportable /> + + setShowLargeQRCode(false)}> - + ) } diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index a4c81d9277..bcf1f0e421 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -20,6 +20,7 @@ import { StateWithDispatch } from 'states/stateProvider/reducer' import appState from 'states/initStates/app' import { PlaceHolders, CapacityUnit } from 'utils/const' +import { shannonToCKBFormatter } from 'utils/formatters' import { useInitialize } from './hooks' @@ -143,7 +144,7 @@ const Send = ({ -
{`${t('send.balance')}: ${balance}`}
+
{`${t('send.balance')}: ${shannonToCKBFormatter(balance)} CKB`}
diff --git a/packages/neuron-ui/src/components/TransactionList/index.tsx b/packages/neuron-ui/src/components/TransactionList/index.tsx index d4608b88ab..1a20ec5d0a 100644 --- a/packages/neuron-ui/src/components/TransactionList/index.tsx +++ b/packages/neuron-ui/src/components/TransactionList/index.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { + Stack, Text, DetailsList, TextField, @@ -8,16 +9,19 @@ import { IGroup, CheckboxVisibility, ITextFieldStyleProps, + getTheme, } from 'office-ui-fabric-react' import { StateDispatch } from 'states/stateProvider/reducer' import { appCalls } from 'services/UILayer' import { useLocalDescription } from 'utils/hooks' +import { shannonToCKBFormatter } from 'utils/formatters' const timeFormatter = new Intl.DateTimeFormat('en-GB') +const theme = getTheme() -const MIN_CELL_WIDTH = 70 +const MIN_CELL_WIDTH = 50 interface FormatTransaction extends State.Transaction { date: string @@ -25,7 +29,20 @@ interface FormatTransaction extends State.Transaction { const onRenderHeader = ({ group }: any) => { const { name } = group - return {name} + return ( + + {name} + + ) } const TransactionList = ({ @@ -56,13 +73,13 @@ const TransactionList = ({ const transactionColumns: IColumn[] = useMemo( (): IColumn[] => [ - { name: t('history.type'), key: 'type', fieldName: 'type', minWidth: MIN_CELL_WIDTH, maxWidth: 150 }, + { name: t('history.type'), key: 'type', fieldName: 'type', minWidth: MIN_CELL_WIDTH, maxWidth: 50 }, { name: t('history.timestamp'), key: 'timestamp', fieldName: 'timestamp', - minWidth: MIN_CELL_WIDTH, - maxWidth: 150, + minWidth: 80, + maxWidth: 80, onRender: (item?: FormatTransaction) => { return item ? {new Date(+(item.timestamp || item.createdAt)).toLocaleTimeString()} : null }, @@ -71,8 +88,18 @@ const TransactionList = ({ name: t('history.transaction-hash'), key: 'hash', fieldName: 'hash', - minWidth: 300, - maxWidth: 450, + minWidth: 100, + maxWidth: 500, + onRender: (item?: FormatTransaction) => { + if (item) { + return ( + + {item.hash} + + ) + } + return '-' + }, }, { name: t('history.status'), key: 'status', fieldName: 'status', minWidth: MIN_CELL_WIDTH, maxWidth: 50 }, { @@ -102,7 +129,23 @@ const TransactionList = ({ ) : null }, }, - { name: t('history.amount'), key: 'value', fieldName: 'value', minWidth: MIN_CELL_WIDTH, maxWidth: 300 }, + { + name: t('history.amount'), + key: 'value', + fieldName: 'value', + minWidth: 100, + maxWidth: 300, + onRender: (item?: FormatTransaction) => { + if (item) { + return ( + + {`${shannonToCKBFormatter(item.value)} CKB`} + + ) + } + return '-' + }, + }, ].map( (col): IColumn => ({ fieldName: col.key, ariaLabel: col.name, isResizable: true, isCollapsable: false, ...col }) ), @@ -161,6 +204,10 @@ const TransactionList = ({ display: 'flex', alignItems: 'center', }, + '.text-overflow': { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, }, }, }} diff --git a/packages/neuron-ui/src/components/WalletSetting/index.tsx b/packages/neuron-ui/src/components/WalletSetting/index.tsx index 697fb3b3f6..fe2c74a9e4 100644 --- a/packages/neuron-ui/src/components/WalletSetting/index.tsx +++ b/packages/neuron-ui/src/components/WalletSetting/index.tsx @@ -59,7 +59,6 @@ const WalletSetting = ({ key: wallet.id, text: wallet.name, checked: wallet.id === currentID, - disabled: wallet.id === currentID, onRenderLabel: ({ text }: IChoiceGroupOption) => { return ( diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts index 4cbdf93224..4c2e1c44f3 100644 --- a/packages/neuron-ui/src/containers/Main/hooks.ts +++ b/packages/neuron-ui/src/containers/Main/hooks.ts @@ -1,14 +1,31 @@ /* globals BigInt */ import { useEffect } from 'react' -import UILayer, { AppMethod, ChainMethod, NetworksMethod, TransactionsMethod, WalletsMethod } from 'services/UILayer' -import { ckbCore, getTipBlockNumber } from 'services/chain' -import { Routes, Channel, ConnectStatus } from 'utils/const' import { WalletWizardPath } from 'components/WalletWizard' import { NeuronWalletActions, StateDispatch, AppActions } from 'states/stateProvider/reducer' import { actionCreators } from 'states/stateProvider/actionCreators' import initStates from 'states/initStates' +import UILayer, { + AppMethod, + ChainMethod, + NetworksMethod, + TransactionsMethod, + WalletsMethod, + walletsCall, + transactionsCall, + networksCall, +} from 'services/UILayer' +import { ckbCore, getTipBlockNumber } from 'services/chain' +import { Routes, Channel, ConnectStatus } from 'utils/const' +import { + wallets as walletsCache, + networks as networksCache, + addresses as addressesCache, + currentNetworkID as currentNetworkIDCache, + currentWallet as currentWalletCache, +} from 'utils/localCache' + let timer: NodeJS.Timeout const SYNC_INTERVAL_TIME = 10000 @@ -16,8 +33,70 @@ const addressesToBalance = (addresses: State.Address[] = []) => { return addresses.reduce((total, addr) => total + BigInt(addr.balance || 0), BigInt(0)).toString() } -export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, dispatch: StateDispatch) => +export const useChannelListeners = ({ + walletID, + chain, + dispatch, + history, + i18n, +}: { + walletID: string + chain: State.Chain + dispatch: StateDispatch + history: any + i18n: any +}) => useEffect(() => { + UILayer.on( + Channel.DataUpdate, + ( + _e: Event, + _actionType: 'create' | 'update' | 'delete', + dataType: 'address' | 'transaction' | 'wallet' | 'network' + ) => { + switch (dataType) { + case 'address': { + walletsCall.getAllAddresses(walletID) + break + } + case 'transaction': { + transactionsCall.getAllByKeywords({ + walletID, + keywords: chain.transactions.keywords, + pageNo: chain.transactions.pageNo, + pageSize: chain.transactions.pageSize, + }) + transactionsCall.get(walletID, chain.transaction.hash) + break + } + case 'wallet': { + walletsCall.getAll() + walletsCall.getCurrent() + break + } + case 'network': { + networksCall.getAll() + networksCall.currentID() + break + } + default: { + walletsCall.getCurrent() + walletsCall.getAll() + walletsCall.getAllAddresses(walletID) + networksCall.currentID() + networksCall.getAll() + transactionsCall.getAllByKeywords({ + walletID, + keywords: chain.transactions.keywords, + pageNo: chain.transactions.pageNo, + pageSize: chain.transactions.pageSize, + }) + transactionsCall.get(walletID, chain.transaction.hash) + break + } + } + } + ) UILayer.on( Channel.Initiate, ( @@ -69,6 +148,12 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, transactions: { ...chain.transactions, ...transactions }, }, }) + + currentWalletCache.save(wallet) + currentNetworkIDCache.save(networkID) + walletsCache.save(wallets) + addressesCache.save(addresses) + networksCache.save(networks) } else { /* eslint-disable no-alert */ // TODO: better prompt, prd required @@ -221,6 +306,7 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, type: NeuronWalletActions.Settings, payload: { wallets: args.result }, }) + walletsCache.save(args.result) if (!args.result.length) { history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) } @@ -231,6 +317,7 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, type: NeuronWalletActions.Wallet, payload: args.result, }) + currentWalletCache.save(args.result) break } case WalletsMethod.SendCapacity: { @@ -257,6 +344,7 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, balance: addressesToBalance(addresses), }, }) + addressesCache.save(addresses) break } case WalletsMethod.RequestPassword: { @@ -298,8 +386,9 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, case NetworksMethod.GetAll: { dispatch({ type: NeuronWalletActions.Settings, - payload: { networks: args.result }, + payload: { networks: args.result || [] }, }) + networksCache.save(args.result || []) break } case NetworksMethod.CurrentID: { @@ -307,6 +396,7 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, type: NeuronWalletActions.Chain, payload: { networkID: args.result }, }) + currentNetworkIDCache.save(args.result) break } case NetworksMethod.Create: @@ -336,7 +426,7 @@ export const useChannelListeners = (i18n: any, history: any, chain: State.Chain, }) } }) - }, [i18n, chain, dispatch, history]) + }, [walletID, i18n, chain, dispatch, history]) export const useSyncTipBlockNumber = ({ networks, @@ -374,7 +464,27 @@ export const useSyncTipBlockNumber = ({ }, [networks, networkID, dispatch]) } +export const useOnCurrentWalletChange = ({ walletID, chain }: { walletID: string; chain: State.Chain }) => { + useEffect(() => { + walletsCall.getAllAddresses(walletID) + transactionsCall.getAllByKeywords({ + walletID, + keywords: chain.transactions.keywords, + pageNo: chain.transactions.pageNo, + pageSize: chain.transactions.pageSize, + }) + transactionsCall.get(walletID, chain.transaction.hash) + }, [ + walletID, + chain.transactions.pageNo, + chain.transactions.pageSize, + chain.transactions.keywords, + chain.transaction.hash, + ]) +} + export default { useChannelListeners, useSyncTipBlockNumber, + useOnCurrentWalletChange, } diff --git a/packages/neuron-ui/src/containers/Main/index.tsx b/packages/neuron-ui/src/containers/Main/index.tsx index 3fb4cb5f2d..d1766f7049 100644 --- a/packages/neuron-ui/src/containers/Main/index.tsx +++ b/packages/neuron-ui/src/containers/Main/index.tsx @@ -20,7 +20,7 @@ import PasswordRequest from 'components/PasswordRequest' import { Routes } from 'utils/const' -import { useChannelListeners, useSyncTipBlockNumber } from './hooks' +import { useChannelListeners, useSyncTipBlockNumber, useOnCurrentWalletChange } from './hooks' export const mainContents: CustomRouter.Route[] = [ { @@ -108,12 +108,22 @@ const MainContent = ({ }: React.PropsWithoutRef<{ dispatch: StateDispatch } & RouteComponentProps>) => { const neuronWalletState = useState() const [, i18n] = useTranslation() - useChannelListeners(i18n, history, neuronWalletState.chain, dispatch) + useChannelListeners({ + walletID: neuronWalletState.wallet.id, + chain: neuronWalletState.chain, + dispatch, + history, + i18n, + }) useSyncTipBlockNumber({ networkID: neuronWalletState.chain.networkID, networks: neuronWalletState.settings.networks, dispatch, }) + useOnCurrentWalletChange({ + walletID: neuronWalletState.wallet.id, + chain: neuronWalletState.chain, + }) return ( <> {mainContents.map(container => ( diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index f321655dd1..3528caf284 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -71,8 +71,8 @@ "receive": { "click-to-copy": "Click to copy the address", "address-not-found": "Address not found", - "prompt": "Neuron will update the receiving address after transaction for the security sake. Please go to the advance view if used receiving addresses are needed", - "address-qrcode": "Address QRCode" + "prompt": "Neuron picks a new receiving address for better privacy. Please go to the Address Book if you want to use a previously used receiving addresses.", + "address-qrcode": "Address QR Code" }, "history": { "meta": "Meta", @@ -160,6 +160,10 @@ "title": "Backup the {{name}} wallet" } }, + "qrcode": { + "copy": "Copy image", + "save": "Save image" + }, "footer": { "fail-to-fetch-tip-block-number": "Cannot fetch tip block number" }, diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 5627620fc5..41cc71b1c9 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -71,7 +71,7 @@ "receive": { "click-to-copy": "点击复制地址", "address-not-found": "未找到地址", - "prompt": "为了保护隐私,Neuron 会在每次收到转账后更新收款地址。如果您想引用用旧的收款地址,请访问高级页面。", + "prompt": "为了保护隐私,Neuron 会怎么选择一个新收款地址。如果您想使用旧的收款地址,请访问地址管理页面。", "address-qrcode": "地址二维码" }, "history": { @@ -160,6 +160,10 @@ "title": "备份钱包 {{name}}" } }, + "qrcode": { + "copy": "复制图片", + "save": "保存图片" + }, "footer": { "fail-to-fetch-tip-block-number": "无法获取最新高度" }, diff --git a/packages/neuron-ui/src/services/UILayer.ts b/packages/neuron-ui/src/services/UILayer.ts index 4b9c8164c4..1e30928ee2 100644 --- a/packages/neuron-ui/src/services/UILayer.ts +++ b/packages/neuron-ui/src/services/UILayer.ts @@ -32,6 +32,7 @@ export enum WalletsMethod { AllAddresses = 'allAddresses', UpdateAddressDescription = 'updateAddressDescription', RequestPassword = 'requestPassword', + GetAllAddresses = 'getAllAddresses', } export enum NetworksMethod { @@ -104,7 +105,7 @@ export const networksCall = instantiateMethodCall(networks) as { create: (network: State.NetworkProperty) => void update: (id: string, options: Partial) => void delete: (id: string) => void - currentOne: () => void + currentID: () => void activate: (id: string) => void } @@ -163,6 +164,7 @@ export const walletsCall = instantiateMethodCall(wallets) as { fee: string description: string }) => void + getAllAddresses: (id: string) => void updateAddressDescription: (params: { walletID: string; address: string; description: string }) => void } diff --git a/packages/neuron-ui/src/states/initStates/chain.ts b/packages/neuron-ui/src/states/initStates/chain.ts index 9d0ff5e592..d177d00ed2 100644 --- a/packages/neuron-ui/src/states/initStates/chain.ts +++ b/packages/neuron-ui/src/states/initStates/chain.ts @@ -1,5 +1,7 @@ +import { currentNetworkID } from 'utils/localCache' + const chainState: State.Chain = { - networkID: '', + networkID: currentNetworkID.load(), connectStatus: 'offline', tipBlockNumber: '', transaction: { diff --git a/packages/neuron-ui/src/states/initStates/settings.ts b/packages/neuron-ui/src/states/initStates/settings.ts index f675580f64..3b1bb12a22 100644 --- a/packages/neuron-ui/src/states/initStates/settings.ts +++ b/packages/neuron-ui/src/states/initStates/settings.ts @@ -1,9 +1,9 @@ -import { addressBook } from 'utils/localCache' +import { addressBook, wallets, networks } from 'utils/localCache' export const settingsState: State.Settings = { showAddressBook: addressBook.isVisible(), - networks: [], - wallets: [], + networks: networks.load(), + wallets: wallets.load(), } export default settingsState diff --git a/packages/neuron-ui/src/states/initStates/wallet.ts b/packages/neuron-ui/src/states/initStates/wallet.ts index 86cf813c87..7ce6e47bbb 100644 --- a/packages/neuron-ui/src/states/initStates/wallet.ts +++ b/packages/neuron-ui/src/states/initStates/wallet.ts @@ -1,8 +1,12 @@ +import { addresses, currentWallet } from 'utils/localCache' + +const wallet = currentWallet.load() + export const walletState: State.Wallet = { - name: '', - id: '', + name: wallet.name || '', + id: wallet.id || '', balance: '0', - addresses: [], + addresses: addresses.load(), sending: false, } diff --git a/packages/neuron-ui/src/types/global/index.d.ts b/packages/neuron-ui/src/types/global/index.d.ts index fbb57324b7..fe2d5e238f 100644 --- a/packages/neuron-ui/src/types/global/index.d.ts +++ b/packages/neuron-ui/src/types/global/index.d.ts @@ -3,6 +3,7 @@ declare interface Window { remote: any require: any bridge: any + nativeImage: any } declare module '*.json' { diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index d3f059d778..6e680b32a4 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -22,6 +22,7 @@ export enum Channel { Transactions = 'transactions', Wallets = 'wallets', Helpers = 'helpers', + DataUpdate = 'dataUpdate', } export enum Routes { diff --git a/packages/neuron-ui/src/utils/formatters.test.ts b/packages/neuron-ui/src/utils/formatters.test.ts index 072bcdb303..8b0c218954 100644 --- a/packages/neuron-ui/src/utils/formatters.test.ts +++ b/packages/neuron-ui/src/utils/formatters.test.ts @@ -1,49 +1,48 @@ import { CapacityUnit } from 'utils/const' -import { currencyFormatter, currencyCode, CKBToShannonFormatter } from 'utils/formatters' +import { currencyFormatter, currencyCode, CKBToShannonFormatter, shannonToCKBFormatter } from 'utils/formatters' -describe('formatters', () => { - it('currencyFormatter', () => { +describe(`formatters`, () => { + it(`currencyFormatter`, () => { const fixtures = [ { source: { - shannons: '1234567890', - unit: 'CKB' as currencyCode, - exchange: '0.000000001', + shannons: `1234567890`, + unit: `CKB` as currencyCode, + exchange: `0.000000001`, }, - target: '1.23456789 CKB', + target: `1.23456789 CKB`, }, { source: { - shannons: '1234567890', - unit: 'CKB' as currencyCode, - exchange: '0.00065', + shannons: `1234567890`, + unit: `CKB` as currencyCode, + exchange: `0.00065`, }, - target: '802,469.1285 CKB', + target: `802,469.1285 CKB`, }, { source: { - shannons: '1234567890', - unit: 'CNY' as currencyCode, - exchange: '0.00065', + shannons: `1234567890`, + unit: `CNY` as currencyCode, + exchange: `0.00065`, }, - target: '802,469.1285 CNY', + target: `802,469.1285 CNY`, }, { source: { - shannons: '1234567890123456789012345678901234567890123456789012345678901234567890', - unit: 'CNY' as currencyCode, - exchange: '0.65', + shannons: `1234567890123456789012345678901234567890123456789012345678901234567890`, + unit: `CNY` as currencyCode, + exchange: `0.65`, }, - target: '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', + target: `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`, }, { source: { - shannons: '12345678901234567890123456789012345678901234567890123456789012345678901234', - unit: 'CNY' as currencyCode, - exchange: '0.65', + shannons: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + unit: `CNY` as currencyCode, + exchange: `0.65`, }, - target: - '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', + target: `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`, }, ] fixtures.forEach(fixture => { @@ -52,75 +51,110 @@ describe('formatters', () => { }) }) - it('CKB Formatter', () => { - const fixtures = [ - { - source: { - amount: '1.234', - uint: CapacityUnit.CKB, + describe(`CKB Formatter`, () => { + it(`CKB to Shannon`, () => { + const fixtures = [ + { + source: { + amount: `1.234`, + uint: CapacityUnit.CKB, + }, + target: `123400000`, }, - target: '123400000', - }, - { - source: { - amount: '1.23456789', - uint: CapacityUnit.CKB, + { + source: { + amount: `1.23456789`, + uint: CapacityUnit.CKB, + }, + target: `123456789`, }, - target: '123456789', - }, - { - source: { - amount: '1.0', - uint: CapacityUnit.CKB, + { + source: { + amount: `1.0`, + uint: CapacityUnit.CKB, + }, + target: `100000000`, }, - target: '100000000', - }, - { - source: { - amount: '1.', - uint: CapacityUnit.CKB, + { + source: { + amount: `1.`, + uint: CapacityUnit.CKB, + }, + target: `100000000`, }, - target: '100000000', - }, - { - source: { - amount: '0.123', - uint: CapacityUnit.CKB, + { + source: { + amount: `0.123`, + uint: CapacityUnit.CKB, + }, + target: `12300000`, }, - target: '12300000', - }, - { - source: { - amount: '.123', - uint: CapacityUnit.CKB, + { + source: { + amount: `.123`, + uint: CapacityUnit.CKB, + }, + target: `12300000`, }, - target: '12300000', - }, - { - source: { - amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', - uint: CapacityUnit.CKB, + { + source: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + uint: CapacityUnit.CKB, + }, + target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, }, - target: '1234567890123456789012345678901234567890123456789012345678901234567890123400000000', - }, - { - source: { - amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', - uint: CapacityUnit.CKKB, + { + source: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + uint: CapacityUnit.CKKB, + }, + target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000`, }, - target: '1234567890123456789012345678901234567890123456789012345678901234567890123400000000000', - }, - { - source: { - amount: '12345678901234567890123456789012345678901234567890123456789012345678901234', - uint: CapacityUnit.CKGB, + { + source: { + amount: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + uint: CapacityUnit.CKGB, + }, + target: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000000000000`, }, - target: '1234567890123456789012345678901234567890123456789012345678901234567890123400000000000000000', - }, - ] + ] - fixtures.forEach(fixture => { - expect(CKBToShannonFormatter(fixture.source.amount, fixture.source.uint)).toBe(fixture.target) + fixtures.forEach(fixture => { + expect(CKBToShannonFormatter(fixture.source.amount, fixture.source.uint)).toBe(fixture.target) + }) + }) + + it(`shannon to CKB`, () => { + const fixtures = [ + { + source: `123`, + target: `0.00000123`, + }, + { + source: `12300000`, + target: `0.123`, + }, + { + source: `123000000`, + target: `1.23`, + }, + { + source: `1234567890123456789012345678901234567890123456789012345678901234567890123400000000`, + target: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + }, + { + source: `12345678901234567890123456789012345678901234567890123456789012345678901234`, + target: `123456789012345678901234567890123456789012345678901234567890123456.78901234`, + }, + { + source: `1234567890123456789012345678901234567890123456789012345678901234567890123400`, + target: `12345678901234567890123456789012345678901234567890123456789012345678.901234`, + }, + ] + + fixtures.forEach(fixture => { + expect(shannonToCKBFormatter(fixture.source)).toBe(fixture.target) + }) }) }) }) diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index ffd7d20425..9091d202d1 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -72,6 +72,21 @@ export const CKBToShannonFormatter = (amount: string, uint: CapacityUnit) => { } } +export const shannonToCKBFormatter = (shannon: string) => { + let ckbStr = [...shannon.padStart(8, '0')] + .map((char, idx) => { + if (idx === Math.max(shannon.length - 8, 0)) { + return `.${char}` + } + return char + }) + .join('') + if (ckbStr.startsWith('.')) { + ckbStr = `0${ckbStr}` + } + return ckbStr.replace(/\.?0+$/, '') +} + export const localNumberFormatter = (num: string | number = 0) => { return numberFormatter.format(+num) } @@ -80,5 +95,6 @@ export default { queryFormatter, currencyFormatter, CKBToShannonFormatter, + shannonToCKBFormatter, localNumberFormatter, } diff --git a/packages/neuron-ui/src/utils/localCache.ts b/packages/neuron-ui/src/utils/localCache.ts index 59cd751ca6..f8426fbe25 100644 --- a/packages/neuron-ui/src/utils/localCache.ts +++ b/packages/neuron-ui/src/utils/localCache.ts @@ -1,5 +1,10 @@ export enum LocalCacheKey { AddressBookVisibility = 'address-book-visibility', + Addresses = 'addresses', + Networks = 'networks', + Wallets = 'wallets', + CurrentWallet = 'currentWallet', + CurrentNetworkID = 'currentNetworkID', } enum AddressBookVisibility { Invisible = '0', @@ -20,7 +25,111 @@ export const addressBook = { }, } +export const addresses = { + save: (addressList: State.Address[]) => { + if (!Array.isArray(addressList)) { + return false + } + const addressesStr = JSON.stringify(addressList) + window.localStorage.setItem(LocalCacheKey.Addresses, addressesStr) + return true + }, + load: () => { + const addressesStr = window.localStorage.getItem(LocalCacheKey.Addresses) || `[]` + try { + const addressList = JSON.parse(addressesStr) + if (!Array.isArray(addressList)) { + throw new TypeError(`Addresses should be type fo Address[]`) + } + return addressList + } catch (err) { + console.error(err) + return [] + } + }, +} + +export const networks = { + save: (networkList: State.Network[]) => { + if (!Array.isArray(networkList)) { + return false + } + const networksStr = JSON.stringify(networkList) + window.localStorage.setItem(LocalCacheKey.Networks, networksStr) + return true + }, + load: () => { + const networksStr = window.localStorage.getItem(LocalCacheKey.Networks) || `[]` + try { + const networkList = JSON.parse(networksStr) + if (!Array.isArray(networkList)) { + throw new TypeError(`Networks should be type of Network[]`) + } + return networkList + } catch (err) { + console.error(err) + return [] + } + }, +} + +export const wallets = { + save: (walletList: State.WalletIdentity[]) => { + if (!Array.isArray(walletList)) { + return false + } + const walletsStr = JSON.stringify(walletList) + window.localStorage.setItem(LocalCacheKey.Wallets, walletsStr) + return true + }, + load: () => { + const walletsStr = window.localStorage.getItem(LocalCacheKey.Wallets) || `[]` + try { + const walletList = JSON.parse(walletsStr) + if (!Array.isArray(walletList)) { + throw new TypeError(`Wallets should be type of WalletIdentity[]`) + } + return walletList + } catch (err) { + console.error(err) + return [] + } + }, +} + +export const currentWallet = { + save: (wallet: State.WalletIdentity | null) => { + const walletStr = JSON.stringify({ id: '', name: '', ...wallet }) + window.localStorage.setItem(LocalCacheKey.CurrentWallet, walletStr) + return true + }, + load: (): { [index: string]: string } => { + const walletStr = window.localStorage.getItem(LocalCacheKey.CurrentWallet) || '{}' + try { + return JSON.parse(walletStr) + } catch (err) { + console.error(`Cannot parse current wallet`) + return {} + } + }, +} + +export const currentNetworkID = { + save: (networkID: string = '') => { + window.localStorage.setItem(LocalCacheKey.CurrentNetworkID, networkID) + return true + }, + load: () => { + return window.localStorage.getItem(LocalCacheKey.CurrentNetworkID) || '' + }, +} + export default { LocalCacheKey, addressBook, + addresses, + networks, + wallets, + currentWallet, + currentNetworkID, } diff --git a/packages/neuron-ui/src/widgets/QRCode/index.tsx b/packages/neuron-ui/src/widgets/QRCode/index.tsx index 60c539ab58..6b77bd126f 100644 --- a/packages/neuron-ui/src/widgets/QRCode/index.tsx +++ b/packages/neuron-ui/src/widgets/QRCode/index.tsx @@ -1,5 +1,8 @@ /* eslint-disable no-bitwise */ -import React from 'react' +import React, { useEffect, useRef, useCallback } from 'react' +import canvg from 'canvg' +import { Stack, DefaultButton } from 'office-ui-fabric-react' +import { useTranslation } from 'react-i18next' const QRCodeImpl = require('qr.js/lib/QRCode') @@ -70,36 +73,89 @@ const generatePath = (cells: boolean[][], margin: number = 0): string => { const QRCode = ({ value, size = 128, + scale = 4, level = ErrorCorrectLevel.Q, bgColor = '#FFF', fgColor = '#000', + onQRCodeClick, includeMargin = false, - ...otherProps + exportable = false, }: { value: string size: number + scale?: number level?: ErrorCorrectLevel bgColor?: string fgColor?: string + onQRCodeClick?: React.MouseEventHandler includeMargin?: boolean + exportable?: boolean }) => { + const [t] = useTranslation() const qrcode = new QRCodeImpl(-1, level) + const canvasRef = useRef(null) qrcode.addData(convertStr(value)) qrcode.make() - const cells = qrcode.modules - if (cells === null) { - return null - } + const cells = qrcode.modules || [] const margin = includeMargin ? 4 : 0 const fgPath = generatePath(cells, margin) const numCells = cells.length + margin * 2 + + const svgStr = `` + + const onDownload = useCallback(() => { + if (canvasRef.current === null) { + return + } + const dataURL = canvasRef.current.toDataURL('image/png') + const downloadLink = document.createElement('a') + downloadLink.download = 'Receive' + downloadLink.href = dataURL + window.document.body.appendChild(downloadLink) + downloadLink.click() + window.document.body.removeChild(downloadLink) + }, []) + + const onCopy = useCallback(() => { + if (canvasRef.current === null) { + return + } + const dataURL = canvasRef.current.toDataURL('image/png') + const img = window.nativeImage.createFromDataURL(dataURL) + window.clipboard.writeImage(img) + }, []) + + useEffect(() => { + if (canvasRef.current !== null) { + canvg(canvasRef.current, svgStr, { + enableRedraw: false, + ignoreMouse: true, + renderCallback: () => { + if (canvasRef.current) { + canvasRef.current.setAttribute(`style`, `width:${size}p;height:${size}px`) + } + }, + }) + } + }, [svgStr, size]) + return ( - - - - + + + + + {exportable ? ( + + {t('qrcode.copy')} + {t('qrcode.save')} + + ) : null} + ) } +QRCode.display = 'QRCode' + export default QRCode diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index 16713d5f3d..9a2281feda 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -3,7 +3,7 @@ "productName": "Neuron", "description": "CKB Neuron Wallet", "homepage": "https://www.nervos.org/", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "private": true, "author": { "name": "Nervos Core Dev", @@ -48,7 +48,7 @@ }, "devDependencies": { "@nervosnetwork/ckb-types": "0.15.1", - "@nervosnetwork/neuron-ui": "0.1.0-alpha.7", + "@nervosnetwork/neuron-ui": "0.1.0-alpha.8", "@types/async": "3.0.0", "@types/electron-devtools-installer": "2.2.0", "@types/elliptic": "6.4.8", diff --git a/packages/neuron-wallet/src/controllers/index.ts b/packages/neuron-wallet/src/controllers/index.ts index f4b2b33132..4ba08e834c 100644 --- a/packages/neuron-wallet/src/controllers/index.ts +++ b/packages/neuron-wallet/src/controllers/index.ts @@ -3,6 +3,7 @@ import NetworksController from './networks' import WalletsController from './wallets' import TransactionsController from './transactions' import HelpersController from './helpers' +import SyncInfoController from './sync-info' export default { AppController, @@ -10,4 +11,5 @@ export default { WalletsController, TransactionsController, HelpersController, + SyncInfoController, } diff --git a/packages/neuron-wallet/src/controllers/sync-info.ts b/packages/neuron-wallet/src/controllers/sync-info.ts new file mode 100644 index 0000000000..80519b4e7a --- /dev/null +++ b/packages/neuron-wallet/src/controllers/sync-info.ts @@ -0,0 +1,18 @@ +import { CatchControllerError } from '../decorators' +import BlockNumber from '../services/sync/block-number' +import { ResponseCode } from '../utils/const' + +export default class SyncInfoController { + @CatchControllerError + public static async currentBlockNumber() { + const blockNumber = new BlockNumber() + const current: bigint = await blockNumber.getCurrent() + + return { + status: ResponseCode.Success, + result: { + currentBlockNumber: current.toString(), + }, + } + } +} diff --git a/packages/neuron-wallet/src/controllers/wallets/index.ts b/packages/neuron-wallet/src/controllers/wallets/index.ts index 9730a203fc..3e61c64a99 100644 --- a/packages/neuron-wallet/src/controllers/wallets/index.ts +++ b/packages/neuron-wallet/src/controllers/wallets/index.ts @@ -274,6 +274,15 @@ export default class WalletsController { }) } + @CatchControllerError + public static async getCurrent() { + const currentWallet = WalletsService.getInstance().getCurrent() || null + return { + status: ResponseCode.Success, + result: currentWallet, + } + } + @CatchControllerError public static async activate(id: string) { const walletsService = WalletsService.getInstance() diff --git a/packages/neuron-wallet/src/database/chain/entities/transaction.ts b/packages/neuron-wallet/src/database/chain/entities/transaction.ts index 5b36c9e66f..8b0a664167 100644 --- a/packages/neuron-wallet/src/database/chain/entities/transaction.ts +++ b/packages/neuron-wallet/src/database/chain/entities/transaction.ts @@ -90,12 +90,14 @@ export default class Transaction extends BaseEntity { outputs!: OutputEntity[] public toInterface(): TransactionInterface { + const inputs = this.inputs ? this.inputs.map(input => input.toInterface()) : [] + const outputs = this.outputs ? this.outputs.map(output => output.toInterface()) : [] return { hash: this.hash, version: this.version, deps: this.deps, - inputs: this.inputs.map(input => input.toInterface()), - outputs: this.outputs.map(output => output.toInterface()), + inputs, + outputs, timestamp: this.timestamp, blockNumber: this.blockNumber, blockHash: this.blockHash, diff --git a/packages/neuron-wallet/src/models/subjects/data-update.ts b/packages/neuron-wallet/src/models/subjects/data-update.ts new file mode 100644 index 0000000000..636e5ab53c --- /dev/null +++ b/packages/neuron-wallet/src/models/subjects/data-update.ts @@ -0,0 +1,13 @@ +import { Subject } from 'rxjs' +import windowManager from '../window-manager' + +const DataUpdateSubject = new Subject<{ + dataType: 'address' | 'transaction' | 'wallet' | 'network' + actionType: 'create' | 'update' | 'delete' +}>() + +DataUpdateSubject.subscribe(({ dataType, actionType }) => { + windowManager.broadcastDataUpdateMessage(actionType, dataType) +}) + +export default DataUpdateSubject diff --git a/packages/neuron-wallet/src/models/subjects/networks.ts b/packages/neuron-wallet/src/models/subjects/networks.ts index ad1d58d566..56dcf54594 100644 --- a/packages/neuron-wallet/src/models/subjects/networks.ts +++ b/packages/neuron-wallet/src/models/subjects/networks.ts @@ -1,6 +1,6 @@ import { Subject } from 'rxjs' import { debounceTime } from 'rxjs/operators' -import { broadcastCurrentNetworkID, broadcastNetworkList } from '../../utils/broadcast' +import DataUpdateSubject from './data-update' const DEBOUNCE_TIME = 50 @@ -9,14 +9,12 @@ export const NetworkListSubject = new Subject<{ }>() export const CurrentNetworkIDSubject = new Subject<{ currentNetworkID: Controller.NetworkID }>() -NetworkListSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe( - ({ currentNetworkList = [] }: { currentNetworkList: Controller.Network[] }) => { - broadcastNetworkList(currentNetworkList) - } -) +NetworkListSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe(() => { + DataUpdateSubject.next({ dataType: 'network', actionType: 'update' }) +}) -CurrentNetworkIDSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe(({ currentNetworkID = '' }) => { - broadcastCurrentNetworkID(currentNetworkID) +CurrentNetworkIDSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe(() => { + DataUpdateSubject.next({ dataType: 'network', actionType: 'update' }) }) export default { diff --git a/packages/neuron-wallet/src/models/subjects/tx-db-changed-subject.ts b/packages/neuron-wallet/src/models/subjects/tx-db-changed-subject.ts index 982e7d053e..3d84741f79 100644 --- a/packages/neuron-wallet/src/models/subjects/tx-db-changed-subject.ts +++ b/packages/neuron-wallet/src/models/subjects/tx-db-changed-subject.ts @@ -1,11 +1,6 @@ import { ReplaySubject } from 'rxjs' -import { delay } from 'rxjs/operators' -import { ResponseCode, Channel } from '../../utils/const' import { Transaction } from '../../types/cell-types' -import windowManager from '../window-manager' -import TransactionsService from '../../services/transactions' -import AddressService from '../../services/addresses' -import LockUtils from '../lock-utils' +import DataUpdateSubject from './data-update' export interface TransactionChangedMessage { event: string @@ -27,24 +22,10 @@ export class TxDbChangedSubject { } static subscribe = () => { - // TODO: since typeorm not provide afterCommit hooks, delay for wait transaction committed - TxDbChangedSubject.subject.pipe(delay(100)).subscribe(async ({ tx }) => { - const transaction = await TransactionsService.get(tx.hash) - if (!transaction) { - return - } - const blake160s = TransactionsService.blake160sOfTx(transaction) - const addresses = blake160s.map(blake160 => LockUtils.blake160ToAddress(blake160)) - const addrs = await AddressService.findByAddresses(addresses) - const walletIDs = addrs.map(addr => addr.walletId) - const uniqueWalletIDs = [...new Set(walletIDs)] - const result = { - tx: transaction, - walletIDs: JSON.stringify(uniqueWalletIDs), - } - windowManager.broadcast(Channel.Transactions, 'transactionUpdated', { - status: ResponseCode.Success, - result, + TxDbChangedSubject.subject.subscribe(() => { + DataUpdateSubject.next({ + dataType: 'transaction', + actionType: 'update', }) }) } diff --git a/packages/neuron-wallet/src/models/subjects/wallets.ts b/packages/neuron-wallet/src/models/subjects/wallets.ts index 5d6ba19ed1..8e04885f46 100644 --- a/packages/neuron-wallet/src/models/subjects/wallets.ts +++ b/packages/neuron-wallet/src/models/subjects/wallets.ts @@ -1,12 +1,7 @@ import { Subject } from 'rxjs' import { debounceTime } from 'rxjs/operators' import { updateApplicationMenu } from '../../utils/application-menu' -import { - broadcastCurrentWallet, - broadcastWalletList, - broadcastAddressList, - broadcastTransactions, -} from '../../utils/broadcast' +import dataUpdateSubject from './data-update' const DEBOUNCE_TIME = 50 @@ -24,18 +19,16 @@ export const CurrentWalletSubject = new Subject<{ WalletListSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe(({ currentWallet = null, currentWalletList = [] }) => { const walletList = currentWalletList.map(({ id, name }) => ({ id, name })) const currentWalletId = currentWallet ? currentWallet.id : null - broadcastWalletList(walletList) + dataUpdateSubject.next({ dataType: 'wallet', actionType: 'update' }) updateApplicationMenu(walletList, currentWalletId) }) CurrentWalletSubject.pipe(debounceTime(DEBOUNCE_TIME)).subscribe(async ({ currentWallet = null, walletList = [] }) => { - broadcastCurrentWallet(currentWallet) updateApplicationMenu(walletList, currentWallet ? currentWallet.id : null) if (!currentWallet) { return } - broadcastAddressList(currentWallet.id) - broadcastTransactions(currentWallet.id) + dataUpdateSubject.next({ dataType: 'wallet', actionType: 'update' }) }) export default { diff --git a/packages/neuron-wallet/src/models/window-manager.ts b/packages/neuron-wallet/src/models/window-manager.ts index 5cd1c692cd..3efe4c079a 100644 --- a/packages/neuron-wallet/src/models/window-manager.ts +++ b/packages/neuron-wallet/src/models/window-manager.ts @@ -8,7 +8,7 @@ interface SendMessage { (channel: Channel.App, method: Controller.AppMethod, params: any): void ( channel: Channel.Wallets, - method: Controller.WalletsMethod | 'allAddresses' | 'sendingStatus' | 'getCurrent' | 'requestPassword', + method: Controller.WalletsMethod | 'allAddresses' | 'sendingStatus' | 'requestPassword', params: any ): void (channel: Channel.Transactions, method: Controller.TransactionsMethod | 'transactionUpdated', params: any): void @@ -31,6 +31,21 @@ export default class WindowManager { }) } + public static broadcastDataUpdateMessage = ( + actionType: 'create' | 'update' | 'delete', + dataType: 'address' | 'transaction' | 'wallet' | 'network' + ) => { + if (!BrowserWindow) { + logger.log(error) + return + } + BrowserWindow.getAllWindows().forEach(window => { + if (window && window.webContents) { + window.webContents.send(Channel.DataUpdate, actionType, dataType) + } + }) + } + public static sendToFocusedWindow: SendMessage = (channel: Channel, method: string, params: any): void => { if (!BrowserWindow) { logger.log(error) diff --git a/packages/neuron-wallet/src/services/wallets.ts b/packages/neuron-wallet/src/services/wallets.ts index 2916d4dc55..83a8483131 100644 --- a/packages/neuron-wallet/src/services/wallets.ts +++ b/packages/neuron-wallet/src/services/wallets.ts @@ -17,9 +17,9 @@ import Keychain from '../models/keys/keychain' import AddressDbChangedSubject from '../models/subjects/address-db-changed-subject' import AddressesUsedSubject from '../models/subjects/addresses-used-subject' import { WalletListSubject, CurrentWalletSubject } from '../models/subjects/wallets' -import { broadcastAddressList } from '../utils/broadcast' import { Channel, ResponseCode } from '../utils/const' import windowManager from '../models/window-manager' +import dataUpdateSubject from '../models/subjects/data-update' const { core } = NodeService.getInstance() const fileService = FileService.getInstance() @@ -139,11 +139,11 @@ export default class WalletService { AddressDbChangedSubject.getSubject() .pipe(debounceTime(DEBOUNCE_TIME)) - .subscribe(async () => { - const currentWallet = this.getCurrent() - if (currentWallet) { - broadcastAddressList(currentWallet.id) - } + .subscribe(() => { + dataUpdateSubject.next({ + dataType: 'address', + actionType: 'update', + }) }) } diff --git a/packages/neuron-wallet/src/startup/preload.ts b/packages/neuron-wallet/src/startup/preload.ts index 20d040a98b..c4c3549ea4 100644 --- a/packages/neuron-wallet/src/startup/preload.ts +++ b/packages/neuron-wallet/src/startup/preload.ts @@ -1,9 +1,10 @@ -import { ipcRenderer, clipboard } from 'electron' +import { ipcRenderer, clipboard, nativeImage } from 'electron' declare global { interface Window { bridge: any clipboard: Electron.Clipboard + nativeImage: any } } @@ -32,3 +33,4 @@ if (process.env.NODE_ENV === 'development') { window.clipboard = clipboard window.bridge = window.bridge || bridge +window.nativeImage = nativeImage diff --git a/packages/neuron-wallet/src/startup/sync-block-task/create.ts b/packages/neuron-wallet/src/startup/sync-block-task/create.ts index 89c4b6960b..d9724cf2a1 100644 --- a/packages/neuron-wallet/src/startup/sync-block-task/create.ts +++ b/packages/neuron-wallet/src/startup/sync-block-task/create.ts @@ -4,16 +4,13 @@ import path from 'path' import { networkSwitchSubject, NetworkWithID } from '../../services/networks' import env from '../../env' import genesisBlockHash from './genesis' -import CellsService from '../../services/cells' -import LockUtils from '../../models/lock-utils' import AddressService from '../../services/addresses' import initDatabase from './init-database' export { genesisBlockHash } const updateAllAddressesTxCount = async () => { - const blake160s: string[] = await CellsService.allBlake160s() - const addresses = blake160s.map(blake160 => LockUtils.blake160ToAddress(blake160)) + const addresses = (await AddressService.allAddresses()).map(addr => addr.address) await AddressService.updateTxCountAndBalances(addresses) } diff --git a/packages/neuron-wallet/src/utils/broadcast.ts b/packages/neuron-wallet/src/utils/broadcast.ts deleted file mode 100644 index 673528293e..0000000000 --- a/packages/neuron-wallet/src/utils/broadcast.ts +++ /dev/null @@ -1,73 +0,0 @@ -import windowManager from '../models/window-manager' -import AddressService from '../services/addresses' -import TransactionsService from '../services/transactions' -import { Channel, ResponseCode } from './const' - -export const broadcastWalletList = (walletList: Controller.Wallet[]) => { - windowManager.broadcast(Channel.Wallets, 'getAll', { - status: ResponseCode.Success, - result: walletList, - }) -} - -export const broadcastCurrentWallet = (wallet: Controller.Wallet | null) => { - const currentWallet = wallet ? { id: wallet.id, name: wallet.name } : null - windowManager.broadcast(Channel.Wallets, 'getCurrent', { - status: ResponseCode.Success, - result: currentWallet, - }) -} - -export const broadcastAddressList = async (currentID: string) => { - const addresses = await AddressService.allAddressesByWalletId(currentID).then(addrs => - addrs.map(({ address, blake160: identifier, addressType: type, txCount, balance, description = '' }) => ({ - address, - identifier, - type, - txCount, - description, - balance, - })) - ) - windowManager.broadcast(Channel.Wallets, 'allAddresses', { - status: ResponseCode.Success, - result: addresses, - }) -} - -export const broadcastTransactions = async (currentID: string) => { - const addresses = await AddressService.usedAddresses(currentID) - const params = { - pageNo: 1, - pageSize: 100, - addresses: addresses.map(addr => addr.address), - } - const transactions = await TransactionsService.getAllByAddresses(params) - windowManager.broadcast(Channel.Transactions, 'getAllByAddresses', { - status: ResponseCode.Success, - result: { ...params, keywords: '', ...transactions }, - }) -} - -export const broadcastNetworkList = async (networkList: Controller.Network[]) => { - windowManager.broadcast(Channel.Networks, 'getAll', { - status: ResponseCode.Success, - result: networkList, - }) -} - -export const broadcastCurrentNetworkID = async (id: Controller.NetworkID) => { - windowManager.broadcast(Channel.Networks, 'currentID', { - status: ResponseCode.Success, - result: id, - }) -} - -export default { - broadcastCurrentWallet, - broadcastWalletList, - broadcastAddressList, - broadcastTransactions, - broadcastNetworkList, - broadcastCurrentNetworkID, -} diff --git a/packages/neuron-wallet/src/utils/const.ts b/packages/neuron-wallet/src/utils/const.ts index 5a8b30b1b2..949c7172fc 100644 --- a/packages/neuron-wallet/src/utils/const.ts +++ b/packages/neuron-wallet/src/utils/const.ts @@ -9,6 +9,7 @@ export enum Channel { Wallets = 'wallets', Transactions = 'transactions', Helpers = 'helpers', + DataUpdate = 'dataUpdate', } export enum ResponseCode { diff --git a/packages/neuron-wallet/tests/services/networks.test.ts b/packages/neuron-wallet/tests/services/networks.test.ts index fdac1ce302..bee217f537 100644 --- a/packages/neuron-wallet/tests/services/networks.test.ts +++ b/packages/neuron-wallet/tests/services/networks.test.ts @@ -1,5 +1,4 @@ import NetworksService, { NetworkWithID } from '../../src/services/networks' - import env from '../../src/env' import i18n from '../../src/utils/i18n' @@ -12,26 +11,38 @@ const ERROR_MESSAGE = { const { presetNetworks: { current, list }, } = env - -const newNetwork: NetworkWithID = { - name: `new network`, - remote: `http://new-network.localhost.com`, - type: 0, - id: '', -} - -const newNetworkWithDefaultTypeOf1 = { - name: `new network with the default type of 1`, - remote: `http://test.localhost.com`, - id: '', -} - -describe(`networks service`, () => { - const service = new NetworksService() - afterAll(() => { +const [testnetNetwork, localNetwork] = list + +describe(`Unit tests of networks service`, () => { + const newNetwork: NetworkWithID = { + name: `new network`, + remote: `http://new-network.localhost.com`, + type: 0, + id: '', + } + + const newNetworkWithDefaultTypeOf1 = { + name: `new network with the default type of 1`, + remote: `http://test.localhost.com`, + id: '', + } + + let service: NetworksService = new NetworksService() + + beforeEach(done => { + service = new NetworksService() + setTimeout(() => { + done() + }, 1000) + }) + afterEach(done => { service.clear() + setTimeout(() => { + done() + }, 1000) }) - describe(`operations on networks succeed`, () => { + + describe(`success cases`, () => { it(`get all networks`, async () => { const networks = await service.getAll() expect(Array.isArray(networks)).toBe(true) @@ -42,14 +53,20 @@ describe(`networks service`, () => { expect(networks).toEqual(list) }) - it(`has a default current network`, async () => { - const currentNetworkID = await service.getCurrentID() - expect(currentNetworkID).toBe(current) + it(`get the default network`, async () => { + const network = await service.defaultOne() + expect(network && network.type).toBe(0) }) - it(`has testnet as the default current network`, async () => { + it(`testnet should be type of default network`, async () => { const defaultNetwork = await service.defaultOne() - expect(defaultNetwork).toEqual(list[0]) + expect(defaultNetwork).toEqual(testnetNetwork) + }) + + it(`testnet should be the current one by default`, async () => { + const currentNetworkID = await service.getCurrentID() + expect(currentNetworkID).toBe(current) + expect(currentNetworkID).toBe(testnetNetwork.id) }) it(`get network by id ${current}`, async () => { @@ -57,94 +74,90 @@ describe(`networks service`, () => { expect(currentNetwork).toEqual(list.find(network => network.id === current)) }) - it(`get network by id which not exists`, async () => { + it(`getting a non-exsiting network should return null`, async () => { const id = `not-existing-id` const network = await service.get(id) expect(network).toBeNull() }) - it(`create new network with ${JSON.stringify(newNetwork)}`, async () => { + it(`create a new network with ${JSON.stringify(newNetwork)}`, async () => { const res = await service.create(newNetwork.name, newNetwork.remote, newNetwork.type) - newNetwork.id = res.id - expect(res).toMatchObject(newNetwork) + expect(res).toMatchObject({ ...newNetwork, id: res.id }) const created = await service.get(res.id) expect(created).toEqual(res) }) - it(`create new network with default type of 1`, async () => { + it(`create a new network with default type of 1`, async () => { const res = await service.create(newNetworkWithDefaultTypeOf1.name, newNetworkWithDefaultTypeOf1.remote) - newNetworkWithDefaultTypeOf1.id = res.id expect(res.type).toBe(1) }) - it(`update new network's name`, async () => { - const name = `updated network name` - await service.update(newNetwork.id, { name }) - const network = await service.get(newNetwork.id) + it(`update the local networks's name`, async () => { + const name = `new local network name` + await service.update(localNetwork.id, { name }) + const network = await service.get(localNetwork.id) expect(network && network.name).toBe(name) }) - it(`update network address`, async () => { + it(`update the local network address`, async () => { const addr = `http://updated-address.com` - await service.update(newNetwork.id, { remote: addr }) - const network = await service.get(newNetwork.id) + await service.update(localNetwork.id, { remote: addr }) + const network = await service.get(localNetwork.id) expect(network && network.remote).toBe(addr) }) - it(`update network type`, async () => { + it(`update the local network type to 1`, async () => { const type = 1 - await service.update(newNetwork.id, { type }) - const network = await service.get(newNetwork.id) + await service.update(localNetwork.id, { type }) + const network = await service.get(localNetwork.id) expect(network && network.type).toBe(type) }) - it(`activate the second network`, async () => { - const { id } = list[1] - await service.activate(id) + it(`set the local network to be the current one`, async () => { + await service.activate(localNetwork.id) const currentNetworkID = await service.getCurrentID() - expect(currentNetworkID).toBe(id) + expect(currentNetworkID).toBe(localNetwork.id) }) - it(`delete inactive network`, async () => { - const prevCurrentID = await service.getCurrentID() + it(`delete an inactive network`, async () => { + const inactiveNetwork = localNetwork + const prevCurrentID = (await service.getCurrentID()) || '' const prevNetworks = await service.getAll() - await service.delete(newNetwork.id) + await service.delete(inactiveNetwork.id) const currentID = await service.getCurrentID() const currentNetworks = await service.getAll() - expect(currentNetworks).toEqual(prevNetworks.filter(n => n.id !== newNetwork.id)) + expect(currentNetworks).toEqual(prevNetworks.filter(n => n.id !== inactiveNetwork.id)) expect(currentID).toBe(prevCurrentID) }) - it(`delete current network and switch to the default one`, async () => { - const { id } = list[1] - const defaultNetwork = list[0] - await service.activate(id) + it(`activate the local network and delete it, the current networks should switch to the testnet network`, async () => { + await service.activate(localNetwork.id) const prevCurrentID = await service.getCurrentID() const prevNetworks = await service.getAll() - await service.delete(id) + expect(prevCurrentID).toBe(localNetwork.id) + expect(prevNetworks).toEqual(list) + await service.delete(prevCurrentID || '') const currentNetworks = await service.getAll() - expect(currentNetworks).toEqual(prevNetworks.filter(n => n.id !== id)) - expect(prevCurrentID).not.toBe(defaultNetwork.id) + expect(currentNetworks).toEqual(prevNetworks.filter(n => n.id !== prevCurrentID)) const currentID = await new Promise(resolve => { setTimeout(() => { service.getCurrentID().then(cID => resolve(cID)) }, 500) }) - expect(currentID).toBe(defaultNetwork.id) + expect(currentID).toBe(testnetNetwork.id) }) - it(`get the default network`, async () => { - const network = await service.defaultOne() - expect(network && network.type).toBe(0) - }) - - it(`reset netowrks`, async () => { + it(`reset the netowrks`, async () => { + await service.create(newNetwork.name, newNetwork.remote) + const newNetworkList = await service.getAll() + expect(newNetworkList.length).toBe(list.length + 1) service.clear() const networks = await service.getAll() expect(networks.length).toBe(list.length) }) }) - describe(`operations on networks throw errors`, () => { + + describe(`validation on parameters`, () => { describe(`validation on parameters`, () => { it(`service.get requires id`, () => { expect(service.get(undefined as any)).rejects.toThrowError(i18n.t(ERROR_MESSAGE.MISSING_ARG)) diff --git a/yarn.lock b/yarn.lock index fb1e6e5d16..f9987d1158 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5174,6 +5174,14 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000955, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000974.tgz#b7afe14ee004e97ce6dc73e3f878290a12928ad8" integrity sha512-xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww== +canvg@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-2.0.0.tgz#9ff77bf8837106e358a12fb3d2bf11aeb60e5b46" + integrity sha512-PiKa+sjzzAv8HONsBaJZRhZ3eCM5uJkpFgF0rSzcamOrdXdls81ukjNxtz7JYyxucj6WpIkZwk9j7Jku0+ivqQ== + dependencies: + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -14756,6 +14764,11 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0= + right-pad@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" @@ -15552,6 +15565,11 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stackblur-canvas@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.2.0.tgz#cacc5924a0744b3e183eb2e6c1d8559c1a17c26e" + integrity sha512-5Gf8dtlf8k6NbLzuly2NkGrkS/Ahh+I5VUjO7TnFizdJtgpfpLLEdQlLe9umbcnZlitU84kfYjXE67xlSXfhfQ== + staged-git-files@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b"