diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ae6cc38f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["bech", "mithril", "multiasset"] +} diff --git a/package-lock.json b/package-lock.json index 20085852..aa1f6286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@emurgo/cip14-js": "^3.0.1", "@fontsource/ubuntu": "^5.0.8", "@ledgerhq/hw-transport-webusb": "^6.28.0", + "@mithril-dev/mithril-client-wasm": "^0.3.3", "@trezor/connect-web": "^9.0.11", "bip39": "^3.0.4", "crc": "^4.1.1", @@ -4570,6 +4571,11 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@mithril-dev/mithril-client-wasm": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mithril-dev/mithril-client-wasm/-/mithril-client-wasm-0.3.3.tgz", + "integrity": "sha512-DU0XD87cdnELhEbwOiZPEBytfnYjpxmCURGqD1scPC9rNRgsVghBv1QwE04hjDmszgc+7cuxCqDxBRhycLLdCQ==" + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", diff --git a/package.json b/package.json index c6923fa4..dac3842a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@fontsource/ubuntu": "^5.0.8", "@ledgerhq/hw-transport-webusb": "^6.28.0", "@trezor/connect-web": "^9.0.11", + "@mithril-dev/mithril-client-wasm": "^0.3.3", "bip39": "^3.0.4", "crc": "^4.1.1", "crypto-random-string": "^5.0.0", diff --git a/src/api/extension/index.js b/src/api/extension/index.js index 6e151167..a7660a73 100644 --- a/src/api/extension/index.js +++ b/src/api/extension/index.js @@ -144,6 +144,20 @@ export const getDelegation = async () => { }; }; +export const getTxCBOR = async (txHash) => { + const result = await blockfrostRequest(`/txs/${txHash}/cbor`); + if (!result || result.error) return null; + return result.cbor; + + // if ( + // txHash === + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd' + // ) { + // return '84a4008282582040f5ce4bc6dad1390c0c56b847f860b1fc27c45f1a531e63d16b0a498c21212200825820eadcfeb38de327a0def9ab03f11ca37ca050c7318e96c9bfcc7c51406a584eec01018282583900f0c60254ecb0addd4c7e40c28fd05b65014ab4c8ecece06c7dcee5a0724bf93336a8225e7ef152b41aea955173be91af19250edea1ddafab821a001226b8a1581c61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d935a15046696e616e636542696e617269657334028258390009fe15e51f5109d5ace334448318d378e8ddf69a45c61b1347a200ed7d2ccdc7af469706878018739bcfde9ae23f009c4ae38aee0a4b4f3a821b0000000253f75452a1581c61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d935a15046696e616e636542696e61726965733408021a0002bb41031a0308319aa1008282582015a834ae1b84aecb354d7b56dd9c2153d947328b0f2ae722a7e61b581b25598458404a29dfdcd159e16232f16df270fb25a3584f303dfcc798206069da32345d4f95f588577bb6928aadcb40af77c4198b6572755bca3c673f4debe2ad354bdea50c8258203fefb4a308e15d716883dea41d5f7fb508cbee5557ea585e7b3351c931b72eb6584057bff5ba29a71db4645da8129f279706a45d556b086445687fd11a19e700ea02c5997af1bbd51eba416151097220142763c6f8a7606f8791fb63ce8324ee3308f5f6'; + // } + // return '84a400818258206dd6cf3fa81222062093885417b5f872f4d5d5ebfcea60de2ba6dc32edf20f0101018382583900f0c60254ecb0addd4c7e40c28fd05b65014ab4c8ecece06c7dcee5a0724bf93336a8225e7ef152b41aea955173be91af19250edea1ddafab821a001226b8a1581c61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d935a15046696e616e636542696e6172696573340a82583900f0c60254ecb0addd4c7e40c28fd05b65014ab4c8ecece06c7dcee5a0724bf93336a8225e7ef152b41aea955173be91af19250edea1ddafab1a001e848082583900e60861fc19a8cb2b9c04b9f9d35b093d7733120851c1e0487386386412671c592938844c1400d65d134135e35b4f29cdc79aa1404a0e1112821a05e8dba6a8581c0d55169aefdbff511ea14a966c0519e0d964c073143201583099cddba14848656c6c6f4e465401581c1f39bdd2257939e0e14d76f7afff2a5bf1ead57224fd10f94c37ae9ea14848656c6c6f4e465401581c3bbd184d7b858623b77f8b203d2af8aff629a7ce72143a9d993e8ee9a14848656c6c6f4e465401581c599d7575d0c0756998c027c5c72461ede01088ca2e919d2b54cfb9afa14848656c6c6f4e465401581c61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d935a95046696e616e636542696e6172696573301a000f42405046696e616e636542696e6172696573321a000f42405046696e616e636542696e6172696573331a000f42405046696e616e636542696e6172696573341a000f42365046696e616e636542696e6172696573351a000f42405046696e616e636542696e6172696573361a000f42405046696e616e636542696e6172696573371a000f42405046696e616e636542696e6172696573381a000f423f5046696e616e636542696e6172696573391a000f4240581c8413eb5387d7cc27d87122c8f6420d95c5773e7cd9fb41c64d99b7dea14848656c6c6f4e465401581c87f310392793674823e9eea90fed63fec31dc2247ab58640b58bb95ea14848656c6c6f4e465401581c8e9d119fcf988b818e1d74361244eb44c886323fc92ed4fea9d46b94a14848656c6c6f4e465401021a0002fe75031a03082880a10081825820106b5fad29bdfe9d6cd55292292bbde7f4fff45adb9473bb186510f54594a80b5840c49f180ef7ab31bc0bbbbdb576c2ba023995782e5e1dce766a5de78fa922dcfbbdf1a0afe9f0b4e76e15fb193ddae3705a2d8910564b967d11719a752bce4d0bf5f6'; +}; + export const getPoolMetadata = async (poolId) => { if (!poolId) { throw new Error('poolId argument not provided'); @@ -213,9 +227,66 @@ export const getTransactions = async (paginate = 1, count = 10) => { txIndex: tx.tx_index, blockHeight: tx.block_height, })); + // return [ + // { + // txHash: + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd', + // txIndex: 1, + // blockHeight: 2152118, + // }, + // ].concat( + // result.map((tx) => ({ + // txHash: tx.tx_hash, + // txIndex: tx.tx_index, + // blockHeight: tx.block_height, + // })) + // ); }; export const getTxInfo = async (txHash) => { + // const fakeTx = { + // hash: '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd', + // block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940', + // block_height: 2152118, + // block_time: 1635505891, + // slot: 42000000, + // index: 1, + // output_amount: [ + // { + // unit: 'lovelace', + // quantity: '1189560', + // }, + // { + // unit: '61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d93546696e616e636542696e617269657334', + // quantity: '10', + // }, + // ], + // fees: '196213', + // deposit: '0', + // size: 433, + // invalid_before: null, + // invalid_hereafter: '13885913', + // utxo_count: 4, + // withdrawal_count: 0, + // mir_cert_count: 0, + // delegation_count: 0, + // stake_cert_count: 0, + // pool_update_count: 0, + // pool_retire_count: 0, + // asset_mint_or_burn_count: 0, + // redeemer_count: 0, + // valid_contract: true, + // }; + + // if ( + // txHash === + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd' + // ) { + // return new Promise((resolve) => { + // resolve(fakeTx); + // }); + // } + const result = await blockfrostRequest(`/txs/${txHash}`); if (!result || result.error) return null; return result; @@ -228,12 +299,116 @@ export const getBlock = async (blockHashOrNumb) => { }; export const getTxUTxOs = async (txHash) => { + // const fakeTx = { + // hash: '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd', + // inputs: [ + // { + // address: + // 'addr_test1qp4sxuprra7029sldt9feq00l4kveqa6yzusxma722ht33ta9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqfrchp5', + // amount: [ + // { + // unit: 'lovelace', + // quantity: '9999842058', + // }, + // { + // unit: '61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d93546696e616e636542696e617269657334', + // quantity: '10', + // }, + // ], + // tx_hash: + // '40f5ce4bc6dad1390c0c56b847f860b1fc27c45f1a531e63d16b0a498c212122', + // output_index: 0, + // data_hash: null, + // inline_datum: null, + // reference_script_hash: null, + // collateral: false, + // reference: false, + // }, + // { + // address: + // 'addr_test1qq2tatdcwmg29fvnlch3k5un38sqwvffpygpwr56r0ncapra9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaq3vredr', + // amount: [ + // { + // unit: 'lovelace', + // quantity: '9998831507', + // }, + // ], + // tx_hash: + // 'eadcfeb38de327a0def9ab03f11ca37ca050c7318e96c9bfcc7c51406a584eec', + // output_index: 1, + // data_hash: null, + // inline_datum: null, + // reference_script_hash: null, + // collateral: false, + // reference: false, + // }, + // ], + // outputs: [ + // { + // address: + // 'addr_test1qrcvvqj5ajc2mh2v0eqv9r7stdjszj45erkwecrv0h8wtgrjf0unxd4gyf08au2jksdw4923wwlfrtcey58dagwa474sn0jfs8', + // amount: [ + // { + // unit: 'lovelace', + // quantity: '1189560', + // }, + // { + // unit: '61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d93546696e616e636542696e617269657334', + // quantity: '2', + // }, + // ], + // output_index: 0, + // data_hash: null, + // inline_datum: null, + // collateral: false, + // reference_script_hash: null, + // }, + // { + // address: + // 'addr_test1qqylu909ragsn4dvuv6yfqcc6duw3h0knfzuvxcng73qpmta9nxu0t6xjurg0qqcwwdulh56uglsp8z2uw9wuzjtfuaqdgqmcp', + // amount: [ + // { + // unit: 'lovelace', + // quantity: '9998652498', + // }, + // { + // unit: '61d87fff4c6150cb4e416c4bc9a0f497f6a50916a47e1b8b5aa7d93546696e616e636542696e617269657334', + // quantity: '8', + // }, + // ], + // output_index: 1, + // data_hash: null, + // inline_datum: null, + // collateral: false, + // reference_script_hash: null, + // }, + // ], + // }; + + // if ( + // txHash === + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd' + // ) { + // return new Promise((resolve) => { + // resolve(fakeTx); + // }); + // } + const result = await blockfrostRequest(`/txs/${txHash}/utxos`); if (!result || result.error) return null; return result; }; export const getTxMetadata = async (txHash) => { + // if ( + // txHash === + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd' + // ) { + // return new Promise((resolve) => { + // resolve([]); + // }); + // } + const result = await blockfrostRequest(`/txs/${txHash}/metadata`); if (!result || result.error) return null; return result; @@ -1558,12 +1733,12 @@ export const getAdaHandle = async (assetName) => { const network = await getNetwork(); if (!network) return null; let handleUrl; - switch (network.id){ + switch (network.id) { case 'mainnet': - handleUrl = 'https://api.handle.me' + handleUrl = 'https://api.handle.me'; break; case 'preprod': - handleUrl = 'https://preprod.api.handle.me' + handleUrl = 'https://preprod.api.handle.me'; break; default: return null; @@ -1769,7 +1944,9 @@ export const getAsset = async (unit) => { const metadata = metadataDatum && Data.toJson(metadataDatum.fields[0]); asset.displayName = metadata.name; - asset.image = metadata.image ? linkToSrc(convertMetadataPropToString(metadata.image)) : ''; + asset.image = metadata.image + ? linkToSrc(convertMetadataPropToString(metadata.image)) + : ''; asset.decimals = 0; } catch (_e) { asset.displayName = asset.name; @@ -1796,7 +1973,8 @@ export const getAsset = async (unit) => { const metadata = metadataDatum && Data.toJson(metadataDatum.fields[0]); asset.displayName = metadata.name; - asset.image = linkToSrc(convertMetadataPropToString(metadata.logo)) || ''; + asset.image = + linkToSrc(convertMetadataPropToString(metadata.logo)) || ''; asset.decimals = metadata.decimals || 0; } catch (_e) { asset.displayName = asset.name; diff --git a/src/config/config.js b/src/config/config.js index 58b92666..b425f19e 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -69,7 +69,8 @@ export const LOCAL_STORAGE = { export const NODE = { mainnet: 'https://cardano-mainnet.blockfrost.io/api/v0', testnet: 'https://cardano-testnet.blockfrost.io/api/v0', - preview: 'https://cardano-preview.blockfrost.io/api/v0', + // TMP: /tx/:hash/cbor only deployed on preview-dev + preview: 'https://cardano-preview-dev.blockfrost.io/api/v0', preprod: 'https://cardano-preprod.blockfrost.io/api/v0', }; diff --git a/src/config/provider.js b/src/config/provider.js index 67b9860c..e27de935 100644 --- a/src/config/provider.js +++ b/src/config/provider.js @@ -9,10 +9,11 @@ const networkToProjectId = { preview: secrets.PROJECT_ID_PREVIEW, }; +const base = (node = NODE.mainnet) => node; export default { api: { ipfs: 'https://ipfs.blockfrost.dev/ipfs', - base: (node = NODE.mainnet) => node, + base, header: { [secrets.NAMI_HEADER || 'dummy']: version }, key: (network = 'mainnet') => ({ project_id: networkToProjectId[network], @@ -23,5 +24,11 @@ export default { ) .then((res) => res.json()) .then((res) => res.cardano[currency]), + mithril: (network) => { + // const mithrilBaseURL = new URL('http://localhost:3000'); + let mithrilBaseURL = base(network).node; + mithrilBaseURL = mithrilBaseURL + '/mithril'; + return mithrilBaseURL; + }, }, }; diff --git a/src/features/mithril/MithrilModal.jsx b/src/features/mithril/MithrilModal.jsx new file mode 100644 index 00000000..0bbad903 --- /dev/null +++ b/src/features/mithril/MithrilModal.jsx @@ -0,0 +1,53 @@ +import { Button } from '@chakra-ui/button'; +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/modal'; +import { useDisclosure } from '@chakra-ui/react'; +import { Link, Text } from '@chakra-ui/layout'; +import React from 'react'; +import PrivacyPolicy from '../../../ui/app/components/privacyPolicy'; + +const MithrilModal = React.forwardRef((props, ref) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + + React.useImperativeHandle(ref, () => ({ + openModal() { + onOpen(); + }, + closeModal() { + onClose(); + }, + })); + return ( + <> + + + + Mithril + + + {props.tx} + + {/* + + */} + + + + ); +}); + +export default MithrilModal; diff --git a/src/ui/app/components/historyViewer.jsx b/src/ui/app/components/historyViewer.jsx index 17a95d91..6a839b8f 100644 --- a/src/ui/app/components/historyViewer.jsx +++ b/src/ui/app/components/historyViewer.jsx @@ -1,15 +1,271 @@ import { Box, Text, Spinner, Accordion, Button } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; import React from 'react'; +import Loader from '../../../api/loader'; import { File } from 'react-kawaii'; import { + getNetwork, getTransactions, + getTxCBOR, setTransactions, setTxDetail, } from '../../../api/extension'; import Transaction from './transaction'; import { useCaptureEvent } from '../../../features/analytics/hooks'; import { Events } from '../../../features/analytics/events'; +import initMithrilClient, { + MithrilClient, +} from '@mithril-dev/mithril-client-wasm'; +import provider from '../../../config/provider'; +import { output } from '../../../../webpack.config'; + +// const broadcast_channel = new BroadcastChannel('mithril-client'); +// broadcast_channel.onmessage = (e) => { +// let event = e.data; +// if (event.type == 'CertificateChainValidationStarted') { +// console.log('The certificate chain validation has started'); +// } else if (event.type == 'CertificateValidated') { +// console.log( +// 'A certificate has been validated, certificate_hash: ' + +// event.payload.certificate_hash +// ); +// } else if (event.type == 'CertificateChainValidated') { +// console.log('The certificate chain is valid'); +// } else { +// console.log(event); +// } +// }; + +const runMithrilVerification = async (txHashes, onStateChange) => { + const network = await getNetwork(); + + const genesis_verification_key = + '5b3132372c37332c3132342c3136312c362c3133372c3133312c3231332c3230372c3131372c3139382c38352c3137362c3139392c3136322c3234312c36382c3132332c3131392c3134352c31332c3233322c3234332c34392c3232392c322c3234392c3230352c3230352c33392c3233352c34345d'; + + // let aggregator_endpoint = + // 'http://mithril:mainnetjTvHPJCsB63oTESdYcua2ZhKTECveeIG@localhost:4000/backend/mithril'; + const aggregator_endpoint = provider.api.mithril(network); + + await initMithrilClient(); + + let client = await new MithrilClient( + aggregator_endpoint, + genesis_verification_key + ); + + // const myHeaders = new Headers(); + // myHeaders.append('project_id', 'test'); + + // client.set_additional_headers(myHeaders); + + if (onStateChange) onStateChange('fetchingProof'); + const proof = await client.unstable.get_cardano_transaction_proofs(txHashes); + console.log('Proof', proof); + + if (onStateChange) onStateChange('validatingCertificateChain'); + let proof_certificate = await client.verify_certificate_chain( + proof.certificate_hash + ); + console.log( + 'verify_certificate_chain OK, last_certificate_from_chain:', + proof_certificate + ); + + if (onStateChange) onStateChange('verifyingProof'); + try { + let valid_cardano_transaction_proof = + await client.unstable.verify_cardano_transaction_proof_then_compute_message( + proof, + proof_certificate + ); + console.log( + 'valid_cardano_transaction_proof:', + valid_cardano_transaction_proof + ); + + return proof; + } catch (error) { + console.error( + `Error while running mithril verification for tx ${txHashes}`, + error + ); + return null; + } finally { + if (onStateChange) onStateChange('done'); + } +}; + +export const multiAssetToArray = (multiAsset) => { + if (!multiAsset) return []; + const assetsArray = []; + const policyHashes = multiAsset.keys(); + + for (let i = 0; i < policyHashes.len(); i++) { + const policyId = policyHashes.get(i); + const assetsInPolicy = multiAsset.get(policyId); + if (!assetsInPolicy) continue; + + const assetNames = assetsInPolicy.keys(); + for (let j = 0; j < assetNames.len(); j++) { + const assetName = assetNames.get(j); + const amount = assetsInPolicy.get(assetName); + if (!amount) continue; + + const policyIdHex = Buffer.from(policyId.to_bytes()).toString('hex'); + const assetNameHex = Buffer.from(assetName.name()).toString('hex'); + + assetsArray.push({ + quantity: amount.to_str(), + unit: `${policyIdHex}${assetNameHex}`, + }); + } + } + return assetsArray; +}; + +const verifyCBORData = async (txHashes, history) => { + const verifiedTxHashes = []; + await Loader.load(); + + const Cardano = Loader.Cardano; + for (const txHash of txHashes) { + const txData = history.details[txHash]; + const txCBOR = await getTxCBOR(txHash); + + console.log(`Verifying ${txHash}...`); + + // if ( + // txHash !== + // '2b31cb16c501bae87940016bb73bf71513c3021abb0a29e9b04949d4220b92cd' + // ) { + // // TODO: TMP until Blockfrost provides tx CBOR endpoint + // verifiedTxHashes.push(txHash); + // continue; + // } + + if (!txData) { + continue; + } + if (!txData.utxos) { + console.log(`Missing UTXOs for tx ${txHash}`); + continue; + } + + if (!txCBOR) { + console.log(`Missing CBOR for tx ${txHash}`); + continue; + } + + // Note: There is a change that computing tx hash using old CML that is included in Nami is flawed. + // The computed tx hash may be different than the original despite the transaction being the same + // due to tx being reconstructed with non-canonical CBOR. + // CSL provides a way to safely compute original tx hash: + // https://github.com/Emurgo/cardano-serialization-lib/issues/604 + // const tx = Cardano.FixedTransaction.from_hex(txCBOR); + // const computedTxHash = Cardano.TransactionHash.from_bytes(blake2b(32).update(tx.raw_body()).digest('binary')); + + const tx = Cardano.Transaction.from_bytes(Buffer.from(txCBOR, 'hex')); + const txBody = tx.body(); + + // Verify that received tx hash matches the received CBOR data + const computedTxHash = Buffer.from( + Cardano.hash_transaction(txBody).to_bytes() + ).toString('hex'); + + if (txHash !== computedTxHash) { + console.log( + `Computed tx hash ${computedTxHash} does not match Blockfrost JSON data ${txHash}` + ); + } + + const { inputs, outputs } = txData.utxos; + + if (txBody.inputs().len() !== inputs.length) { + console.log( + `CBOR verification failed for tx ${txHash}. Inputs length mismatch (CBOR: ${txBody + .inputs() + .len()}, JSON: ${inputs.length})` + ); + continue; + } + + if (txBody.outputs().len() !== outputs.length) { + console.log( + `CBOR verification failed for tx ${txHash}. Outputs length mismatch (CBOR: ${txBody + .outputs() + .len()}, JSON: ${outputs.length})` + ); + continue; + } + + // Compare tx inputs + for (let i = 0; i < inputs.length; i++) { + const cborInput = txBody.inputs().get(i); + const cborInputTxHash = cborInput.transaction_id().to_hex(); + const cborInputIndex = cborInput.index().to_str(); + const jsonInput = inputs.find( + (input) => + input.tx_hash === cborInputTxHash && + input.output_index.toString() === cborInputIndex + ); + if (!jsonInput) { + console.log( + `Tx input index ${i} mismatch (CBOR input tx hash: ${cborInputTxHash} JSON: n/a` + ); + continue; + } + } + + // Compare tx outputs + for (let i = 0; i < outputs.length; i++) { + const cborOutput = txBody.outputs().get(i); + const cborOutputAmount = cborOutput.amount().coin().to_str(); + const cborOutputAddress = cborOutput.address().to_bech32(); + + const jsonOutput = outputs[i]; + + // lovelace amount + const jsonLovelaceAMount = jsonOutput.amount.find( + (a) => a.unit === 'lovelace' + )?.quantity; + if (cborOutputAmount !== jsonLovelaceAMount) { + console.log( + `amounts do not match. CBOR: ${cborOutput + .amount() + .coin() + .to_str()}, JSON: ${jsonLovelaceAMount}` + ); + continue; + } + // address + if (cborOutputAddress !== jsonOutput.address) { + console.log(`addresses do not match`); + continue; + } + + // compare assets + const cborAssets = multiAssetToArray(cborOutput.amount().multiasset()); + for (const cborAsset of cborAssets) { + const jsonAssetQuantity = jsonOutput.amount.find( + (a) => a.unit === cborAsset.unit + ).quantity; + const amountMatch = jsonAssetQuantity === cborAsset.quantity; + if (!amountMatch) { + console.log( + `amount of ${cborAsset.unit} does not match. Received: ${jsonAssetQuantity}. Expected: ${cborAsset.quantity}` + ); + } + } + } + + verifiedTxHashes.push(txHash); + } + + console.log(`CBOR verified txs`, verifiedTxHashes); + return { + verifiedTxHashes, + }; +}; const BATCH = 5; @@ -23,6 +279,10 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { const [page, setPage] = React.useState(1); const [final, setFinal] = React.useState(false); const [loadNext, setLoadNext] = React.useState(false); + const [isMithrilLoading, setIsMithrilLoading] = React.useState(false); + const [verificationData, setVerificationData] = React.useState(); + const [mithrilState, setMithrilState] = React.useState('ready'); + const getTxs = async () => { if (!history) { slice = []; @@ -47,7 +307,12 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { } } if (slice.length < page * BATCH) setFinal(true); + setHistorySlice(slice); + const verificationData = await runTxVerification(slice, (state) => + setMithrilState(state) + ); + setVerificationData(verificationData); }; React.useEffect(() => { @@ -73,6 +338,41 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { if (historySlice.length >= (page - 1) * BATCH) setLoadNext(false); }, [historySlice]); + const runTxVerification = async (txHashes, onStateChange) => { + console.log('runTxVerification'); + // let oldMithrilData = []; + // let oldCborData = []; + // if (verificationData) { + // oldMithrilData = verificationData.mithril.transactions_hashes.filter( + // (item) => !txHashes.includes(item) + // ); + // oldCborData = verificationData.cbor.verifiedTxHashes.filter( + // (item) => !txHashes.includes(item) + // ); + // } + if (onStateChange) onStateChange('verifyingCBOR'); + // Verify that tx JSON data matches CBOR data + const cborVerification = await verifyCBORData(txHashes, history); + // Run mithril verification + const proof = await runMithrilVerification(txHashes, onStateChange); + console.log('Mithril verified hashes', proof?.transactions_hashes); + // setVerificationData({ mithril: proof, cbor: cborVerification }); + + return { mithril: proof, cbor: cborVerification }; + // setVerificationData({ + // mithril: { + // ...proof, + // transactions_hashes: oldMithrilData.concat(proof.transactions_hashes), + // }, + // cbor: { + // verifiedTxHashes: oldCborData.concat(cborVerification.verifiedTxHashes), + // }, + // }); + }; + + console.log('verificationData', verificationData); + console.log('mithrilState', mithrilState); + return ( {!(history && historySlice) ? ( @@ -103,12 +403,24 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { > {historySlice.map((txHash, index) => { if (!history.details[txHash]) history.details[txHash] = {}; + const mithrilVerified = + verificationData?.mithril?.transactions_hashes.find( + (proofTxHash) => proofTxHash === txHash + ); + const cborVerified = verificationData?.cbor.verifiedTxHashes.find( + (proofTxHash) => proofTxHash === txHash + ); return ( { txObject[txHash] = txDetail; }} + mithrilVerified={mithrilVerified && cborVerified} + isMithrilLoading={mithrilState !== 'done'} + mithrilData={verificationData?.mithril} + mithrilState={mithrilState} + runTxVerification={runTxVerification} key={index} txHash={txHash} detail={history.details[txHash]} diff --git a/src/ui/app/components/transaction.jsx b/src/ui/app/components/transaction.jsx index a23f8526..5a704cc8 100644 --- a/src/ui/app/components/transaction.jsx +++ b/src/ui/app/components/transaction.jsx @@ -1,5 +1,5 @@ import { ExternalLinkIcon } from '@chakra-ui/icons'; -import React from 'react'; +import React, { useRef } from 'react'; import { updateTxInfo } from '../../../api/extension'; import UnitDisplay from './unitDisplay'; import { @@ -14,6 +14,8 @@ import { Icon, useColorModeValue, Skeleton, + Spinner, + Tooltip, } from '@chakra-ui/react'; import { compileOutputs } from '../../../api/util'; import TimeAgo from 'javascript-time-ago'; @@ -28,6 +30,7 @@ import { NETWORK_ID } from '../../../config/config'; import { useStoreState } from 'easy-peasy'; import { FaCoins, + FaCheckCircle, FaPiggyBank, FaTrashAlt, FaRegEdit, @@ -35,6 +38,7 @@ import { FaUsers, FaRegFileCode, IoRemoveCircleSharp, + IoWarning, TiArrowForward, TiArrowBack, TiArrowShuffle, @@ -43,6 +47,7 @@ import { } from 'react-icons/all'; import { useCaptureEvent } from '../../../features/analytics/hooks'; import { Events } from '../../../features/analytics/events'; +import MithrilModal from '../../../features/mithril/MithrilModal'; TimeAgo.addDefaultLocale(en); @@ -91,9 +96,16 @@ const Transaction = ({ addresses, network, onLoad, + runTxVerification, + mithrilState, + mithrilData, + mithrilVerified, + isMithrilLoading, }) => { const settings = useStoreState((state) => state.settings.settings); const isMounted = useIsMounted(); + const [isMithrilRevalidating, setIsMithrilRevalidating] = + React.useState(false); const [displayInfo, setDisplayInfo] = React.useState( genDisplayInfo(txHash, detail, currentAddr, addresses) ); @@ -142,6 +154,7 @@ const Transaction = ({ borderRadius={10} borderLeftRadius={30} p={0} + position={'relative'} _hover={{ backgroundColor: colorMode.txBgHover }} _focus={{ border: 'none' }} > @@ -156,6 +169,84 @@ const Transaction = ({ > + +
+ {!isMithrilLoading && !isMithrilRevalidating ? ( + <> + + {mithrilVerified ? ( + + Verified + + ) : ( + + + This transaction could not be verified on the + blockchain. It may be fraudulent, and the data + source could be compromised. For your safety, + please review this transaction carefully. + + + } + fontSize="sm" + hasArrow + placement="auto" + > + + Untrusted + + + )} + + ) : ( + <> + + + Verifying + + + )} +
+
{displayInfo && ( - + { + setIsMithrilRevalidating(true); + try { + const res = await runTxVerification(txHashes, onChangeState); + console.log(`Revalidation for ${txHashes}`, res); + return res; + } catch (error) { + console.error( + `Error while running mithril verification for tx ${displayInfo.txHash}`, + error + ); + } finally { + setIsMithrilRevalidating(false); + } + }} + /> )} @@ -306,14 +417,34 @@ const TxIcon = ({ txType, extra }) => { ); }; -const TxDetail = ({ displayInfo, network }) => { +const TxDetail = ({ + displayInfo, + runTxVerification, + mithril, + mithrilState, + network, +}) => { const capture = useCaptureEvent(); + const [localMithrilState, setLocalMithrilState] = React.useState(); + const [localVerificationData, setLocalVerificationData] = React.useState(); const colorMode = { extraDetail: useColorModeValue('black', 'white'), }; + const mithrilModalRef = useRef(); + const activeMithrilState = localMithrilState ?? mithrilState; + const activeMithrilData = localVerificationData ?? mithril; + + console.log('activeMithrilData', activeMithrilData); + console.log( + 'activeMithrilData?.latest_block_number', + activeMithrilData?.latest_block_number, + typeof activeMithrilData?.latest_block_number + ); + console.log('mithrilModalRef', mithrilModalRef.current); return ( <> + { + + + + Mithril + + + {activeMithrilState !== 'done' && ( + <>{activeMithrilState ?? 'Verifying'}... + )} + {activeMithrilData && activeMithrilState === 'done' && ( + <> + Certificate: {activeMithrilData.certificate_hash} + + Block: {activeMithrilData.latest_block_number.toString()} + + + )} + + {/* */} + + + { + // capture(Events.ActivityActivityDetailTransactionHashClick); + // }} + > + + + + + + + + {/* second col */} + + + {displayInfo.extra.length > 0 ? (