diff --git a/package.json b/package.json index 7ae8307..c549224 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyano-wallet", - "version": "0.7.10", + "version": "0.7.11", "private": true, "scripts": { "lint": "tslint -p .", diff --git a/public/manifest.json b/public/manifest.json index a734fda..59a12a7 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -4,7 +4,7 @@ "name": "Cyano wallet", "author": "Matus Zamborsky ", "description": "Cyano wallet - an Ontology wallet", - "version": "0.7.10", + "version": "0.7.11", "browser_action": { "default_title": "Open the wallet" diff --git a/src/api/identityApi.ts b/src/api/identityApi.ts index a3075b2..9ed2982 100644 --- a/src/api/identityApi.ts +++ b/src/api/identityApi.ts @@ -15,6 +15,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with The Ontology Wallet&ID. If not, see . */ +import { get } from 'lodash'; import { Crypto, Identity, utils, Wallet } from 'ontology-ts-sdk'; import { v4 as uuid } from 'uuid'; import { deserializePrivateKey } from './accountApi'; @@ -34,6 +35,51 @@ export function decryptIdentity(identity: Identity, password: string, scrypt: an }); } +export function decryptDefaultIdentity(wallet: Wallet, password: string, scrypt: any) { + const ontId = wallet.defaultOntid !== '' ? wallet.defaultOntid : null; + + if (ontId === null) { + throw new Error('Default identity not found in wallet'); + } + + const identity = wallet.identities.find((i) => i.ontid === ontId); + + if (identity === undefined) { + throw new Error(`Identity ${ontId} not found in the wallet.`); + } + + return decryptIdentity(identity, password, scrypt); +} + +export function hasIdentity(wallet: Wallet) { + return wallet.identities.length > 0; +} + +export function getDefaultIdentity(wallet: Wallet) { + const ontId = wallet.defaultOntid !== '' ? wallet.defaultOntid : null; + + if (ontId === null) { + if (wallet.identities.length > 0) { + return wallet.identities[0]; + } else { + throw new Error('No identities found.'); + } + } + + const identity = wallet.identities.find((i) => i.ontid === ontId); + + if (identity === undefined) { + throw new Error(`Identity ${ontId} not found in the wallet.`); + } + + return identity; +} + +export function isIdentityLedgerKey(wallet: Wallet) { + return get(getDefaultIdentity(wallet).controls[0].encryptedKey, 'type') === 'LEDGER'; +} + + export function identitySignUp(password: string, scrypt: any, neo: boolean) { const mnemonics = utils.generateMnemonic(32); return identityImportMnemonics(mnemonics, password, scrypt, neo); diff --git a/src/background/api/smartContractApi.ts b/src/background/api/smartContractApi.ts index eae67ff..d3030d1 100644 --- a/src/background/api/smartContractApi.ts +++ b/src/background/api/smartContractApi.ts @@ -16,7 +16,7 @@ * along with The Ontology Wallet&ID. If not, see . */ import { Parameter } from 'ontology-dapi'; -import { Crypto, TransactionBuilder, utils } from 'ontology-ts-sdk'; +import { Crypto, Transaction, TransactionBuilder, utils } from 'ontology-ts-sdk'; import { buildInvokePayload } from 'ontology-ts-test'; import { decryptAccount, getAccount } from '../../api/accountApi'; import { getWallet } from '../../api/authApi'; @@ -25,37 +25,77 @@ import { getClient } from '../network'; import { getStore } from '../redux'; import Address = Crypto.Address; - -export async function scCall(request: ScCallRequest, password: string) { +import {decryptDefaultIdentity } from 'src/api/identityApi'; + +/** + * Creates, signs and sends the transaction for Smart Contract call. + * + * Can work in two modes: + * 1. simple account sign (requireIdentity = false) + * - the transaction is created, signed and sent in one step + * 2. account + identity sign (requireIdentity = true) + * - the transaction is created, signed by account and stored in first step (presignedTransaction = undefined) + * - signed by identity and sent in second step (presignedTransaction = serialized transaction) + * + * @param request request describing the SC call + * @param password password to decode the private key (account or identity) + */ +export async function scCall(request: ScCallRequest, password: string): Promise { request.parameters = request.parameters !== undefined ? request.parameters : []; request.gasPrice = request.gasPrice !== undefined ? request.gasPrice : 500; request.gasLimit = request.gasLimit !== undefined ? request.gasLimit : 30000; const state = getStore().getState(); const wallet = getWallet(state.wallet.wallet!); - const account = getAccount(state.wallet.wallet!).address; - const privateKey = decryptAccount(wallet, password); - - // convert params - const params = convertParams(request.parameters); - const payload = buildInvokePayload(request.contract, request.method, params); + + let tx: Transaction; + if (request.presignedTransaction) { + tx = Transaction.deserialize(request.presignedTransaction); + } else { + // convert params + const params = convertParams(request.parameters); + const payload = buildInvokePayload(request.contract, request.method, params); + + tx = TransactionBuilder.makeInvokeTransaction( + request.method, + [], + new Address(utils.reverseHex(request.contract)), + String(request.gasPrice), + String(request.gasLimit), + account, + ); + + (tx.payload as any).code = payload.toString('hex'); + } - const tx = TransactionBuilder.makeInvokeTransaction( - request.method, - [], - new Address(utils.reverseHex(request.contract)), - String(request.gasPrice), - String(request.gasLimit), - account, - ); + let privateKey: Crypto.PrivateKey; + if (request.requireIdentity && request.presignedTransaction) { + // mode 2, step 2: + // already signed by account + // do signature by identity + privateKey = decryptDefaultIdentity(wallet, password, wallet.scrypt); - (tx.payload as any).code = payload.toString('hex'); + // fixme: add support for async sign + TransactionBuilder.addSign(tx, privateKey); + } else { + // mode 1 or mode 2, step 1: + // do signature by account + privateKey = decryptAccount(wallet, password); + await TransactionBuilder.signTransactionAsync(tx, privateKey); + } - await TransactionBuilder.signTransactionAsync(tx, privateKey); + if (request.requireIdentity && !request.presignedTransaction) { + // mode 2, step 1 + // return the presigned transaction to be stored in request + // outside of this method + return tx.serialize(); - const client = getClient(); - return await client.sendRawTransaction(tx.serialize(), false, true); + } else { + // mode 1 or mode 2, step 2 + const client = getClient(); + return await client.sendRawTransaction(tx.serialize(), false, true); + } } export async function scCallRead(request: ScCallReadRequest) { diff --git a/src/background/popUpManager.ts b/src/background/popUpManager.ts index ba9e0b5..1deeadc 100644 --- a/src/background/popUpManager.ts +++ b/src/background/popUpManager.ts @@ -17,6 +17,7 @@ */ import { MethodType, Rpc } from 'ontology-dapi'; import { Identity } from 'ontology-ts-sdk'; +import { decryptDefaultIdentity } from 'src/api/identityApi'; import { browser } from 'webextension-polyfill-ts'; import { decryptAccount } from '../api/accountApi'; import { getWallet } from '../api/authApi'; @@ -53,6 +54,7 @@ export class PopupManager { this.rpc.register('popup_initialized', this.pupupInitialized.bind(this)); this.rpc.register('check_account_password', this.checkAccountPassword.bind(this)); + this.rpc.register('check_identity_password', this.checkIdentityPassword.bind(this)); this.rpc.register('check_ont_id', this.checkOntId.bind(this)); this.rpc.register('get_oep4_token', this.getOEP4Token.bind(this)); this.rpc.register('is_ledger_supported', this.isLedgerSupported.bind(this)); @@ -124,6 +126,22 @@ export class PopupManager { } } + private checkIdentityPassword(password: string) { + const encodedWallet = this.store.getState().wallet.wallet; + if (encodedWallet === null) { + throw new Error('NO_IDENTITY'); + } + + try { + const wallet = getWallet(encodedWallet); + decryptDefaultIdentity(wallet, password, wallet.scrypt); + + return true; + } catch (e) { + return false; + } + } + private checkOntId(identityEncoded: string, password: string) { const identity = Identity.parseJson(identityEncoded); return checkOntId(identity, password); diff --git a/src/background/redux/transactionRequestsReducer.ts b/src/background/redux/transactionRequestsReducer.ts index d4ab158..1ce7096 100644 --- a/src/background/redux/transactionRequestsReducer.ts +++ b/src/background/redux/transactionRequestsReducer.ts @@ -108,6 +108,9 @@ export const transactionRequestsAliases = { break; case 'sc_call': result = await submitScCall(request as ScCallRequest, password!, dispatch, state); + if (result === undefined) { + return; + } break; case 'sc_call_read': result = await submitScCallRead(request as ScCallReadRequest); @@ -186,6 +189,10 @@ async function submitRegisterOntId( } function isTrustedSc(request: ScCallRequest, state: GlobalState) { + if (request.requireIdentity) { + return false; + } + const trustedScs = state.settings.trustedScs; const trustedSc = trustedScs.find( @@ -206,22 +213,30 @@ function isTrustedSc(request: ScCallRequest, state: GlobalState) { async function submitScCall(request: ScCallRequest, password: string, dispatch: Dispatch, state: GlobalState) { if (isTrustedSc(request, state)) { + // fixme: add support for account+identity password await dispatch(Actions.password.setPassword(password)); } const response = await timeout(scCall(request, password), 15000); - if (response.Result.State === 0) { - throw new Error('OTHER'); + if (typeof response === 'string') { + dispatch(Actions.transactionRequests.updateRequest(request.id, { presignedTransaction: response })); + return undefined; + } else { + if (response.Result.State === 0) { + throw new Error('OTHER'); + } + + const notify = response.Result.Notify.filter((element: any) => element.ContractAddress === request.contract).map( + (element: any) => element.States, + ); + return { + result: notify, + transaction: response.Result.TxHash, + }; } - const notify = response.Result.Notify.filter((element: any) => element.ContractAddress === request.contract).map( - (element: any) => element.States, - ); - return { - result: notify, - transaction: response.Result.TxHash, - }; + } async function submitMessageSign(request: MessageSignRequest, password: string) { diff --git a/src/background/requestsManager.ts b/src/background/requestsManager.ts index a2cfe50..21c52bc 100644 --- a/src/background/requestsManager.ts +++ b/src/background/requestsManager.ts @@ -1,4 +1,6 @@ import { Parameter } from 'ontology-dapi'; +import { getWallet } from 'src/api/authApi'; +import { hasIdentity } from 'src/api/identityApi'; import { v4 as uuid } from 'uuid'; import { Deferred } from '../deffered'; import Actions from '../redux/actions'; @@ -109,6 +111,13 @@ export class RequestsManager { gasLimit?: number; requireIdentity?: boolean; }) { + const state = this.store.getState(); + const wallet = getWallet(state.wallet.wallet!); + + if (args.requireIdentity && !hasIdentity(wallet)) { + return Promise.reject('NO_IDENTITY'); + } + const requestId = uuid(); // stores deferred object to resolve when the transaction is resolved @@ -123,12 +132,12 @@ export class RequestsManager { }), ); - const state = this.store.getState(); const password = state.password.password; const trustedScs = state.settings.trustedScs; - if (password !== undefined) { + if (password !== undefined && args.requireIdentity !== true) { // check if we already have password stored + // whitelisting is not supported for account+identity sign const trustedSc = trustedScs.find( (t) => diff --git a/src/popup/backgroundManager.ts b/src/popup/backgroundManager.ts index 0c97cf3..920e996 100644 --- a/src/popup/backgroundManager.ts +++ b/src/popup/backgroundManager.ts @@ -42,6 +42,10 @@ class BackgroundManager { return this.rpc.call('check_account_password', password); } + public checkIdentityPassword(password: string) { + return this.rpc.call('check_identity_password', password); + } + public checkOntId(encodedIdentity: string, password: string) { return this.rpc.call('check_ont_id', encodedIdentity, password); } diff --git a/src/popup/pages/call/call.tsx b/src/popup/pages/call/call.tsx index 060a63f..95713a2 100644 --- a/src/popup/pages/call/call.tsx +++ b/src/popup/pages/call/call.tsx @@ -49,6 +49,7 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone reduxConnect(mapStateToProps, mapDispatchToProps, (reduxProps, actions, getReduxProps) => withProps( { + allowWhitelist: !get(reduxProps.requests.find((r) => r.id === get(props.location, 'state.requestId'))!, 'requireIdentity', false), handleCancel: async () => { props.history.goBack(); @@ -78,8 +79,11 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone method, } as Partial); - if (reduxProps.password !== undefined) { + const requireIdentity = get(reduxProps.requests.find((r) => r.id === get(props.location, 'state.requestId'))!, 'requireIdentity', false) + + if (reduxProps.password !== undefined && requireIdentity !== true) { // check if we already have password stored + // whitelisting is not supported for account+identity sign const trustedSc = reduxProps.trustedScs.find( (t) => diff --git a/src/popup/pages/call/callView.tsx b/src/popup/pages/call/callView.tsx index d278110..f15c295 100644 --- a/src/popup/pages/call/callView.tsx +++ b/src/popup/pages/call/callView.tsx @@ -30,6 +30,7 @@ export interface InitialValues { } export interface Props { + allowWhitelist: boolean; initialValues: InitialValues; loading: boolean; locked: boolean; @@ -138,26 +139,30 @@ export const CallView: React.SFC = (props) => ( /> - -
- - } - content="Be responsible when using this functionality" - /> -
- - ( - t.input.onChange(d.checked)} - checked={Boolean(t.input.value)} - error={t.meta.touched && t.meta.invalid} + {props.allowWhitelist ? ( + +
+ + } + content="Be responsible when using this functionality" /> - )} - /> - +
+ ( + t.input.onChange(d.checked)} + checked={Boolean(t.input.value)} + error={t.meta.touched && t.meta.invalid} + /> + )} + /> +
+ ) : ( + <> + ) + }
diff --git a/src/popup/pages/confirm/confirm.tsx b/src/popup/pages/confirm/confirm.tsx index ab35e43..89c833f 100644 --- a/src/popup/pages/confirm/confirm.tsx +++ b/src/popup/pages/confirm/confirm.tsx @@ -17,6 +17,7 @@ */ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; +import { isIdentityLedgerKey } from 'src/api/identityApi'; import { isLedgerKey } from '../../../api/accountApi'; import { getWallet } from '../../../api/authApi'; import { isTrezorKey } from '../../../api/trezorApi'; @@ -35,13 +36,23 @@ const enhancer = (Component: React.ComponentType<{}>) => (props: RouteComponentP componentDidMount: async () => { const wallet = getWallet(reduxProps.wallet!); - if (isLedgerKey(wallet)) { - props.history.replace('/ledger/confirm', props.location.state); - } else if (isTrezorKey(wallet)) { - props.history.replace('/trezor/confirm', props.location.state); + const identityConfirm = props.location.state.identityConfirm; + + if (identityConfirm) { + if (isIdentityLedgerKey(wallet)) { + props.history.replace('/ledger/confirm', props.location.state); + } else { + props.history.replace('/confirm-normal', props.location.state); + } } else { - props.history.replace('/confirm-normal', props.location.state); - } + if (isLedgerKey(wallet)) { + props.history.replace('/ledger/confirm', props.location.state); + } else if (isTrezorKey(wallet)) { + props.history.replace('/trezor/confirm', props.location.state); + } else { + props.history.replace('/confirm-normal', props.location.state); + } + } } }, () => ( diff --git a/src/popup/pages/confirm/confirmNormal.tsx b/src/popup/pages/confirm/confirmNormal.tsx index 018236d..ec9cd1a 100644 --- a/src/popup/pages/confirm/confirmNormal.tsx +++ b/src/popup/pages/confirm/confirmNormal.tsx @@ -20,6 +20,7 @@ import { get } from 'lodash'; import * as React from 'react'; import { RouteComponentProps } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; +import { isScCallRequest } from 'src/redux/transactionRequests'; import { getBackgroundManager } from '../../backgroundManager'; import { reduxConnect, withProps } from '../../compose'; import { Actions, GlobalState } from '../../redux'; @@ -51,15 +52,30 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone const requestId: string = get(props.location, 'state.requestId'); const redirectSucess: string = get(props.location, 'state.redirectSucess'); const redirectFail: string = get(props.location, 'state.redirectFail'); + const identityConfirm: boolean = get(props.location, 'state.identityConfirm', false); const password: string = get(values, 'password', ''); - const passwordCorrect = await getBackgroundManager().checkAccountPassword(password); - if (!passwordCorrect) { - formApi.change('password', ''); + // test if the password is correct + if (!identityConfirm) { + // in case of Identity sign, check password for default identity + const passwordCorrect = await getBackgroundManager().checkAccountPassword(password); + if (!passwordCorrect) { + formApi.change('password', ''); - return { - password: '', - }; + return { + password: '', + }; + } + } else { + // in case of Account sign, check password for default account + const passwordCorrect = await getBackgroundManager().checkIdentityPassword(password); + if (!passwordCorrect) { + formApi.change('password', ''); + + return { + password: '', + }; + } } await actions.startLoading(); @@ -76,13 +92,22 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone if (request.error !== undefined) { props.history.push(redirectFail, { ...props.location.state, request }); } else { - props.history.push(redirectSucess, { ...props.location.state, request }); + + if (isScCallRequest(request) && request.requireIdentity && !identityConfirm) { + // if this is SC CALL request + // and it requires identity Confirm + // and this is account confirm + // go to identity confirm instead of success + props.history.push('/confirm', { ...props.location.state, identityConfirm: true }); + } else { + props.history.push(redirectSucess, { ...props.location.state, request }); + } } return {}; }, }, - (injectedProps) => , + (injectedProps) => , ), ); diff --git a/src/popup/pages/confirm/confirmView.tsx b/src/popup/pages/confirm/confirmView.tsx index 2239713..c589bd9 100644 --- a/src/popup/pages/confirm/confirmView.tsx +++ b/src/popup/pages/confirm/confirmView.tsx @@ -23,6 +23,7 @@ import { AccountLogoHeader, Filler, StatusBar, View } from '../../components'; import { required } from '../../utils/validate'; export interface Props { + identityConfirm: boolean; handleSubmit: (values: object, formApi: FormApi) => Promise; handleCancel: () => void; loading: boolean; @@ -33,7 +34,11 @@ export const ConfirmView: React.SFC = (props) => ( - Confirm the transaction by unlocking the wallet with your password. + {props.identityConfirm ? ( + Enter password to your identity. + ) : ( + Enter password to your account. + )} diff --git a/src/popup/pages/ledger/confirm/ledgerConfirm.tsx b/src/popup/pages/ledger/confirm/ledgerConfirm.tsx index 235edd6..cddf7ec 100644 --- a/src/popup/pages/ledger/confirm/ledgerConfirm.tsx +++ b/src/popup/pages/ledger/confirm/ledgerConfirm.tsx @@ -19,6 +19,7 @@ import { get } from 'lodash'; import * as React from 'react'; import { RouteComponentProps } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; +import { isScCallRequest } from 'src/redux/transactionRequests'; import { reduxConnect, withProps } from '../../../compose'; import { Actions, GlobalState } from '../../../redux'; import { LedgerConfirmView, Props } from './ledgerConfirmView'; @@ -44,6 +45,7 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone const requestId: string = get(props.location, 'state.requestId'); const redirectSucess: string = get(props.location, 'state.redirectSucess'); const redirectFail: string = get(props.location, 'state.redirectFail'); + const identityConfirm: boolean = get(props.location, 'state.identityConfirm', false); await actions.startLoading(); @@ -60,11 +62,19 @@ const enhancer = (Component: React.ComponentType) => (props: RouteCompone if (request.error !== undefined) { props.history.push(redirectFail, { requestId }); } else { - props.history.push(redirectSucess, { requestId }); + if (isScCallRequest(request) && request.requireIdentity && !identityConfirm) { + // if this is SC CALL request + // and it requires identity Confirm + // and this is account confirm + // go to identity confirm instead of success + props.history.push('/confirm', { ...props.location.state, identityConfirm: true }); + } else { + props.history.push(redirectSucess, { ...props.location.state, request }); + } } } }, (injectedProps) => ( - + )) )) ) diff --git a/src/popup/pages/ledger/confirm/ledgerConfirmView.tsx b/src/popup/pages/ledger/confirm/ledgerConfirmView.tsx index 0c3e32d..0dafcdb 100644 --- a/src/popup/pages/ledger/confirm/ledgerConfirmView.tsx +++ b/src/popup/pages/ledger/confirm/ledgerConfirmView.tsx @@ -20,6 +20,7 @@ import { Button } from 'semantic-ui-react'; import { AccountLogoHeader, Filler, StatusBar, View } from '../../../components'; export interface Props { + identityConfirm: boolean; handleSubmit: () => Promise; handleCancel: () => void; loading: boolean; @@ -30,7 +31,11 @@ export const LedgerConfirmView: React.SFC = (props) => ( - Confirm the transaction on your Ledger. + {props.identityConfirm ? ( + Confirm identity on Ledger. + ) : ( + Confirm account on Ledger. + )} diff --git a/src/redux/transactionRequests.ts b/src/redux/transactionRequests.ts index 8692b4f..ca9a2df 100644 --- a/src/redux/transactionRequests.ts +++ b/src/redux/transactionRequests.ts @@ -73,6 +73,7 @@ export interface ScCallRequest extends TransactionRequest { requireIdentity?: boolean; parameters?: Parameter[]; paramsHash?: string; + presignedTransaction?: string; } export interface ScDeployRequest extends TransactionRequest { @@ -126,3 +127,7 @@ export const submitRequest = (id: string, password?: string) => ({ password, type: SUBMIT_REQUEST, }); + +export function isScCallRequest(request: TransactionRequest): request is ScCallRequest { + return request.type === 'sc_call'; +} \ No newline at end of file