diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..d24fdfc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/package.json b/package.json index 3e6ca10..6cf1103 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "tsc && vite build", "type-check": "tsc", "lint": "npm run prettify && eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "prepare": "husky install", + "prepare": "husky", "preprocess": "npx madge --circular --extensions ts --extensions tsx ./", "prettify": "prettier --ignore-unknown --write .", "preview": "vite preview", diff --git a/src/action/multi-sig/signing.ts b/src/action/multi-sig/signing.ts index 413a21c..54b0573 100644 --- a/src/action/multi-sig/signing.ts +++ b/src/action/multi-sig/signing.ts @@ -176,7 +176,7 @@ const extractAndAddSignedHints = async ( ) => { const simulatedPropositions = arrayToProposition(simulated); const realPropositions = arrayToProposition(signed); - const context = await getChain(wallet.networkType).fakeContext(); + const context = getChain(wallet.networkType).fakeContext(); if (partial) { const ergoBoxes = wasm.ErgoBoxes.empty(); boxes.forEach((box) => ergoBoxes.add(box)); @@ -266,7 +266,7 @@ export const sign = async ( // generate signed const signedAddresses = signed - .filter((item) => item.completed == true) + .filter((item) => item.completed) .map((item) => item.address); const signedPKs = addresses .filter((item) => signedAddresses.includes(item.address)) @@ -336,3 +336,9 @@ export const arrayToProposition = (input: Array): wasm.Propositions => { }); return output; }; + +export const addressesToPk = (input: Array): Array => { + return input.map((item) => + Buffer.from(wasm.Address.from_base58(item).content_bytes()).toString('hex'), + ); +}; diff --git a/src/action/multi-sig/verify.ts b/src/action/multi-sig/verify.ts new file mode 100644 index 0000000..52361b8 --- /dev/null +++ b/src/action/multi-sig/verify.ts @@ -0,0 +1,324 @@ +import { getInputPks, getMyInputPks } from '@/action/multi-sig/wallet-keys'; +import * as wasm from 'ergo-lib-wasm-browser'; +import { addressesToPk, arrayToProposition } from './signing'; +import getChain from '@/utils/networks'; +import { MultiSigDataRow, MultiSigShareData } from '@/types/multi-sig'; +import { deserialize } from '../box'; +import { + fetchMultiSigRows, + notAvailableAddresses, + storeMultiSigRow, + updateMultiSigRow, +} from './store'; +import { StateWallet } from '@/store/reducer/wallet'; +import { dottedText } from '@/utils/functions'; +import { boxArrayToBoxes, boxesToArrayBox } from '@/utils/convert'; +import { hintBagToArray } from './commitment'; + +interface VerificationResponse { + valid: boolean; + message: string; + txId?: string; +} + +// verify commitments +// my commitment must not change +const verifyMyCommitments = ( + commitments: Array>, + oldCommitments: Array>, + pks: Array>, + myPks: Array, +): VerificationResponse => { + const filteredMyPks = pks.map((row) => + row.map((item) => (myPks.indexOf(item) === -1 ? '' : item)), + ); + const valid = + commitments.filter((row, rowIndex) => { + return ( + row.filter((item, itemIndex) => { + if ( + oldCommitments[rowIndex][itemIndex] !== item && + filteredMyPks[rowIndex][itemIndex] !== '' + ) { + return true; + } + }).length > 0 + ); + }).length === 0; + return { + valid, + message: valid + ? '' + : 'Your commitment changed.\nThis transaction can not sign anymore.\nPlease try sign it again from beginning', + }; +}; + +// verify commitments +// my wallet must not commit new transaction +const verifyNotCommittedNewTx = ( + commitments: Array>, + pks: Array>, + myPks: Array, +): VerificationResponse => { + const filteredRows = commitments.filter((commitmentRow, index) => { + const myIndexes = myPks + .map((item) => pks[index].indexOf(item)) + .filter((item) => item >= 0); + return myIndexes.filter((index) => commitmentRow[index] !== '').length > 0; + }); + return filteredRows.length > 0 + ? { valid: false, message: 'Already have my commitment' } + : { valid: true, message: '' }; +}; + +const verifyTxAddresses = ( + tx: wasm.ReducedTransaction, + commitments: Array>, + boxes: Array, + wallet: StateWallet, +): VerificationResponse => { + // verify addresses + const invalidAddresses = notAvailableAddresses( + wallet, + commitments, + tx.unsigned_tx(), + boxes, + ); + if (invalidAddresses.length > 0) { + const messageLines = [ + 'Some addresses used in transaction are not derived.', + 'Please derive them and try again', + 'Not derived addresses are:', + ...invalidAddresses.map((item) => dottedText(item, 10)), + ]; + return { valid: false, message: messageLines.join('\n') }; + } + return { valid: true, message: '' }; +}; + +// verify inputs +// verify all inputs of transaction exists in list of boxes +const verifyTxInputs = ( + tx: wasm.ReducedTransaction, + boxes: Array, +): VerificationResponse => { + const inputs = tx.unsigned_tx().inputs(); + for (let index = 0; index < inputs.len(); index++) { + const input = inputs.get(index); + if ( + boxes.filter((item) => item.box_id().to_str() === input.box_id().to_str()) + .length !== 1 + ) { + return { valid: false, message: 'Transaction inputs are invalid' }; + } + } + return { valid: true, message: '' }; +}; + +// verify partial +// verify used commitment is valid for tx +const verifyTxPartial = async ( + wallet: StateWallet, + signer: StateWallet, + signed: Array, + simulated: Array, + partialBase64: string, + networkType: string, + boxes: wasm.ErgoBoxes, + dataBoxes: wasm.ErgoBoxes = wasm.ErgoBoxes.empty(), + commitments: Array>, +): Promise => { + const simulatedPropositions = arrayToProposition(addressesToPk(simulated)); + const realPropositions = arrayToProposition(addressesToPk(signed)); + const context = getChain(networkType).fakeContext(); + const hints = wasm.extract_hints( + wasm.Transaction.sigma_parse_bytes(Buffer.from(partialBase64, 'base64')), + context, + boxes, + dataBoxes, + realPropositions, + simulatedPropositions, + ); + const converted = await hintBagToArray( + wallet, + signer, + wasm.Transaction.sigma_parse_bytes(Buffer.from(partialBase64, 'base64')), + boxesToArrayBox(boxes), + hints, + ); + console.log(converted, commitments); + console.log(hints.to_json()); + return { valid: true, message: '' }; + // compare hints with commitments +}; + +const verifyNotSigningNewTx = ( + sharedData: MultiSigShareData, +): VerificationResponse => { + if ( + (sharedData.signed ?? []).length > 0 || + (sharedData.simulated ?? []).length > 0 || + (sharedData.partial ?? '').length > 0 + ) + return { + valid: false, + message: 'Transaction already signing without your commitment', + }; + return { valid: true, message: '' }; +}; + +const verifyNewTx = async ( + sharedData: MultiSigShareData, + wallet: StateWallet, + signer: StateWallet, +): Promise => { + const tx = wasm.ReducedTransaction.sigma_parse_bytes( + Buffer.from(sharedData.tx, 'base64'), + ); + const boxes = sharedData.boxes.map(deserialize); + const verifyAddress = verifyTxAddresses( + tx, + sharedData.commitments, + boxes, + wallet, + ); + if (!verifyAddress.valid) return verifyAddress; + const notSigning = verifyNotSigningNewTx(sharedData); + if (!notSigning.valid) return notSigning; + const txInputsValid = verifyTxInputs(tx, boxes); + if (!txInputsValid.valid) return txInputsValid; + const unsigned = tx.unsigned_tx(); + const pks = await getInputPks(wallet, signer, unsigned, boxes); + const myPks = await getMyInputPks(wallet, signer, unsigned, boxes); + const notCommittedValid = verifyNotCommittedNewTx( + sharedData.commitments, + pks, + myPks, + ); + if (!notCommittedValid.valid) return notCommittedValid; + return verifyTxAddresses(tx, sharedData.commitments, boxes, wallet); +}; + +const verifyExistingTx = async ( + sharedData: MultiSigShareData, + wallet: StateWallet, + signer: StateWallet, + row?: MultiSigDataRow, + txId?: string, +): Promise => { + const tx = wasm.ReducedTransaction.sigma_parse_bytes( + Buffer.from(sharedData.tx, 'base64'), + ); + if (txId && tx.unsigned_tx().id().to_str() !== txId) { + return { + valid: false, + message: 'This tx does not belong to selected transaction', + }; + } + if (!row) { + return { + valid: false, + message: 'Invalid transaction entered', + }; + } + const boxes = sharedData.boxes.map(deserialize); + const verifyInputs = verifyTxInputs(tx, boxes); + if (!verifyInputs.valid) return verifyInputs; + const verifyAddress = verifyTxAddresses( + tx, + sharedData.commitments, + boxes, + wallet, + ); + if (!verifyAddress.valid) return verifyAddress; + const unsigned = tx.unsigned_tx(); + const pks = await getInputPks(wallet, signer, unsigned, boxes); + const myPks = await getMyInputPks(wallet, signer, unsigned, boxes); + const verifyCommitments = verifyMyCommitments( + sharedData.commitments, + row.commitments, + pks, + myPks, + ); + if (!verifyCommitments.valid) return verifyCommitments; + if (sharedData.partial && sharedData.signed && sharedData.simulated) { + const verifyPartial = await verifyTxPartial( + wallet, + signer, + sharedData.signed, + sharedData.simulated, + sharedData.partial, + wallet.networkType, + boxArrayToBoxes(boxes), + wasm.ErgoBoxes.empty(), + row.commitments, + ); + if (!verifyPartial.valid) return verifyPartial; + } + return { valid: true, message: '' }; +}; + +const verifyAndSaveData = async ( + data: MultiSigShareData, + wallet: StateWallet, + signer: StateWallet, + txId?: string, +): Promise => { + const rows = await fetchMultiSigRows(wallet); + const tx = wasm.ReducedTransaction.sigma_parse_bytes( + Buffer.from(data.tx, 'base64'), + ); + const filteredRow = rows.filter( + (item) => + item.tx.unsigned_tx().id().to_str() == tx.unsigned_tx().id().to_str(), + ); + const verification = await (txId === undefined && filteredRow.length === 0 + ? verifyNewTx(data, wallet, signer) + : verifyExistingTx( + data, + wallet, + signer, + filteredRow.length === 0 ? undefined : filteredRow[0], + txId, + )); + if (!verification.valid) return verification; + if (filteredRow.length > 0) { + const row = filteredRow[0]; + await updateMultiSigRow( + row.rowId, + data.commitments, + row.secrets, + data.signed || [], + data.simulated || [], + Date.now(), + data.partial + ? wasm.Transaction.sigma_parse_bytes( + Buffer.from(data.partial, 'base64'), + ) + : undefined, + ); + } else { + await storeMultiSigRow( + wallet, + tx, + data.boxes.map(deserialize), + data.commitments, + [[]], + data.signed || [], + data.simulated || [], + Date.now(), + data.partial + ? wasm.Transaction.sigma_parse_bytes( + Buffer.from(data.partial, 'base64'), + ) + : undefined, + ); + } + return { + valid: true, + message: 'Updated Successfully', + txId: tx.unsigned_tx().id().to_str(), + }; +}; + +export { verifyNewTx, verifyExistingTx, verifyAndSaveData }; diff --git a/src/action/sync.ts b/src/action/sync.ts index fc0e1ac..395d5b2 100644 --- a/src/action/sync.ts +++ b/src/action/sync.ts @@ -164,10 +164,10 @@ const syncWallet = async (wallet: StateWallet) => { ); try { await Promise.all( - addresses.map(async address => { - await syncInfo(network, address) - }) - ) + addresses.map(async (address) => { + await syncInfo(network, address); + }), + ); } catch (e) { console.log(e); } diff --git a/src/pages/wallet-page/multi-sig/MultiSigCommunication.tsx b/src/pages/wallet-page/multi-sig/MultiSigCommunication.tsx index b12fbd9..52c6008 100644 --- a/src/pages/wallet-page/multi-sig/MultiSigCommunication.tsx +++ b/src/pages/wallet-page/multi-sig/MultiSigCommunication.tsx @@ -1,11 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { - fetchMultiSigBriefRow, - fetchMultiSigRows, - notAvailableAddresses, - storeMultiSigRow, -} from '@/action/multi-sig/store'; +import { fetchMultiSigBriefRow } from '@/action/multi-sig/store'; import ListController from '@/components/list-controller/ListController'; import { MultiSigBriefRow, MultiSigShareData } from '@/types/multi-sig'; import AppFrame from '@/layouts/AppFrame'; @@ -15,16 +10,15 @@ import { StateWallet } from '@/store/reducer/wallet'; import MultiSigTransactionItem from './MultiSigTransactionItem'; import { readClipBoard } from '@/utils/clipboard'; import MessageContext from '@/components/app/messageContext'; -import * as wasm from 'ergo-lib-wasm-browser'; -import { deserialize } from '@/action/box'; import { useNavigate } from 'react-router-dom'; import { RouteMap, getRoute } from '@/router/routerMap'; -import { dottedText } from '@/utils/functions'; import BackButtonRouter from '@/components/back-button/BackButtonRouter'; import { Fab } from '@mui/material'; import FabStack from '@/components/fab-stack/FabStack'; import { QrCodeContext } from '@/components/qr-code-scanner/QrCodeContext'; import { QrCodeTypeEnum } from '@/types/qrcode'; +import { verifyAndSaveData } from '@/action/multi-sig/verify'; +import { useSignerWallet } from '@/hooks/multi-sig/useSignerWallet'; interface MultiSigCommunicationPropsType { wallet: StateWallet; @@ -38,6 +32,7 @@ const MultiSigCommunication = (props: MultiSigCommunicationPropsType) => { const message = useContext(MessageContext); const navigate = useNavigate(); const scanContext = useContext(QrCodeContext); + const signer = useSignerWallet(props.wallet); const lastChanged = useSelector( (state: GlobalStateType) => state.config.multiSigLoadedTime, ); @@ -54,52 +49,21 @@ const MultiSigCommunication = (props: MultiSigCommunicationPropsType) => { }, [loading, loadedTime, lastChanged, props.wallet]); const processNewData = async (content: string) => { - const data = JSON.parse(content) as MultiSigShareData; - const tx = wasm.ReducedTransaction.sigma_parse_bytes( - Buffer.from(data.tx, 'base64'), - ); - const boxes = data.boxes.map(deserialize); - const invalidAddresses = notAvailableAddresses( - props.wallet, - data.commitments, - tx.unsigned_tx(), - boxes, - ); - if (invalidAddresses.length === 0) { - const oldRow = await fetchMultiSigRows(props.wallet, [ - tx.unsigned_tx().id().to_str(), - ]); - const secrets = oldRow.length > 0 ? oldRow[0].secrets : [[]]; - const row = await storeMultiSigRow( - props.wallet, - tx, - boxes, - data.commitments, - secrets, - data.signed || [], - data.simulated || [], - Date.now(), - data.partial - ? wasm.Transaction.sigma_parse_bytes( - Buffer.from(data.partial, 'base64'), - ) - : undefined, - ); - if (row) { - const route = getRoute(RouteMap.WalletMultiSigTxView, { - id: props.wallet.id, - txId: row.txId, - }); - navigate(route); + if (signer) { + const data = JSON.parse(content) as MultiSigShareData; + const response = await verifyAndSaveData(data, props.wallet, signer); + if (!response.valid) { + message.insert(response.message, 'error'); + } else { + if (response.txId) { + const route = getRoute(RouteMap.WalletMultiSigTxView, { + id: props.wallet.id, + txId: response.txId, + }); + navigate(route); + } + message.insert(response.message, 'success'); } - } else { - const messageLines = [ - 'Some addresses used in transaction are not derived.', - 'Please derive them and try again', - 'Not derived addresses are:', - ...invalidAddresses.map((item) => dottedText(item, 10)), - ]; - message.insert(messageLines.join('\n'), 'error'); } }; diff --git a/src/pages/wallet-page/multi-sig/components/MultiSigToolbar.tsx b/src/pages/wallet-page/multi-sig/components/MultiSigToolbar.tsx index 647edff..ee3f490 100644 --- a/src/pages/wallet-page/multi-sig/components/MultiSigToolbar.tsx +++ b/src/pages/wallet-page/multi-sig/components/MultiSigToolbar.tsx @@ -9,11 +9,12 @@ import { MultiSigShareData, MultiSigStateEnum } from '@/types/multi-sig'; import { commit, sign } from '@/action/multi-sig/signing'; import { TxDataContext } from '@/components/sign/context/TxDataContext'; import { readClipBoard } from '@/utils/clipboard'; -import * as wasm from 'ergo-lib-wasm-browser'; -import { updateMultiSigRow } from '@/action/multi-sig/store'; import { QrCodeContext } from '@/components/qr-code-scanner/QrCodeContext'; import TxSubmitContext from '@/components/sign/context/TxSubmitContext'; import { QrCodeTypeEnum } from '@/types/qrcode'; +import { verifyAndSaveData } from '@/action/multi-sig/verify'; +import MessageContext from '@/components/app/messageContext'; +import { useSignerWallet } from '@/hooks/multi-sig/useSignerWallet'; const MultiSigToolbar = () => { const context = useContext(MultiSigContext); @@ -21,7 +22,8 @@ const MultiSigToolbar = () => { const multiSigData = useContext(MultiSigDataContext); const scanContext = useContext(QrCodeContext); const submitContext = useContext(TxSubmitContext); - + const message = useContext(MessageContext); + const signer = useSignerWallet(data.wallet); const getLabel = () => { switch (multiSigData.state) { case MultiSigStateEnum.SIGNING: @@ -96,26 +98,19 @@ const MultiSigToolbar = () => { }; const processNewData = async (newContent: string) => { - const clipBoardData = JSON.parse(newContent) as MultiSigShareData; - const tx = wasm.ReducedTransaction.sigma_parse_bytes( - Buffer.from(clipBoardData.tx, 'base64'), - ); - if (tx.unsigned_tx().id().to_str() !== data.tx?.id().to_str()) { - throw Error('Invalid transaction'); + if (signer) { + const clipBoardData = JSON.parse(newContent) as MultiSigShareData; + const verification = await verifyAndSaveData( + clipBoardData, + data.wallet, + signer, + data.tx?.id().to_str(), + ); + message.insert( + verification.message, + verification.valid ? 'success' : 'error', + ); } - await updateMultiSigRow( - context.rowId, - clipBoardData.commitments, - context.data.secrets, - clipBoardData.signed || [], - clipBoardData.simulated || [], - Date.now(), - clipBoardData.partial - ? wasm.Transaction.sigma_parse_bytes( - Buffer.from(clipBoardData.partial, 'base64'), - ) - : undefined, - ); }; const publishAction = async () => { diff --git a/src/types/multi-sig.ts b/src/types/multi-sig.ts index 59a2671..c6467c7 100644 --- a/src/types/multi-sig.ts +++ b/src/types/multi-sig.ts @@ -90,13 +90,23 @@ export interface MultiSigDataContextType { setNeedPassword: (needPassword: boolean) => unknown; } +interface HintPublicKey { + op: string; + h: string; +} + +export interface SecretHintType { + hint: string; + challenge: string; + position: string; + proof: string; + pubkey: HintPublicKey; +} + export interface HintType { hint: string; secret?: string; - pubkey: { - op: string; - h: string; - }; + pubkey: HintPublicKey; type: string; a: string; position: string; @@ -106,7 +116,11 @@ export interface TxHintType { [key: string]: Array; } +export interface TxSecretHintType { + [key: string]: Array; +} + export interface TransactionHintBagType { publicHints: TxHintType; - secretHints: TxHintType; + secretHints: TxSecretHintType; }