diff --git a/packages/yoroi-extension/app/api/ada/index.js b/packages/yoroi-extension/app/api/ada/index.js index a2053d4b99..33fd8083e9 100644 --- a/packages/yoroi-extension/app/api/ada/index.js +++ b/packages/yoroi-extension/app/api/ada/index.js @@ -63,6 +63,7 @@ import type { IPublicDeriver, UsedStatus, Value, + QueriedUtxo, } from './lib/storage/models/PublicDeriver/interfaces'; import type { BaseGetTransactionsRequest, @@ -83,8 +84,8 @@ import { generateAdaMnemonic, generateWalletRootKey, } from './lib/cardanoCrypto import { buildCoseSign1FromSignature, cip8Sign, makeCip8Key, v4PublicToV2 } from './lib/cardanoCrypto/utils'; import { isValidBip39Mnemonic, } from './lib/cardanoCrypto/wallet'; import type { CardanoSignTransaction } from 'trezor-connect-flow'; -import { createTrezorSignTxPayload, toTrezorSignRequest, } from './transactions/shelley/trezorTx'; -import { createLedgerSignTxPayload, toLedgerSignRequest, } from './transactions/shelley/ledgerTx'; +import { toTrezorSignRequest, } from './transactions/shelley/trezorTx'; +import { toLedgerSignRequest, } from './transactions/shelley/ledgerTx'; import { GenericApiError, IncorrectWalletPasswordError, @@ -157,7 +158,7 @@ import type { DefaultTokenEntry } from '../common/lib/MultiToken'; import { MultiToken } from '../common/lib/MultiToken'; import { getReceiveAddress } from '../../stores/stateless/addressStores'; import { generateRegistrationMetadata } from './lib/cardanoCrypto/catalyst'; -import { bytesToHex, fail, hexToBytes, hexToUtf, iterateLenGet } from '../../coreUtils'; +import { bytesToHex, fail, hexToBytes, hexToUtf, iterateLenGet, first, sorted } from '../../coreUtils'; import type { PersistedSubmittedTransaction } from '../localStorage'; import type WalletTransaction from '../../domain/WalletTransaction'; import { derivePrivateByAddressing, derivePublicByAddressing } from './lib/cardanoCrypto/deriveByAddressing'; @@ -256,8 +257,11 @@ export type CreateHWSignTxDataRequestFromRawTx = {| txBodyHex: string, network: $ReadOnly, addressingMap: string => (void | $PropertyType), + changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, senderUtxos: Array, additionalRequiredSigners?: Array, + ledgerSupportsCip36?: boolean, + catalystData?: LedgerNanoCatalystRegistrationTxSignData, |}; // createUnsignedTx @@ -519,7 +523,8 @@ export type GetTransactionRowsToExportFunc = ( export type ForeignUtxoFetcher = (Array) => Promise>; export const FETCH_TXS_BATCH_SIZE = 20; -const MIN_REORG_OUTPUT_AMOUNT = '1000000'; +const MIN_REORG_OUTPUT_AMOUNT = '1000000'; +const MAX_PICKED_COLLATERAL_UTXO_ADA = 10_000_000; // 10 ADA export default class AdaApi { @@ -747,63 +752,6 @@ export default class AdaApi { } } - createTrezorSignTxData(request: {| - signRequest: HaskellShelleyTxSignRequest, - network: $ReadOnly, - |}): CreateTrezorSignTxDataResponse { - try { - Logger.debug(`${nameof(AdaApi)}::${nameof(this.createTrezorSignTxData)} called`); - - const config = getCardanoHaskellBaseConfig( - request.network - ).reduce((acc, next) => Object.assign(acc, next), {}); - - const trezorSignTxPayload = createTrezorSignTxPayload( - request.signRequest, - config.ByronNetworkId, - Number.parseInt(config.ChainNetworkId, 10), - ); - Logger.debug(`${nameof(AdaApi)}::${nameof(this.createTrezorSignTxData)} success: ` + stringifyData(trezorSignTxPayload)); - return { - trezorSignTxPayload, - }; - } catch (error) { - Logger.error(`${nameof(AdaApi)}::${nameof(this.createTrezorSignTxData)} error: ` + stringifyError(error)); - if (error instanceof LocalizableError) throw error; - throw new GenericApiError(); - } - } - - createLedgerSignTxData( - request: CreateLedgerSignTxDataRequest - ): CreateLedgerSignTxDataResponse { - try { - Logger.debug(`${nameof(AdaApi)}::${nameof(this.createLedgerSignTxData)} called`); - - const config = getCardanoHaskellBaseConfig( - request.network - ).reduce((acc, next) => Object.assign(acc, next), {}); - - const ledgerSignTxPayload = createLedgerSignTxPayload({ - signRequest: request.signRequest, - byronNetworkMagic: config.ByronNetworkId, - networkId: Number.parseInt(config.ChainNetworkId, 10), - addressingMap: request.addressingMap, - cip36: request.cip36, - }); - - Logger.debug(`${nameof(AdaApi)}::${nameof(this.createLedgerSignTxData)} success: ` + stringifyData(ledgerSignTxPayload)); - return { - ledgerSignTxPayload - }; - } catch (error) { - Logger.error(`${nameof(AdaApi)}::${nameof(this.createLedgerSignTxData)} error: ` + stringifyError(error)); - - if (error instanceof LocalizableError) throw error; - throw new GenericApiError(); - } - } - createHwSignTxDataFromRawTx( hw: 'ledger' | 'trezor', request: CreateHWSignTxDataRequestFromRawTx @@ -826,8 +774,11 @@ export default class AdaApi { Number(config.ChainNetworkId), protocolMagic, addressMap, + request.changeAddrs ?? [], request.senderUtxos, request.additionalRequiredSigners ?? [], + request.ledgerSupportsCip36, + request.catalystData, ); Logger.debug(`${nameof(AdaApi)}::${nameof(this.createHwSignTxDataFromRawTx)} success: ` + stringifyData(ledgerSignTxPayload)); @@ -840,7 +791,9 @@ export default class AdaApi { Number(config.ChainNetworkId), protocolMagic, addressMap, + request.changeAddrs ?? [], request.senderUtxos, + request.catalystData, ); Logger.debug(`${nameof(AdaApi)}::${nameof(this.createHwSignTxDataFromRawTx)} success: ` + stringifyData(trezorSignTxPayload)); return { hw, result: { trezorSignTxPayload } }; @@ -1873,7 +1826,7 @@ export default class AdaApi { network, }); } - + const cip1852Wallet = await Cip1852Wallet.createCip1852Wallet( db, wallet.cip1852WrapperRow, @@ -2535,6 +2488,20 @@ export default class AdaApi { }); }; }; + + pickCollateralUtxo: ({| wallet: WalletState |}) => Promise = async ({ wallet }) => { + const allUtxos = wallet.utxos; + if (allUtxos.length === 0) { + fail('Cannot pick a collateral utxo! No utxo available at all in the wallet!'); + } + const utxoDefaultCoinAmount = (u: QueriedUtxo): BigNumber => + new BigNumber(u.output.tokens.find(x => x.Token.Identifier === '')?.TokenList.Amount ?? 0); + const compareDefaultCoins = (a: QueriedUtxo, b: QueriedUtxo): number => + utxoDefaultCoinAmount(a).comparedTo(utxoDefaultCoinAmount(b)); + const smallPureUtxos = allUtxos + .filter(u => u.output.tokens.length === 1 && utxoDefaultCoinAmount(u).lte(MAX_PICKED_COLLATERAL_UTXO_ADA)); + return first(sorted(smallPureUtxos, compareDefaultCoins)); + } } // ========== End of class AdaApi ========= diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js index 5e2c545486..804aa3cd07 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js @@ -31,12 +31,10 @@ import { VoterType, VoteOption, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; -import type { Address, Addressing, Value, } from '../../lib/storage/models/PublicDeriver/interfaces'; -import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest'; +import type { Addressing, Address, Value } from '../../lib/storage/models/PublicDeriver/interfaces'; +import type { LedgerNanoCatalystRegistrationTxSignData } from './HaskellShelleyTxSignRequest'; import { RustModule } from '../../lib/cardanoCrypto/rustLoader'; -import { toHexOrBase58 } from '../../lib/storage/bridge/utils'; -import { Bip44DerivationLevels, } from '../../lib/storage/database/walletTypes/bip44/api/utils'; -import { ChainDerivations, HARD_DERIVATION_START } from '../../../../config/numbersConfig'; +import { HARD_DERIVATION_START } from '../../../../config/numbersConfig'; import { derivePublicByAddressing } from '../../lib/cardanoCrypto/deriveByAddressing'; import { bytesToHex, @@ -47,174 +45,10 @@ import { iterateLenGetMap, maybe } from '../../../../coreUtils'; -import { mergeWitnessSets } from '../utils'; +import { transactionHexToHash } from '../../lib/cardanoCrypto/utils'; +import { WalletTypePurpose } from '../../../../config/numbersConfig'; // ==================== LEDGER ==================== // -/** Generate a payload for Ledger SignTx */ -export function createLedgerSignTxPayload(request: {| - signRequest: HaskellShelleyTxSignRequest, - byronNetworkMagic: number, - networkId: number, - addressingMap: string => (void | $PropertyType), - cip36: boolean, -|}): SignTransactionRequest { - - const tx = request.signRequest.unsignedTx.build_tx(); - const txBody = tx.body(); - - const tagsState = RustModule.WasmScope(Module => - Module.WalletV4.has_transaction_set_tag(tx.to_bytes())); - - if (tagsState === RustModule.WalletV4.TransactionSetsState.MixedSets) { - throw new Error('Transaction with mixed sets cannot be signed by Ledger'); - } - - const txHasSetTags = tagsState === RustModule.WalletV4.TransactionSetsState.AllSetsHaveTag; - - // Inputs - const ledgerInputs = _transformToLedgerInputs( - request.signRequest.senderUtxos - ); - - // Output - const ledgerOutputs = _transformToLedgerOutputs({ - networkId: request.networkId, - txOutputs: txBody.outputs(), - changeAddrs: request.signRequest.changeAddr, - addressingMap: request.addressingMap, - }); - - // withdrawals - const withdrawals = txBody.withdrawals(); - - const certificates = txBody.certs(); - - const ledgerWithdrawal = []; - if (withdrawals != null && withdrawals.len() > 0) { - ledgerWithdrawal.push(...formatLedgerWithdrawals( - withdrawals, - request.addressingMap, - )); - } - - const ledgerCertificates = []; - if (certificates != null && certificates.len() > 0) { - ledgerCertificates.push(...formatLedgerCertificates( - request.networkId, - certificates, - request.addressingMap, - )); - } - - let auxiliaryData = undefined; - if (request.signRequest.ledgerNanoCatalystRegistrationTxSignData) { - const { votingPublicKey, stakingKeyPath, nonce, paymentKeyPath, } = - request.signRequest.ledgerNanoCatalystRegistrationTxSignData; - - if (request.cip36) { - auxiliaryData = { - type: TxAuxiliaryDataType.CIP36_REGISTRATION, - params: { - format: CIP36VoteRegistrationFormat.CIP_36, - delegations: [ - { - type: CIP36VoteDelegationType.KEY, - voteKeyHex: votingPublicKey.replace(/^0x/, ''), - weight: 1, - }, - ], - stakingPath: stakingKeyPath, - paymentDestination: { - type: TxOutputDestinationType.DEVICE_OWNED, - params: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: paymentKeyPath, - stakingPath: stakingKeyPath, - }, - }, - }, - nonce, - votingPurpose: 0, - } - }; - } else { - auxiliaryData = { - type: TxAuxiliaryDataType.CIP36_REGISTRATION, - params: { - format: CIP36VoteRegistrationFormat.CIP_15, - voteKeyHex: votingPublicKey.replace(/^0x/, ''), - stakingPath: stakingKeyPath, - paymentDestination: { - type: TxOutputDestinationType.DEVICE_OWNED, - params: { - type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, - params: { - spendingPath: paymentKeyPath, - stakingPath: stakingKeyPath, - }, - }, - }, - nonce, - } - }; - } - } else if (request.signRequest.metadata != null) { - auxiliaryData = { - type: TxAuxiliaryDataType.ARBITRARY_HASH, - params: { - hashHex: RustModule.WalletV4.hash_auxiliary_data(request.signRequest.metadata).to_hex(), - }, - }; - } - - return { - signingMode: TransactionSigningMode.ORDINARY_TRANSACTION, - tx: { - inputs: ledgerInputs, - outputs: ledgerOutputs, - ttl: txBody.ttl_bignum()?.to_str() ?? null, - validityIntervalStart: txBody.validity_start_interval_bignum()?.to_str() ?? null, - fee: txBody.fee().to_str(), - network: { - networkId: request.networkId, - protocolMagic: request.byronNetworkMagic, - }, - withdrawals: ledgerWithdrawal.length === 0 ? null : ledgerWithdrawal, - certificates: ledgerCertificates.length === 0 ? null : ledgerCertificates, - auxiliaryData, - scriptDataHashHex: txBody.script_data_hash()?.to_hex() ?? null, - }, - additionalWitnessPaths: [], - options: { - tagCborSets: txHasSetTags, - } - }; -} - -/** - * Canonical inputs sorting: by tx hash and then by index - */ -function compareInputs(a: TxInput, b: TxInput): number { - if (a.txHashHex !== b.txHashHex) { - return a.txHashHex < b.txHashHex ? -1 : 1; - } - return a.outputIndex - b.outputIndex; -} - -function _transformToLedgerInputs( - inputs: Array -): Array { - for (const input of inputs) { - verifyFromDerivationRoot(input.addressing); - } - return inputs.map(input => ({ - txHashHex: input.tx_hash, - outputIndex: input.tx_index, - path: input.addressing.path, - })).sort(compareInputs); -} - function toLedgerTokenBundle( assets: ?RustModule.WalletV4.MultiAsset ): Array | null { @@ -268,54 +102,6 @@ function compareCborKey(hex1: string, hex2: string): number { return 0; } -function _transformToLedgerOutputs(request: {| - networkId: number, - txOutputs: RustModule.WalletV4.TransactionOutputs, - changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, - addressingMap: string => (void | $PropertyType), -|}): Array { - const result = []; - - // support post-alonzo map - - for (const output of iterateLenGet(request.txOutputs)) { - const address = output.address(); - const jsAddr = toHexOrBase58(address); - const datumHashHex = output.data_hash()?.to_hex() ?? null; - - const changeAddr = request.changeAddrs.find(change => jsAddr === change.address); - if (changeAddr != null) { - verifyFromDerivationRoot(changeAddr.addressing); - const addressParams = toLedgerAddressParameters({ - networkId: request.networkId, - address, - path: changeAddr.addressing.path, - addressingMap: request.addressingMap, - }); - result.push({ - amount: output.amount().coin().to_str(), - tokenBundle: toLedgerTokenBundle(output.amount().multiasset()), - destination: { - type: TxOutputDestinationType.DEVICE_OWNED, - params: addressParams, - }, - datumHashHex, - }); - } else { - result.push({ - amount: output.amount().coin().to_str(), - tokenBundle: toLedgerTokenBundle(output.amount().multiasset()), - destination: { - type: TxOutputDestinationType.THIRD_PARTY, - params: { addressHex: address.to_hex() }, - }, - datumHashHex, - }); - } - } - return result; -} - function formatLedgerWithdrawals( withdrawals: RustModule.WalletV4.Withdrawals, addressingMap: string => (void | { +path: Array, ... }), @@ -669,135 +455,6 @@ export function toLedgerAddressParameters(request: {| throw new Error(`${nameof(toLedgerAddressParameters)} unknown address type`); } -export function buildSignedTransaction( - tx: RustModule.WalletV4.Transaction, - senderUtxos: Array, - witnesses: Array, - publicKey: {| - ...Addressing, - key: RustModule.WalletV4.Bip32PublicKey, - |}, - metadata: RustModule.WalletV4.AuxiliaryData | void -): RustModule.WalletV4.Transaction { - const isSameArray = (array1: Array, array2: Array) => ( - array1.length === array2.length && array1.every((value, index) => value === array2[index]) - ); - const findWitness = (path: Array) => { - for (const witness of witnesses) { - if (isSameArray(witness.path, path)) { - return witness.witnessSignatureHex; - } - } - throw new Error(`${nameof(buildSignedTransaction)} no witness for ${JSON.stringify(path)}`); - }; - - const keyLevel = publicKey.addressing.startLevel + publicKey.addressing.path.length - 1; - - const witSet = RustModule.WalletV4.TransactionWitnessSet.new(); - const bootstrapWitnesses: Array = []; - const vkeys: Array = []; - - // Note: Ledger removes duplicate witnesses - // but there may be a one-to-many relationship - // ex: same witness is used in both a bootstrap witness and a vkey witness - const seenVKeyWit = new Set(); - const seenBootstrapWit = new Set(); - - for (const utxo of senderUtxos) { - verifyFromDerivationRoot(utxo.addressing); - - const witness = findWitness(utxo.addressing.path); - const addressKey = derivePublicByAddressing({ - addressing: utxo.addressing, - startingFrom: { - level: keyLevel, - key: publicKey.key, - } - }); - - if (RustModule.WalletV4.ByronAddress.is_valid(utxo.receiver)) { - - const byronAddr = RustModule.WalletV4.ByronAddress.from_base58(utxo.receiver); - const bootstrapWit = RustModule.WalletV4.BootstrapWitness.new( - RustModule.WalletV4.Vkey.new(addressKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness), - addressKey.chaincode(), - byronAddr.attributes(), - ); - const asString = bootstrapWit.to_hex(); - if (seenBootstrapWit.has(asString)) { - continue; - } - seenBootstrapWit.add(asString); - bootstrapWitnesses.push(bootstrapWit); - continue; - } - - const vkeyWit = RustModule.WalletV4.Vkeywitness.new( - RustModule.WalletV4.Vkey.new(addressKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness), - ); - const asString = vkeyWit.to_hex(); - if (seenVKeyWit.has(asString)) { - continue; - } - seenVKeyWit.add(asString); - vkeys.push(vkeyWit); - } - - // add any staking key needed - for (const witness of witnesses) { - const addressing = { - path: witness.path, - startLevel: 1, - }; - verifyFromDerivationRoot(addressing); - if (witness.path[Bip44DerivationLevels.CHAIN.level - 1] === ChainDerivations.CHIMERIC_ACCOUNT) { - const stakingKey = derivePublicByAddressing({ - addressing, - startingFrom: { - level: keyLevel, - key: publicKey.key, - } - }); - const vkeyWit = RustModule.WalletV4.Vkeywitness.new( - RustModule.WalletV4.Vkey.new(stakingKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness.witnessSignatureHex), - ); - const asString = vkeyWit.to_hex(); - if (seenVKeyWit.has(asString)) { - continue; - } - seenVKeyWit.add(asString); - vkeys.push(vkeyWit); - } - } - if (bootstrapWitnesses.length > 0) { - const bootstrapWitWasm = RustModule.WalletV4.BootstrapWitnesses.new(); - for (const bootstrapWit of bootstrapWitnesses) { - bootstrapWitWasm.add(bootstrapWit); - } - witSet.set_bootstraps(bootstrapWitWasm); - } - if (vkeys.length > 0) { - const vkeyWitWasm = RustModule.WalletV4.Vkeywitnesses.new(); - for (const vkey of vkeys) { - vkeyWitWasm.add(vkey); - } - witSet.set_vkeys(vkeyWitWasm); - } - - const mergedWitnessSet = RustModule.WalletV4.TransactionWitnessSet.from_hex( - mergeWitnessSets(tx.witness_set().to_hex(), witSet.to_hex()) - ); - - return RustModule.WalletV4.Transaction.new( - tx.body(), - mergedWitnessSet, - metadata - ); -} - type AddressMap = (addressHex: string) => ?Array; // Convert connector sign tx input into request to Ledger. @@ -809,8 +466,13 @@ export function toLedgerSignRequest( networkId: number, protocolMagic: number, ownAddressMap: AddressMap, + // when sending money, `ownAddressMap` doesn't contain the change address, so we need to + // pass it in explicitly + changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, senderUtxos: Array, additionalRequiredSigners: Array = [], + ledgerSupportsCip36?: boolean, + catalystData?: LedgerNanoCatalystRegistrationTxSignData, ): SignTransactionRequest { const tagsState = RustModule.WasmScope(Module => Module.WalletV4.has_transaction_set_tag( @@ -886,7 +548,8 @@ export function toLedgerSignRequest( networkId, baseAddr.payment_cred() ).to_address().to_hex(); - const ownPaymentPath = ownAddressMap(paymentAddress); + const ownPaymentPath = ownAddressMap(paymentAddress) || + changeAddrs.find(({ address }) => address === addr.to_hex())?.addressing.path; if (ownPaymentPath) { const stake = baseAddr.stake_cred(); const stakeAddr = RustModule.WalletV4.RewardAddress.new( @@ -1065,7 +728,6 @@ export function toLedgerSignRequest( ); } - // TODO: support CIP36 aux data let formattedAuxiliaryData = null; const auxiliaryDataHash = txBody.auxiliary_data_hash(); if (auxiliaryDataHash) { @@ -1077,6 +739,61 @@ export function toLedgerSignRequest( }; } + // note: we know that `catelystData` is only used for voting in the extension and there + // should be no other auxiliary data in this scenario so we just overwrite the auxiliary data + if (catalystData) { + const { votingPublicKey, stakingKeyPath, nonce, paymentKeyPath, } = catalystData; + + if (ledgerSupportsCip36) { + formattedAuxiliaryData = { + type: TxAuxiliaryDataType.CIP36_REGISTRATION, + params: { + format: CIP36VoteRegistrationFormat.CIP_36, + delegations: [ + { + type: CIP36VoteDelegationType.KEY, + voteKeyHex: votingPublicKey.replace(/^0x/, ''), + weight: 1, + }, + ], + stakingPath: stakingKeyPath, + paymentDestination: { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: paymentKeyPath, + stakingPath: stakingKeyPath, + }, + }, + }, + nonce, + votingPurpose: 0, + } + }; + } else { + formattedAuxiliaryData = { + type: TxAuxiliaryDataType.CIP36_REGISTRATION, + params: { + format: CIP36VoteRegistrationFormat.CIP_15, + voteKeyHex: votingPublicKey.replace(/^0x/, ''), + stakingPath: stakingKeyPath, + paymentDestination: { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: paymentKeyPath, + stakingPath: stakingKeyPath, + }, + }, + }, + nonce, + } + }; + } + } + let formattedCollateral = null; const collateral = txBody.collateral(); if (collateral) { @@ -1187,7 +904,7 @@ export function toLedgerSignRequest( } let signingMode = TransactionSigningMode.ORDINARY_TRANSACTION; - if (formattedCollateral) { + if (formattedCollateral || formattedReferenceInputs) { signingMode = TransactionSigningMode.PLUTUS_TRANSACTION; } @@ -1236,9 +953,18 @@ export function buildConnectorSignedTransaction( ...Addressing, key: RustModule.WalletV4.Bip32PublicKey, |}, -): string { + metadata: ?RustModule.WalletV4.AuxiliaryData, + // to support transfering from Byron address when initializing Ledger wallets + pathToReceiverMapping: Map = new Map(), +): {| txHex: string, txId: string |} { const fixedTx = RustModule.WalletV4.FixedTransaction.from_hex(rawTxHex); + if (metadata) { + fixedTx.set_auxiliary_data(metadata.to_bytes()); + const body = fixedTx.body(); + body.set_auxiliary_data_hash(RustModule.WalletV4.hash_auxiliary_data(metadata)); + fixedTx.set_body(body.to_bytes()); + } const keyLevel = publicKey.addressing.startLevel + publicKey.addressing.path.length - 1; for (const witness of witnesses) { @@ -1255,13 +981,30 @@ export function buildConnectorSignedTransaction( key: publicKey.key, } }); - const vkeyWit = RustModule.WalletV4.Vkeywitness.new( - RustModule.WalletV4.Vkey.new(witnessKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness.witnessSignatureHex), - ); + if (witness.path[0] === WalletTypePurpose.BIP44) { + const receiver = pathToReceiverMapping.get(witness.path.join('/')); + if (!receiver) { + continue; + } + const bootstrapWit = RustModule.WalletV4.BootstrapWitness.new( + RustModule.WalletV4.Vkey.new(witnessKey.to_raw_key()), + RustModule.WalletV4.Ed25519Signature.from_hex(witness.witnessSignatureHex), + witnessKey.chaincode(), + RustModule.WalletV4.ByronAddress.from_base58(receiver).attributes(), + ); - fixedTx.add_vkey_witness(vkeyWit); - } + fixedTx.add_bootstrap_witness(bootstrapWit); + } else if (witness.path[0] === WalletTypePurpose.CIP1852) { + const vkeyWit = RustModule.WalletV4.Vkeywitness.new( + RustModule.WalletV4.Vkey.new(witnessKey.to_raw_key()), + RustModule.WalletV4.Ed25519Signature.from_hex(witness.witnessSignatureHex), + ); - return fixedTx.to_hex(); + fixedTx.add_vkey_witness(vkeyWit); + } else { + throw new Error(`unexpected witness path purpose value ${witness.path[0]}`); + } + } + const txHex = fixedTx.to_hex(); + return { txHex, txId: transactionHexToHash(txHex)}; } diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.test.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.test.js index e70c1d3765..ec6e7b11bd 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.test.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.test.js @@ -1,11 +1,10 @@ // @flow import '../../lib/test-config.forTests'; -import BigNumber from 'bignumber.js'; import { RustModule } from '../../lib/cardanoCrypto/rustLoader'; import { - createLedgerSignTxPayload, - buildSignedTransaction, + toLedgerSignRequest, + buildConnectorSignedTransaction, toLedgerAddressParameters, } from './ledgerTx'; import { @@ -13,13 +12,13 @@ import { getCardanoSpendingKeyHash, normalizeToAddress, } from '../../lib/storage/bridge/utils'; -import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest'; import { AddressType, CertificateType, TransactionSigningMode, TxOutputDestinationType, CredentialParamsType, + TxOutputFormat, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import type { DeviceOwnedAddress, SignTransactionRequest } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { networks } from '../../lib/storage/database/prepackaged/networks'; @@ -260,7 +259,7 @@ test('Create Ledger transaction', async () => { RustModule.WalletV4.ByronAddress.from_base58(utxo.receiver), RustModule.WalletV4.TransactionInput.new( RustModule.WalletV4.TransactionHash.from_hex(utxo.tx_hash), - 1 + utxo.tx_index, ), RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str(utxo.amount)) ); @@ -269,7 +268,7 @@ test('Create Ledger transaction', async () => { keyHash, RustModule.WalletV4.TransactionInput.new( RustModule.WalletV4.TransactionHash.from_hex(utxo.tx_hash), - 1 + utxo.tx_index, ), RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str(utxo.amount)) ); @@ -316,39 +315,24 @@ test('Create Ledger transaction', async () => { ], }, }; - const signRequest = new HaskellShelleyTxSignRequest({ - unsignedTx: txBuilder, - changeAddr: [], - senderUtxos, - metadata: undefined, - networkSettingSnapshot: { - ChainNetworkId: Number.parseInt(baseConfig.ChainNetworkId, 10), - PoolDeposit: new BigNumber(baseConfig.PoolDeposit), - KeyDeposit: new BigNumber(baseConfig.KeyDeposit), - NetworkId: network.NetworkId, - }, - neededStakingKeyHashes: { - neededHashes: new Set([stakeCredential.to_hex()]), - wits: new Set() // not needed for this test, but something should be here - }, - }); const rewardAddressString = RustModule.WalletV4.RewardAddress.new( Number.parseInt(baseConfig.ChainNetworkId, 10), stakeCredential ).to_address().to_hex(); - const response = await createLedgerSignTxPayload({ - signRequest, - byronNetworkMagic: ByronNetworkId, - networkId: Number.parseInt(ChainNetworkId, 10), - addressingMap: (address) => { + const response = await toLedgerSignRequest( + txBuilder.build().to_hex(), + Number.parseInt(ChainNetworkId, 10), + ByronNetworkId, + (address) => { if (address === rewardAddressString) { - return stakingKeyInfo.addressing; + return stakingKeyInfo.addressing.path; } return undefined; }, - cip36: true, - }); + [], + senderUtxos, + ); expect(response).toStrictEqual(({ options: { @@ -405,6 +389,7 @@ test('Create Ledger transaction', async () => { outputIndex: 1, }], outputs: [{ + format: TxOutputFormat.ARRAY_LEGACY, destination: { params: { addressHex: '82d818582183581c891ac9abaac999b097c81ea3c0450b0fbb693d0bd232bebc0f4a391fa0001af2ff7e21', @@ -431,15 +416,21 @@ test('Create Ledger transaction', async () => { }, type: CertificateType.STAKE_REGISTRATION, }], - auxiliaryData: undefined, + auxiliaryData: null, validityIntervalStart: null, + collateralInputs: null, + collateralOutput: undefined, + includeNetworkId: false, + mint: null, + referenceInputs: null, + requiredSigners: null, + totalCollateral: null, }, additionalWitnessPaths: [], }: SignTransactionRequest)); - buildSignedTransaction( - txBuilder.build_tx(), - signRequest.senderUtxos, + buildConnectorSignedTransaction( + txBuilder.build_tx().to_hex(), [ // this witnesses doesn't belong to the transaction / key. Just used to test wit generation { diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js index 0e4dad9362..51ebc8dd4c 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js @@ -1,6 +1,5 @@ -// // @flow +// @flow import type { CardanoAddressedUtxo, } from '../types'; -import { verifyFromDerivationRoot } from '../../lib/storage/models/utils'; import { toDerivationPathString } from '../../lib/cardanoCrypto/keys/path'; import type { CardanoAddressParameters, @@ -11,7 +10,6 @@ import type { CardanoSignedTxWitness, CardanoSignTransaction, CardanoToken, - CardanoWithdrawal, } from 'trezor-connect-flow/index'; import { CardanoAddressType, @@ -22,161 +20,13 @@ import { CardanoTxWitnessType, CardanoDRepType, } from 'trezor-connect-flow'; -import type { Address, Addressing, Value, } from '../../lib/storage/models/PublicDeriver/interfaces'; -import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest'; -import { Bip44DerivationLevels, } from '../../lib/storage/database/walletTypes/bip44/api/utils'; -import { ChainDerivations, } from '../../../../config/numbersConfig'; - +import type { Addressing, Address, Value } from '../../lib/storage/models/PublicDeriver/interfaces'; +import type { TrezorTCatalystRegistrationTxSignData } from './HaskellShelleyTxSignRequest'; import { RustModule } from '../../lib/cardanoCrypto/rustLoader'; -import { toHexOrBase58 } from '../../lib/storage/bridge/utils'; -import blake2b from 'blake2b'; -import { derivePublicByAddressing } from '../../lib/cardanoCrypto/deriveByAddressing'; -import { bytesToHex, iterateLenGet, iterateLenGetMap, maybe, forceNonNull, hexToBytes } from '../../../../coreUtils'; -import { mergeWitnessSets } from '../utils'; +import { bytesToHex, iterateLenGet, iterateLenGetMap, forceNonNull, hexToBytes } from '../../../../coreUtils'; +import { transactionHexToHash } from '../../lib/cardanoCrypto/utils'; // ==================== TREZOR ==================== // -/** Generate a payload for Trezor SignTx */ -export function createTrezorSignTxPayload( - signRequest: HaskellShelleyTxSignRequest, - byronNetworkMagic: number, - networkId: number, -): $Exact { - const stakingKeyPath = (() => { - // TODO: this entire block is super hacky - // need to instead pass in a mapping from wallet addresses to addressing - // or add something similar to the sign request - - // assume the withdrawal is the same path as the UTXOs being spent - // so just take the first UTXO arbitrarily and change it to the staking key path - const firstUtxo = signRequest.senderUtxos[0]; - if (firstUtxo.addressing.startLevel !== Bip44DerivationLevels.PURPOSE.level) { - throw new Error(`${nameof(createTrezorSignTxPayload)} unexpected addressing start level`); - } - const result = [...firstUtxo.addressing.path]; - result[Bip44DerivationLevels.CHAIN.level - 1] = ChainDerivations.CHIMERIC_ACCOUNT; - result[Bip44DerivationLevels.ADDRESS.level - 1] = 0; - return result; - })(); - - const tx = signRequest.unsignedTx.build_tx(); - const txBody = tx.body(); - - const tagsState = RustModule.WasmScope(Module => - Module.WalletV4.has_transaction_set_tag(tx.to_bytes())); - - if (tagsState === RustModule.WalletV4.TransactionSetsState.MixedSets) { - throw new Error('Transaction with mixed sets cannot be signed by Ledger'); - } - - const txHasSetTags = tagsState === RustModule.WalletV4.TransactionSetsState.AllSetsHaveTag; - - // Inputs - const trezorInputs = _transformToTrezorInputs( - signRequest.senderUtxos - ); - - // Output - const trezorOutputs = _generateTrezorOutputs( - txBody.outputs(), - signRequest.changeAddr, - stakingKeyPath, - ); - - let request = { - signingMode: CardanoTxSigningMode.ORDINARY_TRANSACTION, - inputs: trezorInputs, - outputs: trezorOutputs, - fee: txBody.fee().to_str(), - ttl: txBody.ttl_bignum()?.to_str(), - validityIntervalStart: txBody.validity_start_interval_bignum()?.to_str(), - scriptDataHash: txBody.script_data_hash()?.to_hex(), - protocolMagic: byronNetworkMagic, - networkId, - }; - - // withdrawals - const withdrawals = txBody.withdrawals(); - request = withdrawals == null - ? request - : { - ...request, - withdrawals: formatTrezorWithdrawals( - withdrawals, - [stakingKeyPath], - ) - }; - - // certificates - const certificates = txBody.certs(); - request = certificates == null - ? request - : { - ...request, - certificates: formatTrezorCertificates( - certificates, - (_) => stakingKeyPath, - ) - }; - - if (signRequest.trezorTCatalystRegistrationTxSignData) { - const { votingPublicKey, nonce, paymentKeyPath } = - signRequest.trezorTCatalystRegistrationTxSignData; - request = { - ...request, - auxiliaryData: { - cVoteRegistrationParameters: { - delegations: [ - { - votePublicKey: votingPublicKey.replace(/^0x/, ''), - weight: 1, - } - ], - stakingPath: stakingKeyPath, - paymentAddressParameters: { - addressType: CardanoAddressType.BASE, - path: paymentKeyPath, - stakingPath: stakingKeyPath, - }, - nonce: String(nonce), - format: CardanoGovernanceRegistrationFormat.CIP36, - votingPurpose: 0, - }, - } - }; - } else { - const metadata = signRequest.metadata; - request = metadata === undefined - ? request - : { - ...request, - auxiliaryData: { - hash: blake2b(256 / 8).update(metadata.to_bytes()).digest('hex') - } - }; - } - if (txHasSetTags) { - request = { - ...request, - tagCborSets: true, - }; - } - return request; -} - -function formatTrezorWithdrawals( - withdrawals: RustModule.WalletV4.Withdrawals, - paths: Array>, -): Array { - return iterateLenGetMap(withdrawals) - .values() - .nonNull() - .zip(paths) - .map(([withdrawalAmount, path]) => ({ - amount: withdrawalAmount.to_str(), - path, - })) - .toArray(); -} function formatTrezorCertificates( certificates: RustModule.WalletV4.Certificates, @@ -252,29 +102,6 @@ function formatTrezorCertificates( return result; } -/** - * Canonical inputs sorting: by tx hash and then by index - */ -function compareInputs(a: CardanoInput, b: CardanoInput): number { - if (a.prev_hash !== b.prev_hash) { - return a.prev_hash < b.prev_hash ? -1 : 1; - } - return a.prev_index - b.prev_index; -} - -function _transformToTrezorInputs( - inputs: Array -): Array { - for (const input of inputs) { - verifyFromDerivationRoot(input.addressing); - } - return inputs.map(input => ({ - prev_hash: input.tx_hash, - prev_index: input.tx_index, - path: toDerivationPathString(input.addressing.path), - })).sort(compareInputs); -} - function toTrezorTokenBundle( assets: ?RustModule.WalletV4.MultiAsset ): {| @@ -304,61 +131,6 @@ function toTrezorTokenBundle( return { tokenBundle }; } -function _generateTrezorOutputs( - txOutputs: RustModule.WalletV4.TransactionOutputs, - changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, - stakingKeyPath: Array, -): Array { - const result = []; - for (const output of iterateLenGet(txOutputs)) { - const address = output.address(); - const jsAddr = toHexOrBase58(output.address()); - - // support post-alonzo map - - const tokenBundle = toTrezorTokenBundle(output.amount().multiasset()); - const dataHash = maybe(output.data_hash()?.to_hex(), datumHash => ({ datumHash })) ?? {}; - - const changeAddr = changeAddrs.find(change => jsAddr === change.address); - if (changeAddr != null) { - verifyFromDerivationRoot(changeAddr.addressing); - if (RustModule.WalletV4.BaseAddress.from_address(address)) { - result.push({ - addressParameters: { - addressType: CardanoAddressType.BASE, - path: changeAddr.addressing.path, - stakingPath: stakingKeyPath, - }, - amount: output.amount().coin().to_str(), - ...tokenBundle, - ...dataHash, - }); - } else if (RustModule.WalletV4.ByronAddress.from_address(address)) { - result.push({ - addressParameters: { - addressType: CardanoAddressType.BYRON, - path: changeAddr.addressing.path, - }, - amount: output.amount().coin().to_str(), - ...dataHash, - }); - } else { - throw new Error('unexpected change address type'); - } - } else { - const byronWasm = RustModule.WalletV4.ByronAddress.from_address(address); - result.push({ - address: byronWasm == null - ? address.to_bech32() - : byronWasm.to_base58(), - amount: output.amount().coin().to_str(), - ...tokenBundle, - ...dataHash, - }); - } - } - return result; -} export function toTrezorAddressParameters( address: RustModule.WalletV4.Address, @@ -426,128 +198,6 @@ export function toTrezorAddressParameters( throw new Error(`${nameof(toTrezorAddressParameters)} unknown address type`); } -export function buildSignedTransaction( - tx: RustModule.WalletV4.Transaction, - senderUtxos: Array, - witnesses: Array, - publicKey: {| - ...Addressing, - key: RustModule.WalletV4.Bip32PublicKey, - |}, - stakingKey: ?RustModule.WalletV4.Bip32PublicKey, - metadata: RustModule.WalletV4.AuxiliaryData | void, -): RustModule.WalletV4.Transaction { - const findWitness = (pubKey: string) => { - for (const witness of witnesses) { - if (witness.pubKey === pubKey) { - return witness.signature; - } - } - throw new Error(`${nameof(buildSignedTransaction)} no witness for ${pubKey}`); - }; - - const keyLevel = publicKey.addressing.startLevel + publicKey.addressing.path.length - 1; - - const witSet = RustModule.WalletV4.TransactionWitnessSet.new(); - const bootstrapWitnesses: Array = []; - const vkeys: Array = []; - - const seenVKeyWit = new Set(); - const seenBootstrapWit = new Set(); - - for (const utxo of senderUtxos) { - verifyFromDerivationRoot(utxo.addressing); - - const addressKey = derivePublicByAddressing({ - addressing: utxo.addressing, - startingFrom: { - level: keyLevel, - key: publicKey.key, - } - }); - const pubKey = addressKey.to_raw_key().to_hex(); - - const witness = findWitness(pubKey); - - if (RustModule.WalletV4.ByronAddress.is_valid(utxo.receiver)) { - - const byronAddr = RustModule.WalletV4.ByronAddress.from_base58(utxo.receiver); - const bootstrapWit = RustModule.WalletV4.BootstrapWitness.new( - RustModule.WalletV4.Vkey.new(addressKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness), - addressKey.chaincode(), - byronAddr.attributes(), - ); - const asString = bootstrapWit.to_hex(); - if (seenBootstrapWit.has(asString)) { - continue; - } - seenBootstrapWit.add(asString); - bootstrapWitnesses.push(bootstrapWit); - continue; - } - - const vkeyWit = RustModule.WalletV4.Vkeywitness.new( - RustModule.WalletV4.Vkey.new(addressKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness), - ); - const asString = vkeyWit.to_hex(); - if (seenVKeyWit.has(asString)) { - continue; - } - seenVKeyWit.add(asString); - vkeys.push(vkeyWit); - } - - // add any staking key needed - const stakingPubKey = stakingKey - ? bytesToHex(stakingKey.to_raw_key().as_bytes()) - : null; - - for (const witness of witnesses) { - if (witness.pubKey === stakingPubKey) { - if (stakingKey == null) { - throw new Error('unexpected nullish staking key'); - } - const vkeyWit = RustModule.WalletV4.Vkeywitness.new( - RustModule.WalletV4.Vkey.new(stakingKey.to_raw_key()), - RustModule.WalletV4.Ed25519Signature.from_hex(witness.signature), - ); - const asString = vkeyWit.to_hex(); - if (seenVKeyWit.has(asString)) { - continue; - } - seenVKeyWit.add(asString); - vkeys.push(vkeyWit); - } - } - - if (bootstrapWitnesses.length > 0) { - const bootstrapWitWasm = RustModule.WalletV4.BootstrapWitnesses.new(); - for (const bootstrapWit of bootstrapWitnesses) { - bootstrapWitWasm.add(bootstrapWit); - } - witSet.set_bootstraps(bootstrapWitWasm); - } - if (vkeys.length > 0) { - const vkeyWitWasm = RustModule.WalletV4.Vkeywitnesses.new(); - for (const vkey of vkeys) { - vkeyWitWasm.add(vkey); - } - witSet.set_vkeys(vkeyWitWasm); - } - - const mergedWitnessSet = RustModule.WalletV4.TransactionWitnessSet.from_hex( - mergeWitnessSets(tx.witness_set().to_hex(), witSet.to_hex()), - ); - - return RustModule.WalletV4.Transaction.new( - tx.body(), - mergedWitnessSet, - metadata - ); -} - type AddressMap = (addressHex: string) => ?Array; // Convert connector sign tx input into request to Trezor. @@ -559,7 +209,11 @@ export function toTrezorSignRequest( networkId: number, protocolMagic: number, ownAddressMap: AddressMap, + // when sending money, `ownAddressMap` doesn't contain the change address, so we need to + // pass it in explicitly + changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, senderUtxos: Array, + catalystData?: TrezorTCatalystRegistrationTxSignData, ): $Exact { const tagsState = RustModule.WasmScope(Module => Module.WalletV4.has_transaction_set_tag( @@ -643,7 +297,8 @@ export function toTrezorSignRequest( networkId, baseAddr.payment_cred() ).to_address().to_hex(); - const ownPaymentPath = ownAddressMap(paymentAddress); + const ownPaymentPath = ownAddressMap(paymentAddress) || + changeAddrs.find(({ address }) => address === addr.to_hex())?.addressing.path; if (ownPaymentPath) { const stake = baseAddr.stake_cred(); const stakeAddr = RustModule.WalletV4.RewardAddress.new( @@ -797,7 +452,6 @@ export function toTrezorSignRequest( formattedWithdrawals = result; } - // TODO: support CIP36 aux data let formattedAuxiliaryData = null; const auxiliaryDataHash = txBody.auxiliary_data_hash(); if (auxiliaryDataHash) { @@ -806,6 +460,31 @@ export function toTrezorSignRequest( }; } + // note: we know that `catelystData` is only used for voting in the extension and there + // should be no other auxiliary data in this scenario so we just overwrite the auxiliary data + if (catalystData) { + const { votingPublicKey, nonce, paymentKeyPath, stakingKeyPath } = catalystData; + formattedAuxiliaryData = { + cVoteRegistrationParameters: { + delegations: [ + { + votePublicKey: votingPublicKey.replace(/^0x/, ''), + weight: 1, + } + ], + stakingPath: stakingKeyPath, + paymentAddressParameters: { + addressType: CardanoAddressType.BASE, + path: paymentKeyPath, + stakingPath: stakingKeyPath, + }, + nonce: String(nonce), + format: CardanoGovernanceRegistrationFormat.CIP36, + votingPurpose: 0, + }, + }; + } + let formattedCollateral = null; const collateral = txBody.collateral(); if (collateral) { @@ -859,7 +538,6 @@ export function toTrezorSignRequest( } if (formattedCollateral) { result.collateralInputs = formattedCollateral; - result.signingMode = CardanoTxSigningMode.PLUTUS_TRANSACTION; } if (requiredSigners) { result.requiredSigners = formattedRequiredSigners; @@ -890,15 +568,25 @@ export function toTrezorSignRequest( result.tagCborSets = true; } + if (formattedCollateral || referenceInputs) { + result.signingMode = CardanoTxSigningMode.PLUTUS_TRANSACTION; + } return result; } export function buildConnectorSignedTransaction( rawTxHex: string, witnesses: Array, -): string { + metadata: ?RustModule.WalletV4.AuxiliaryData, +): {| txHex: string, txId: string |} { const fixedTx = RustModule.WalletV4.FixedTransaction.from_hex(rawTxHex); + if (metadata) { + fixedTx.set_auxiliary_data(metadata.to_bytes()); + const body = fixedTx.body(); + body.set_auxiliary_data_hash(RustModule.WalletV4.hash_auxiliary_data(metadata)); + fixedTx.set_body(body.to_bytes()); + } for (const witness of witnesses) { if (witness.type === CardanoTxWitnessType.BYRON_WITNESS) { @@ -919,5 +607,6 @@ export function buildConnectorSignedTransaction( } - return fixedTx.to_hex(); + const txHex = fixedTx.to_hex(); + return { txHex, txId: transactionHexToHash(txHex)}; } diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.test.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.test.js index 3ce0911960..748b70cee2 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.test.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.test.js @@ -1,19 +1,10 @@ // @flow import '../../lib/test-config.forTests'; -import BigNumber from 'bignumber.js'; import { RustModule } from '../../lib/cardanoCrypto/rustLoader'; -import { - createTrezorSignTxPayload, - toTrezorAddressParameters, -} from './trezorTx'; +import { toTrezorSignRequest, toTrezorAddressParameters } from './trezorTx'; import { networks } from '../../lib/storage/database/prepackaged/networks'; -import { HaskellShelleyTxSignRequest } from './HaskellShelleyTxSignRequest'; -import { - byronAddrToHex, - getCardanoSpendingKeyHash, - normalizeToAddress, -} from '../../lib/storage/bridge/utils'; +import { getCardanoSpendingKeyHash, normalizeToAddress } from '../../lib/storage/bridge/utils'; import { CardanoCertificateType, CardanoAddressType, @@ -172,7 +163,7 @@ test('Create Trezor transaction', async () => { RustModule.WalletV4.ByronAddress.from_base58(utxo.receiver), RustModule.WalletV4.TransactionInput.new( RustModule.WalletV4.TransactionHash.from_hex(utxo.tx_hash), - 1 + utxo.tx_index, ), RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str(utxo.amount)) ); @@ -181,7 +172,7 @@ test('Create Trezor transaction', async () => { keyHash, RustModule.WalletV4.TransactionInput.new( RustModule.WalletV4.TransactionHash.from_hex(utxo.tx_hash), - 1 + utxo.tx_index, ), RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str(utxo.amount)) ); @@ -189,7 +180,7 @@ test('Create Trezor transaction', async () => { } txBuilder.add_output( RustModule.WalletV4.TransactionOutput.new( - RustModule.WalletV4.Address.from_hex(byronAddrToHex('Ae2tdPwUPEZAVDjkPPpwDhXMSAjH53CDmd2xMwuR9tZMAZWxLhFphrHKHXe')), + RustModule.WalletV4.Address.from_bech32('addr1stvpskppsdvpezg6ex464jvekztus84rcpzskramdy7sh53jh67q7j3er7sqqxhjlalzzsk0pgc'), RustModule.WalletV4.Value.new(RustModule.WalletV4.BigNum.from_str('6323634')) ) ); @@ -214,52 +205,39 @@ test('Create Trezor transaction', async () => { .reduce((acc, next) => Object.assign(acc, next), {}); const { ByronNetworkId, ChainNetworkId } = baseConfig; - const response = await createTrezorSignTxPayload( - new HaskellShelleyTxSignRequest({ - unsignedTx: txBuilder, - changeAddr: [], - senderUtxos, - metadata: undefined, - networkSettingSnapshot: { - ChainNetworkId: Number.parseInt(baseConfig.ChainNetworkId, 10), - PoolDeposit: new BigNumber(baseConfig.PoolDeposit), - KeyDeposit: new BigNumber(baseConfig.KeyDeposit), - NetworkId: network.NetworkId, - }, - neededStakingKeyHashes: { - neededHashes: new Set([stakeCredential.to_hex()]), - wits: new Set() // not needed for this test, but something should be here - }, - }), - ByronNetworkId, + const response = toTrezorSignRequest( + txBuilder.build().to_hex(), Number.parseInt(ChainNetworkId, 10), + ByronNetworkId, + _address => [2147483692, 2147485463, 2147483648, 2, 0], + [], + senderUtxos, ); expect(response).toStrictEqual({ fee: '2000', ttl: '500', networkId: 1, protocolMagic: 764824073, - scriptDataHash: undefined, - validityIntervalStart: undefined, + includeNetworkId: false, inputs: [{ - path: `m/44'/1815'/0'/1/1`, + path: [2147483692, 2147485463, 2147483648, 1, 1], prev_hash: '058405892f66075d83abd1b7fe341d2d5bfd2f6122b2f874700039e5078e0dd5', prev_index: 1, }, { - path: `m/44'/1815'/0'/0/7`, + path: [2147483692, 2147485463, 2147483648, 0, 7], prev_hash: '1029eef5bb0f06979ab0b9530a62bac11e180797d08cab980fe39389d42b3657', prev_index: 0, }, { - path: `m/44'/1815'/0'/0/7`, + path: [2147483692, 2147485463, 2147483648, 0, 7], prev_hash: '2029eef5bb0f06979ab0b9530a62bac11e180797d08cab980fe39389d42b3658', prev_index: 0, }, { - path: `m/44'/1815'/0'/1/2`, + path: [2147483692, 2147485463, 2147483648, 1, 2], prev_hash: '3677e75c7ba699bfdc6cd57d42f246f86f69aefd76025006ac78313fad2bba20', prev_index: 1, }], outputs: [{ - address: 'Ae2tdPwUPEZAVDjkPPpwDhXMSAjH53CDmd2xMwuR9tZMAZWxLhFphrHKHXe', + address: 'addr1stvpskppsdvpezg6ex464jvekztus84rcpzskramdy7sh53jh67q7j3er7sqqxhjlalzzsk0pgc', amount: `6323634` }], certificates: [{ diff --git a/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.js b/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.js deleted file mode 100644 index 1e6f49f842..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.js +++ /dev/null @@ -1,240 +0,0 @@ -// @flow -import type { Node } from 'react'; -import React, { Component, } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import { intlShape } from 'react-intl'; -import type { MessageDescriptor, $npm$ReactIntl$IntlFormat } from 'react-intl'; - -import Dialog from '../../widgets/Dialog'; -import DialogCloseButton from '../../widgets/DialogCloseButton'; -import ErrorBlock from '../../widgets/ErrorBlock'; -import WarningBox from '../../widgets/WarningBox'; - -import globalMessages from '../../../i18n/global-messages'; -import LocalizableError from '../../../i18n/LocalizableError'; - -import ExplorableHashContainer from '../../../containers/widgets/ExplorableHashContainer'; -import RawHash from '../../widgets/hashWrappers/RawHash'; - -import { SelectedExplorer } from '../../../domain/SelectedExplorer'; -import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; -import styles from './HWSendConfirmationDialog.scss'; -import { truncateAddress, truncateToken } from '../../../utils/formatters'; -import { - MultiToken, -} from '../../../api/common/lib/MultiToken'; -import type { - TokenLookupKey, TokenEntry, -} from '../../../api/common/lib/MultiToken'; -import { getTokenName, genFormatTokenAmount, } from '../../../stores/stateless/tokenHelpers'; -import type { TokenRow } from '../../../api/ada/lib/storage/database/primitives/tables'; - -type ExpectedMessages = {| - infoLine1: MessageDescriptor, - infoLine2: MessageDescriptor, - sendUsingHWButtonLabel: MessageDescriptor, -|}; - -type Props = {| - +staleTx: boolean, - +selectedExplorer: SelectedExplorer, - +amount: MultiToken, - +receivers: Array, - +totalAmount: MultiToken, - +transactionFee: MultiToken, - +messages: ExpectedMessages, - +isSubmitting: boolean, - +error: ?LocalizableError, - +onSubmit: void => PossiblyAsync, - +onCancel: void => void, - +getTokenInfo: $ReadOnly> => $ReadOnly, - +unitOfAccountSetting: UnitOfAccountSettingType, - +addressToDisplayString: string => string, - +getCurrentPrice: (from: string, to: string) => ?string, -|}; - -@observer -export default class HWSendConfirmationDialog extends Component { - - static contextTypes: {|intl: $npm$ReactIntl$IntlFormat|} = { - intl: intlShape.isRequired, - }; - - renderSingleAmount: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo(entry))) - } - -
- ); - } - renderTotalAmount: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo(entry))) - } - -
- ); - } - renderSingleFee: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
- +{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo( - entry - ))) - } - -
- ); - } - - renderBundle: {| - amount: MultiToken, - render: TokenEntry => Node, - |} => Node = (request) => { - return ( - <> - {request.render(request.amount.getDefaultEntry())} - {request.amount.nonDefaultEntries().map(entry => ( - - {request.render(entry)} - - ))} - - ); - } - - render(): Node { - const { intl } = this.context; - const { - amount, - receivers, - isSubmitting, - messages, - error, - onCancel, - } = this.props; - - const staleTxWarning = ( -
- - {intl.formatMessage(globalMessages.staleTxnWarningLine1)}
- {intl.formatMessage(globalMessages.staleTxnWarningLine2)} -
-
- ); - - const infoBlock = ( -
-
    -
  • {intl.formatMessage(messages.infoLine1)}
  • -
  • {intl.formatMessage(messages.infoLine2)}
  • -
-
); - - const addressBlock = ( -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationAddressToLabel)} -
- {receivers.map((receiver, i) => ( - - - - {truncateAddress(this.props.addressToDisplayString(receiver))} - - - - ))} -
); - - const amountBlock = ( -
-
-
- {intl.formatMessage(globalMessages.amountLabel)} -
- {this.renderBundle({ - amount, - render: this.renderSingleAmount, - })} -
- -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationFeesLabel)} -
- {this.renderBundle({ - amount: this.props.transactionFee, - render: this.renderSingleFee, - })} -
-
); - - const totalAmountBlock = ( -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationTotalLabel)} -
- {this.renderBundle({ - amount: this.props.totalAmount, - render: this.renderTotalAmount, - })} -
); - - const confirmButtonClasses = classnames([ - 'confirmButton', - isSubmitting ? styles.submitButtonSpinning : null, - ]); - const actions = [ - { - label: intl.formatMessage(globalMessages.backButtonLabel), - disabled: isSubmitting, - onClick: onCancel, - }, - { - label: intl.formatMessage(messages.sendUsingHWButtonLabel), - onClick: this.props.onSubmit, - primary: true, - className: confirmButtonClasses, - isSubmitting, - }, - ]; - - return ( - } - > - {this.props.staleTx && staleTxWarning} - {infoBlock} - {addressBlock} - {amountBlock} - {totalAmountBlock} - - ); - } -} diff --git a/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.scss b/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.scss deleted file mode 100644 index d03d9b623d..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/HWSendConfirmationDialog.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import './WalletSendConfirmationDialog.scss'; // Reusing this as it's almost same - -.dialog { - .infoBlock { - background-color: var(--yoroi-palette-gray-50); - font-weight: 400; - padding: 10px; - margin-bottom: 30px; - line-height: 1.38; - font-size: 14px; - opacity: 0.7; - letter-spacing: 0.5px; - border-radius: 10px; - li { - list-style-type: disc; - margin-left: 18px; - } - } -} diff --git a/packages/yoroi-extension/app/components/wallet/send/SendTokensButton.js b/packages/yoroi-extension/app/components/wallet/send/SendTokensButton.js index 1a6fffd092..77e7ebbf63 100644 --- a/packages/yoroi-extension/app/components/wallet/send/SendTokensButton.js +++ b/packages/yoroi-extension/app/components/wallet/send/SendTokensButton.js @@ -3,7 +3,7 @@ import { TransactionResult } from '../../../UI/features/transaction-review/commo import { useTxReviewModal } from '../../../UI/features/transaction-review/module/ReviewTxProvider'; export const SendTokensButton = ({ disabled, onSuccess, label, stores }) => { - const { openTxReviewModal, startLoadingTxReview, showTxResultModal, isHardwareWallet, walletType } = useTxReviewModal(); + const { openTxReviewModal, startLoadingTxReview, showTxResultModal } = useTxReviewModal(); const handleSubmit = async () => { const signTxRequest = stores.transactionBuilderStore.updateTentativeTx(); @@ -18,54 +18,18 @@ export const SendTokensButton = ({ disabled, onSuccess, label, stores }) => { }); }; - const submitTx = async (passswordInput, signTxRequest) => { - const selectedWallet = stores.wallets.selected; - + const submitTx = async (password, signRequest) => { try { startLoadingTxReview(); - if (isHardwareWallet) { - if (walletType === 'ledger') { - const ledgerSendStore = stores.substores.ada.ledgerSend; - await ledgerSendStore.sendUsingLedgerWallet({ - params: { signRequest: signTxRequest }, - onSuccess: () => { - onSuccess(); - showTxResultModal(TransactionResult.SUCCESS); - }, - onFail: () => { - showTxResultModal(TransactionResult.FAIL); - }, - wallet: selectedWallet, - }); - - } - if (walletType === 'trezor') { - const trezorSendStore = stores.substores.ada.trezorSend; - await trezorSendStore.sendUsingTrezor({ - params: { signRequest: signTxRequest }, - onSuccess: () => { - onSuccess(); - showTxResultModal(TransactionResult.SUCCESS); - }, - onFail: () => { - showTxResultModal(TransactionResult.FAIL); - }, - wallet: selectedWallet, - }); - - } - } else { - await stores.substores.ada.mnemonicSend.sendMoney({ - signRequest: signTxRequest, - password: passswordInput, - wallet: selectedWallet, - onSuccess: () => { - onSuccess(); - showTxResultModal(TransactionResult.SUCCESS); - }, - - }); - } + stores.transactionProcessingStore.adaSendAndRefresh({ + wallet: stores.wallets.selected, + signRequest, + password, + callback: async () => { + onSuccess(); + showTxResultModal(TransactionResult.SUCCESS); + }, + }); } catch (error) { console.log('Send Sign Error', error); showTxResultModal(TransactionResult.FAIL); diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.js deleted file mode 100644 index d39cdd9a45..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.js +++ /dev/null @@ -1,298 +0,0 @@ -// @flow - -/* eslint react/jsx-one-expression-per-line: 0 */ // the   in the html breaks this - -import type { Node } from 'react'; -import React, { Component, } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import TextField from '../../common/TextField'; -import { intlShape } from 'react-intl'; -import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; -import vjf from 'mobx-react-form/lib/validators/VJF'; -import Dialog from '../../widgets/Dialog'; -import DialogCloseButton from '../../widgets/DialogCloseButton'; -import globalMessages from '../../../i18n/global-messages'; -import LocalizableError from '../../../i18n/LocalizableError'; -import styles from './WalletSendConfirmationDialog.scss'; -import config from '../../../config'; -import ExplorableHashContainer from '../../../containers/widgets/ExplorableHashContainer'; -import RawHash from '../../widgets/hashWrappers/RawHash'; -import { SelectedExplorer } from '../../../domain/SelectedExplorer'; -import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; -import WarningBox from '../../widgets/WarningBox'; -import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; -import { - truncateAddress, truncateToken, -} from '../../../utils/formatters'; -import { - MultiToken, -} from '../../../api/common/lib/MultiToken'; -import type { - TokenLookupKey, TokenEntry, -} from '../../../api/common/lib/MultiToken'; -import type { TokenRow } from '../../../api/ada/lib/storage/database/primitives/tables'; -import { getTokenName, genFormatTokenAmount } from '../../../stores/stateless/tokenHelpers'; -import { Box } from '@mui/system'; - -type Props = {| - +staleTx: boolean, - +selectedExplorer: SelectedExplorer, - +amount: MultiToken, - +receivers: Array, - +totalAmount: MultiToken, - +transactionFee: MultiToken, - +transactionSize: ?string, - +onSubmit: ({| password: string |}) => PossiblyAsync, - +addressToDisplayString: string => string, - +onCancel: void => void, - +isSubmitting: boolean, - +error: ?LocalizableError, - +unitOfAccountSetting: UnitOfAccountSettingType, - +getTokenInfo: $ReadOnly> => $ReadOnly, - +getCurrentPrice: (from: string, to: string) => ?string, -|}; - -@observer -export default class WalletSendConfirmationDialog extends Component { - - static contextTypes: {|intl: $npm$ReactIntl$IntlFormat|} = { - intl: intlShape.isRequired, - }; - - form: ReactToolboxMobxForm = new ReactToolboxMobxForm({ - fields: { - walletPassword: { - type: 'password', - label: this.context.intl.formatMessage(globalMessages.walletPasswordLabel), - placeholder: '', - value: '', - validators: [({ field }) => { - if (field.value === '') { - return [false, this.context.intl.formatMessage(globalMessages.fieldIsRequired)]; - } - return [true]; - }], - }, - } - }, { - options: { - validateOnChange: true, - validationDebounceWait: config.forms.FORM_VALIDATION_DEBOUNCE_WAIT, - }, - plugins: { - vjf: vjf() - }, - }); - - submit(): void { - this.form.submit({ - onSuccess: async (form) => { - const { walletPassword } = form.values(); - const transactionData = { - password: walletPassword, - }; - await this.props.onSubmit(transactionData); - }, - onError: () => {} - }); - } - - renderSingleAmount: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo(entry))) - } - -
- ); - } - renderTotalAmount: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo(entry))) - } - -
- ); - } - renderSingleFee: TokenEntry => Node = (entry) => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - return ( -
- +{formatValue(entry)} -  { - truncateToken(getTokenName(this.props.getTokenInfo( - entry - ))) - } - -
- ); - } - - renderBundle: {| - amount: MultiToken, - render: TokenEntry => Node, - |} => Node = (request) => { - return ( - <> - {request.render(request.amount.getDefaultEntry())} - {request.amount.nonDefaultEntries().map(entry => ( - - {request.render(entry)} - - ))} - - ); - } - - render(): Node { - const { form } = this; - const { intl } = this.context; - const walletPasswordField = form.$('walletPassword'); - const { - onCancel, - amount, - receivers, - isSubmitting, - error, - } = this.props; - - const staleTxWarning = ( -
- - {intl.formatMessage(globalMessages.staleTxnWarningLine1)}
- {intl.formatMessage(globalMessages.staleTxnWarningLine2)} -
-
- ); - - const confirmButtonClasses = classnames([ - 'confirmButton', - isSubmitting ? styles.submitButtonSpinning : null, - ]); - - const actions = [ - { - label: intl.formatMessage(globalMessages.backButtonLabel), - disabled: isSubmitting, - onClick: onCancel, - }, - { - label: intl.formatMessage(globalMessages.sendButtonLabel), - onClick: this.submit.bind(this), - primary: true, - className: confirmButtonClasses, - isSubmitting, - disabled: !walletPasswordField.isValid, - }, - ]; - - return ( - } - > - {this.props.staleTx && staleTxWarning} - -
-
-
- {intl.formatMessage(globalMessages.walletSendConfirmationAddressToLabel)} -
- {[...new Set(receivers)].map((receiver, i) => ( - - - - - {truncateAddress(this.props.addressToDisplayString(receiver))} - - - - - ))} -
- - {this.props.transactionSize != null ? ( -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationTxSizeLabel)} -
- - {this.props.transactionSize} - -
- ) : null} - -
-
-
- {intl.formatMessage(globalMessages.amountLabel)} -
- {this.renderBundle({ - amount, - render: this.renderSingleAmount, - })} -
- -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationFeesLabel)} -
- {this.renderBundle({ - amount: this.props.transactionFee, - render: this.renderSingleFee, - })} -
-
- -
-
- {intl.formatMessage(globalMessages.walletSendConfirmationTotalLabel)} -
- {this.renderBundle({ - amount: this.props.totalAmount, - render: this.renderTotalAmount, - })} -
- - -
- - {error - ? ( -
- {intl.formatMessage(error, error.values)} -
- ) - : null - } - -
- ); - } -} diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.scss b/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.scss deleted file mode 100644 index 59d27bc84d..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendConfirmationDialog.scss +++ /dev/null @@ -1,218 +0,0 @@ -@import '../../../themes/mixins/loading-spinner'; -@import '../../../themes/mixins/error-message'; - -.dialog { - font-weight: 500; - - :global { - .Dialog_title { - margin-bottom: 24px; - } - } - - .warningBox { - margin-bottom: 24px; - } - - .submitButtonSpinning { - @include loading-spinner('../../../assets/images/spinner-light.svg'); - } - - .currencySymbol { - font-weight: 300; - text-transform: uppercase; - } - - .addressToLabelWrapper, - .txSizeWrapper, - .amountFeesWrapper { - margin-bottom: 20px; - } - - .addressToLabel { - font-size: 11px; - font-weight: 500; - letter-spacing: 0.5px; - line-height: 1.38; - margin-left: 10px; - text-transform: uppercase; - } - - .addressTo { - font-size: 15px; - line-height: 1.38; - margin-top: 6px; - } - - .txSize { - font-size: 13px; - line-height: 1.28; - margin-top: 6px; - color: #b0b0b0; - } - - .amountFeesWrapper { - display: flex; - flex-direction: row; - - .amountWrapper, - .feesWrapper { - width: 50%; - } - - .amountWrapper { - margin-right: 10px; - } - - .feesWrapper { - margin-left: 10px; - } - - .amountLabel, - .feesLabel { - font-size: 11px; - font-weight: 500; - letter-spacing: 0.5px; - line-height: 1.38; - margin-left: 10px; - text-transform: uppercase; - } - - .amount, - .fees { - font-size: 15px; - line-height: 1.38; - margin-top: 6px; - word-break: break-word; - } - - .amountSmall, - .feesSmall { - @extend .amount; - font-size: 12px; - } - } - - .totalAmountLabel { - font-size: 11px; - font-weight: 500; - letter-spacing: 0.5px; - line-height: 1.38; - margin-left: 10px; - text-transform: uppercase; - } - - .totalAmount { - color: var(--yoroi-send-confirmation-dialog-send-values-color); - line-height: 1.38; - margin-top: 6px; - word-break: break-word; - } - - .totalAmountSmall { - @extend .totalAmount; - font-size: 13px; - } - - .walletPassword { - margin-top: 20px; - } - - .error { - @include error-message; - text-align: center; - } -} - -:global(.YoroiClassic) .dialog { - .amountFeesWrapper { - .amount, - .fees { - color: var(--yoroi-send-confirmation-dialog-send-values-color); - } - - .fees { - opacity: 0.5; - } - } -} - -:global(.YoroiModern) .dialog { - min-width: var(--yoroi-comp-dialog-min-width-lg); - max-width: var(--yoroi-comp-dialog-min-width-lg); - font-weight: 400; - - .currencySymbol { - font-weight: 400; - } - - .addressToLabelWrapper, - .amountFeesWrapper { - margin-bottom: 34px; - } - - .addressToLabel { - font-size: 16px; - font-weight: normal; - letter-spacing: 0; - margin-left: 0; - text-transform: none; - } - - .addressTo { - font-size: 14px; - line-height: 1.57; - margin-top: 2px; - } - - .totalAmountWrapper { - margin-bottom: 24px; - } - - .walletPassword { - margin-top: 0; - } - - .amountFeesWrapper { - .amountLabel, - .feesLabel { - font-size: 16px; - font-weight: normal; - letter-spacing: 0; - margin-left: 0; - text-transform: none; - } - - .amount, - .fees { - font-size: 14px; - line-height: 1.57; - margin-top: 2px; - opacity: 0.4; - } - - .amountSmall, - .feesSmall { - @extend .amount; - font-size: 12px; - } - } - - .totalAmountLabel { - font-size: 16px; - font-weight: normal; - letter-spacing: 0; - margin-left: 0; - text-transform: none; - } - - .totalAmount { - font-weight: 500; - line-height: 1.57; - margin-top: 2px; - - .currencySymbol { - font-weight: 500; - } - } -} diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js index 3b81a4dd3b..4645066415 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js @@ -48,18 +48,14 @@ import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType' import { calculateAndFormatValue } from '../../../utils/unit-of-account'; import { CannotSendBelowMinimumValueError } from '../../../api/common/errors'; import { getImageFromTokenMetadata } from '../../../utils/nftMetadata'; -import WalletSendPreviewStepContainer from './WalletSendFormSteps/WalletSendPreviewStepContainer'; import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; import { ampli } from '../../../../ampli/index'; import type { DomainResolverFunc, DomainResolverResponse } from '../../../stores/ada/AdaAddressesStore'; import { isResolvableDomain } from '@yoroi/resolver'; import SupportedAddressDomainsBanner from '../../../containers/wallet/SupportedAddressDomainsBanner'; -import type { SendMoneyRequest } from '../../../stores/toplevel/WalletStore'; import type { MaxSendableAmountRequest } from '../../../stores/toplevel/TransactionBuilderStore'; import type { WalletState } from '../../../../chrome/extension/background/types'; import LoadingSpinner from '../../widgets/LoadingSpinner'; -import LedgerSendStore from '../../../stores/ada/send/LedgerSendStore'; -import TrezorSendStore from '../../../stores/ada/send/TrezorSendStore'; import { SendTokensButton } from './SendTokensButton'; const messages = defineMessages({ @@ -212,21 +208,6 @@ type Props = {| +signRequest: null | ISignRequest, +staleTx: boolean, +openTransactionSuccessDialog: void => void, - +sendMoneyRequest: SendMoneyRequest, - +sendMoney: (params: {| - password: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - ... - }, - signRequest: ISignRequest, - onSuccess?: void => void, - |}) => Promise, - +ledgerSendError: null | LocalizableError, - +trezorSendError: null | LocalizableError, - +ledgerSend: LedgerSendStore, - +trezorSend: TrezorSendStore, |}; const SMemoTextField = styled(MemoTextField)(({ theme }) => ({ @@ -955,29 +936,6 @@ export default class WalletSendFormRevamp extends Component { /> ); - case SEND_FORM_STEP.PREVIEW: - return ( - - ); default: throw Error(`${step} is not a valid step`); } @@ -1063,21 +1021,19 @@ export default class WalletSendFormRevamp extends Component { > {this.renderCurrentStep(currentStep)} - {currentStep !== SEND_FORM_STEP.PREVIEW && ( - bodyRef.clientHeight ? '1px solid' : '0'} - borderColor="grayscale.200" - display="flex" - alignItems="center" - justifyContent="center" - gap="24px" - p="24px" - mx="-24px" - mt="30px" - > - {this.renderCurrentFooter(currentStep)} - - )} + bodyRef.clientHeight ? '1px solid' : '0'} + borderColor="grayscale.200" + display="flex" + alignItems="center" + justifyContent="center" + gap="24px" + p="24px" + mx="-24px" + mt="30px" + > + {this.renderCurrentFooter(currentStep)} + @@ -1091,17 +1047,6 @@ export default class WalletSendFormRevamp extends Component { this.maxStep = step; if (step === SEND_FORM_STEP.AMOUNT) { ampli.sendSelectAssetPageViewed(); - } else if (step === SEND_FORM_STEP.PREVIEW) { - const { totalInput } = this.props; - if (totalInput == null) { - throw new Error('expect totalInput'); - } - ampli.sendSelectAssetSelected({ - asset_count: totalInput.nonDefaultEntries().length, - }); - ampli.sendSummaryPageViewed({ - asset_count: totalInput.nonDefaultEntries().length, - }); } } } diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js deleted file mode 100644 index 59434363f8..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js +++ /dev/null @@ -1,638 +0,0 @@ -// @flow - -/* eslint react/jsx-one-expression-per-line: 0 */ // the   in the html breaks this - -import type { Node } from 'react'; -import type { UnitOfAccountSettingType } from '../../../../types/unitOfAccountType'; -import type { $npm$ReactIntl$IntlFormat, $npm$ReactIntl$MessageDescriptor } from 'react-intl'; -import type { TokenLookupKey, TokenEntry } from '../../../../api/common/lib/MultiToken'; -import type { TokenRow, NetworkRow } from '../../../../api/ada/lib/storage/database/primitives/tables'; -import type LocalizableError from '../../../../i18n/LocalizableError'; -import { observer } from 'mobx-react'; -import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; -import { SelectedExplorer } from '../../../../domain/SelectedExplorer'; -import { calculateAndFormatValue } from '../../../../utils/unit-of-account'; -import { truncateToken } from '../../../../utils/formatters'; -import { MultiToken } from '../../../../api/common/lib/MultiToken'; -import { getTokenName, genFormatTokenAmount } from '../../../../stores/stateless/tokenHelpers'; -import { Button, Link, Stack, Tooltip, Typography, styled } from '@mui/material'; -import { getNFTs, getTokens } from '../../../../utils/wallet'; -import { IncorrectWalletPasswordError } from '../../../../api/common/errors'; -import { isCardanoHaskell } from '../../../../api/ada/lib/storage/database/prepackaged/networks'; -import { Box } from '@mui/system'; -import { ReactComponent as InfoIcon } from '../../../../assets/images/attention-big-light.inline.svg'; -import React, { Component } from 'react'; -import TextField from '../../../common/TextField'; -import ReactToolboxMobxForm from '../../../../utils/ReactToolboxMobxForm'; -import vjf from 'mobx-react-form/lib/validators/VJF'; -import globalMessages from '../../../../i18n/global-messages'; -import styles from './WalletSendPreviewStep.scss'; -import config from '../../../../config'; -import WarningBox from '../../../widgets/WarningBox'; -import AssetsDropdown from './AssetsDropdown'; -import LoadingSpinner from '../../../widgets/LoadingSpinner'; -import { SEND_FORM_STEP } from '../../../../types/WalletSendTypes'; -import { ReactComponent as AttentionIcon } from '../../../../assets/images/attention-modern.inline.svg'; - -const SBox = styled(Box)(({ theme }) => ({ - backgroundImage: theme.palette.ds.bg_gradient_3, - color: 'ds.gray_min', -})); - -const SBoxHWNotes = styled(Box)(({ theme }) => ({ - flex: 1, - display: 'flex', - flexDirection: 'column', - padding: '16px', - marginTop: '16px', - backgroundImage: theme.palette.ds.bg_gradient_1, - borderRadius: '8px', -})); - -type Props = {| - +staleTx: boolean, - +selectedExplorer: SelectedExplorer, - +amount: MultiToken, - +receiver: {| address: string, handle: {| handle: string, nameServer: string |} | void |}, - +totalAmount: MultiToken, - +transactionFee: MultiToken, - +transactionSize: ?string, - +onSubmit: ({| password: string |}) => PossiblyAsync, - +addressToDisplayString: string => string, - +isSubmitting: boolean, - +unitOfAccountSetting: UnitOfAccountSettingType, - +getTokenInfo: ($ReadOnly>) => $ReadOnly, - +getCurrentPrice: (from: string, to: string) => ?string, - +isDefaultIncluded: boolean, - +minAda: ?MultiToken, - +plannedTxInfoMap: Array<{| - token: $ReadOnly, - amount?: string, - shouldSendAll?: boolean, - |}>, - +selectedNetwork: $ReadOnly, - +walletType: 'trezor' | 'ledger' | 'mnemonic', - +ledgerSendError: ?LocalizableError, - +trezorSendError: ?LocalizableError, - +onUpdateStep: (step: number) => void, -|}; - -type State = {| - passwordError: string | null, - txError: string | null, -|}; - -const messages = defineMessages({ - receiverHandleLabel: { - id: 'wallet.send.form.preview.receiverHandleLabel', - defaultMessage: '!!!Receiver', - }, - receiverLabel: { - id: 'wallet.send.form.preview.receiverLabel', - defaultMessage: '!!!Receiver wallet address', - }, - nAssets: { - id: 'wallet.send.form.preview.nAssets', - defaultMessage: '!!!{number} Assets', - }, - minAdaHelp: { - id: 'wallet.send.form.preview.minAdaHelp', - defaultMessage: '!!!Minimum ADA required to send these assets. {moreDetails}', - }, - moreDetails: { - id: 'wallet.send.form.preview.moreDetails', - defaultMessage: '!!!More details here', - }, - txConfirmationLedgerNanoLine1: { - id: 'wallet.send.ledger.confirmationDialog.info.line.1', - defaultMessage: '!!!After connecting your Ledger device to your computer’s USB port, press the Send using Ledger button.', - }, - sendUsingLedgerNano: { - id: 'wallet.send.ledger.confirmationDialog.submit', - defaultMessage: '!!!Send using Ledger', - }, - txConfirmationTrezorTLine1: { - id: 'wallet.send.trezor.confirmationDialog.info.line.1', - defaultMessage: '!!!After connecting your Trezor device to your computer, press the Send using Trezor button.', - }, - sendUsingTrezorT: { - id: 'wallet.send.trezor.confirmationDialog.submit', - defaultMessage: '!!!Send using Trezor', - }, - transactionErrorTitle: { - id: 'wallet.send.error.title.transactionError', - defaultMessage: '!!!Transaction Error', - }, - transactionErrorMnemonic: { - id: 'wallet.send.error.description.mnemonic', - defaultMessage: '!!!The transaction cannot be done due to technical reasons. Try again or ask our support team', - }, -}); - -@observer -export default class WalletSendPreviewStep extends Component { - static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { - intl: intlShape.isRequired, - }; - - state: State = { - passwordError: null, - txError: null, - }; - - form: ReactToolboxMobxForm = new ReactToolboxMobxForm( - { - fields: { - walletPassword: { - type: 'password', - label: this.context.intl.formatMessage(globalMessages.walletPasswordLabel), - placeholder: '', - value: '', - validators: [ - ({ field }) => { - if (field.value === '') { - return [false, this.context.intl.formatMessage(globalMessages.fieldIsRequired)]; - } - return [true]; - }, - ], - }, - }, - }, - { - options: { - validateOnChange: true, - validationDebounceWait: config.forms.FORM_VALIDATION_DEBOUNCE_WAIT, - }, - plugins: { - vjf: vjf(), - }, - } - ); - - submit(): void { - if (this.props.walletType === 'mnemonic') { - this.form.submit({ - onSuccess: async form => { - const { walletPassword } = form.values(); - const transactionData = { - password: walletPassword, - }; - try { - await this.props.onSubmit(transactionData); - } catch (error) { - const errorMessage = this.context.intl.formatMessage(error, error.values); - if (error instanceof IncorrectWalletPasswordError) { - this.setState({ passwordError: errorMessage }); - } else { - this.setState({ txError: errorMessage }); - } - } - }, - onError: () => {}, - }); - } else { - // hw wallets are not using passwords - this.props.onSubmit({ password: '' }); - } - } - - convertedToUnitOfAccount: (TokenEntry, string) => string = (token, toCurrency) => { - const tokenInfo = this.props.getTokenInfo(token); - - const shiftedAmount = token.amount.shiftedBy(-tokenInfo.Metadata.numberOfDecimals); - - const ticker = tokenInfo.Metadata.ticker; - if (ticker == null) { - throw new Error('unexpected main token type'); - } - const coinPrice = this.props.getCurrentPrice(ticker, toCurrency); - - if (coinPrice == null) return '-'; - - return calculateAndFormatValue(shiftedAmount, coinPrice); - }; - - renderDefaultTokenAmount: TokenEntry => Node = entry => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - return ( -
- {formatValue(entry)} -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} -
- ); - }; - - renderTotalAmount: TokenEntry => Node = entry => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - - const { unitOfAccountSetting } = this.props; - return unitOfAccountSetting.enabled ? ( - - - - {formatValue(entry)} - - -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} - - - - - {this.convertedToUnitOfAccount(entry, unitOfAccountSetting.currency)} - - -  {unitOfAccountSetting.currency} - - - - ) : ( - - - {formatValue(entry)} - - -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} - - - ); - }; - renderSingleFee: TokenEntry => Node = entry => { - const formatValue = genFormatTokenAmount(this.props.getTokenInfo); - return ( -
- {formatValue(entry)} -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} -
- ); - }; - - renderBundle: ({| - amount: MultiToken, - render: TokenEntry => Node, - |}) => Node = request => { - return ( - <> - {request.render(request.amount.getDefaultEntry())} - {request.amount.nonDefaultEntries().map(entry => ( - {request.render(entry)} - ))} - - ); - }; - - _amountLabel: void => Node = () => { - const { selectedNetwork, plannedTxInfoMap, minAda } = this.props; - const { intl } = this.context; - const isCardano = isCardanoHaskell(selectedNetwork); - - if (isCardano) { - const tokenInfo = plannedTxInfoMap.find(({ token }) => token.IsDefault); - if ( - (!tokenInfo || // Show Min-Ada label if the ADA is not included - // Or if included ADA less than Min-ADA - minAda?.getDefaultEntry().amount.gt(tokenInfo.amount ?? 0)) && - !tokenInfo?.shouldSendAll - ) { - const moreDetailsLink = ( - - {intl.formatMessage(messages.moreDetails)} - - ); - return ( - - {intl.formatMessage(globalMessages.minAda)} - - - - - } - > - svg': { - width: 20, - height: 20, - }, - }} - > - - - - - ); - } - } - - return {intl.formatMessage(globalMessages.amountLabel)}; - }; - - renderHWWalletInfo(): Node { - const { intl } = this.context; - const { walletType } = this.props; - if (walletType === 'mnemonic') { - return null; - } - - let infoLine1; - let infoLine2; - if (walletType === 'trezor') { - infoLine1 = messages.txConfirmationTrezorTLine1; - infoLine2 = globalMessages.txConfirmationTrezorTLine2; - } - if (walletType === 'ledger') { - infoLine1 = messages.txConfirmationLedgerNanoLine1; - infoLine2 = globalMessages.txConfirmationLedgerNanoLine2; - } - return ( - - - - {intl.formatMessage(infoLine1)} - - - - {intl.formatMessage(infoLine2)} - - - ); - } - - getSendButtonText(): $npm$ReactIntl$MessageDescriptor { - const { walletType } = this.props; - if (walletType === 'ledger') { - return messages.sendUsingLedgerNano; - } - if (walletType === 'trezor') { - return messages.sendUsingTrezorT; - } - return globalMessages.confirm; - } - - renderErrorBanner: (string, Node) => Node = (errorTitle, descriptionNode) => { - return ( - - - - - {errorTitle} - - - - {descriptionNode} - - - ); - }; - - renderError(): Node { - const { walletType } = this.props; - const { intl } = this.context; - const txErrorTitle = intl.formatMessage(messages.transactionErrorTitle); - if (walletType === 'mnemonic') { - const { txError } = this.state; - if (txError !== null) { - const re = /(.*)(.*)<\/link>(.*)/; - let m = intl.formatMessage(messages.transactionErrorMnemonic).match(re); - if (!m) { - // the translation has an error, fall back - m = [ - undefined, - 'The transaction cannot be done due to technical reasons. Try again or ', - 'ask our support team', - '' - ]; - } - return this.renderErrorBanner( - txErrorTitle, -
- {m[1]} - - {m[2]} - - {m[3]} -
- ); - } - return null; - } - if (walletType === 'trezor') { - const { trezorSendError } = this.props; - if (trezorSendError !== null) { - return this.renderErrorBanner(txErrorTitle, intl.formatMessage(trezorSendError)); - } - return null; - } - if (walletType === 'ledger') { - const { ledgerSendError } = this.props; - if (ledgerSendError !== null) { - return this.renderErrorBanner(txErrorTitle, intl.formatMessage(ledgerSendError)); - } - return null; - } - throw new Error('unexpected wallet type'); - } - - render(): Node { - const { form } = this; - const { intl } = this.context; - const walletPasswordField = form.$('walletPassword'); - const { amount, receiver, isSubmitting, walletType } = this.props; - const { passwordError } = this.state; - - const staleTxWarning = ( -
- - {intl.formatMessage(globalMessages.staleTxnWarningLine1)} -
- {intl.formatMessage(globalMessages.staleTxnWarningLine2)} -
-
- ); - - return ( -
- - - {this.renderError()} - {this.props.staleTx ?
{staleTxWarning}
: null} - {receiver.handle != null ? ( -
- - - {intl.formatMessage(messages.receiverHandleLabel)} - - - - - {receiver.handle?.nameServer}: {receiver.handle?.handle} - - -
- ) : null} -
- - - {intl.formatMessage(messages.receiverLabel)} - - - - - {this.props.addressToDisplayString(receiver.address)} - - -
- - - - {intl.formatMessage(globalMessages.walletSendConfirmationTotalLabel)} - -
- - {/* */} - {this.renderTotalAmount(this.props.totalAmount.getDefaultEntry())} - {/* */} - - {amount.nonDefaultEntries().length > 0 && ( - - {intl.formatMessage(messages.nAssets, { - number: amount.nonDefaultEntries().length, - })} - - )} -
-
- -
- - {intl.formatMessage(globalMessages.transactionFee)} - - - {this.renderBundle({ - amount: this.props.transactionFee, - render: this.renderSingleFee, - })} - -
- -
- - {this._amountLabel()} - - - {this.renderDefaultTokenAmount(amount.getDefaultEntry())} - -
- -
- {this.props.transactionSize != null ? ( -
- - {intl.formatMessage(globalMessages.walletSendConfirmationTxSizeLabel)} - - {this.props.transactionSize} -
- ) : null} - - - {amount.nonDefaultEntries().length > 0 && ( - - )} - - - {walletType === 'mnemonic' && ( - { - this.setState({ passwordError: null }); - walletPasswordField.set('value', e.target.value); - }} - error={walletPasswordField.error || passwordError} - sx={{ mt: '24px' }} - /> - )} -
- -
{this.renderHWWalletInfo()}
-
-
- - - - - - - -
- ); - } -} diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js deleted file mode 100644 index a9ce430ffa..0000000000 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js +++ /dev/null @@ -1,171 +0,0 @@ -// @flow -import type { Node } from 'react'; -import { Component } from 'react'; -import { observer } from 'mobx-react'; -import WalletSendPreviewStep from './WalletSendPreviewStep'; -import type { UnitOfAccountSettingType } from '../../../../types/unitOfAccountType'; -import LocalizableError from '../../../../i18n/LocalizableError'; -import { SelectedExplorer } from '../../../../domain/SelectedExplorer'; -import { addressToDisplayString } from '../../../../api/ada/lib/storage/bridge/utils'; -import type { ISignRequest } from '../../../../api/common/lib/transactions/ISignRequest'; -import type { TokenRow } from '../../../../api/ada/lib/storage/database/primitives/tables'; -import type { MultiToken, TokenLookupKey } from '../../../../api/common/lib/MultiToken'; -import { ampli } from '../../../../../ampli/index'; -import type { SendMoneyRequest } from '../../../../stores/toplevel/WalletStore'; -import { getNetworkById } from '../../../../api/ada/lib/storage/database/prepackaged/networks'; -import type { WalletState } from '../../../../../chrome/extension/background/types'; -import { HaskellShelleyTxSignRequest } from '../../../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import LedgerSendStore from '../../../../stores/ada/send/LedgerSendStore'; -import TrezorSendStore from '../../../../stores/ada/send/TrezorSendStore'; - -// TODO: unmagic the constants -const MAX_VALUE_BYTES = 5000; -const MAX_TX_BYTES = 16384; - -type Props = {| - +signRequest: null | ISignRequest, - +staleTx: boolean, - +unitOfAccountSetting: UnitOfAccountSettingType, - +isDefaultIncluded: boolean, - +plannedTxInfoMap: Array<{| - token: $ReadOnly, - amount?: string, - shouldSendAll?: boolean, - |}>, - +minAda: ?MultiToken, - +onUpdateStep: (step: number) => void, - +getCurrentPrice: (from: string, to: string) => ?string, - +getTokenInfo: ($ReadOnly>) => $ReadOnly, - +openTransactionSuccessDialog: void => void, - +sendMoneyRequest: SendMoneyRequest, - +sendMoney: (params: {| - password: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - ... - }, - signRequest: ISignRequest, - onSuccess?: void => void, - |}) => Promise, - +ledgerSendError: null | LocalizableError, - +trezorSendError: null | LocalizableError, - +ledgerSend: LedgerSendStore, - +trezorSend: TrezorSendStore, - selectedExplorer: Map, - +selectedWallet: WalletState, -|}; - -@observer -export default class WalletSendPreviewStepContainer extends Component { - componentWillUnmount() { - this.props.sendMoneyRequest.reset(); - this.props.ledgerSend.cancel(); - this.props.trezorSend.cancel(); - } - - onSubmit: ({| password: string |}) => Promise = async ({ password }) => { - const { signRequest, openTransactionSuccessDialog } = this.props; - const { ledgerSend, trezorSend, sendMoney, selectedWallet } = this.props; - - if (signRequest == null) throw new Error('Unexpected missing active signing request'); - - const amount = signRequest.totalInput(); - const { numberOfDecimals } = this.props.getTokenInfo(amount.getDefaultEntry()).Metadata; - ampli.sendSummarySubmitted({ - ada_amount: amount.getDefault().shiftedBy(-numberOfDecimals).toNumber(), - asset_count: signRequest.totalInput().nonDefaultEntries().length, - }); - - if (selectedWallet.type === 'ledger') { - await ledgerSend.sendUsingLedgerWallet({ - params: { signRequest }, - onSuccess: openTransactionSuccessDialog, - wallet: selectedWallet, - }); - } else if (selectedWallet.type === 'trezor') { - await trezorSend.sendUsingTrezor({ - params: { signRequest }, - onSuccess: openTransactionSuccessDialog, - wallet: selectedWallet, - }); - } else { - // walletType === 'mnemonic' - await sendMoney({ - signRequest, - password, - wallet: selectedWallet, - onSuccess: openTransactionSuccessDialog, - }); - } - }; - - render(): Node { - const { - signRequest, - unitOfAccountSetting, - onUpdateStep, - selectedWallet, - selectedExplorer, - sendMoneyRequest, - getTokenInfo, - getCurrentPrice, - } = this.props; - - if (selectedWallet == null) - throw new Error(`Active wallet required for ${nameof(WalletSendPreviewStepContainer)}`); - if (signRequest == null) throw new Error('Unexpected missing active signing request'); - - const totalInput = signRequest.totalInput(); - const fee = signRequest.fee(); - const size = signRequest.size?.(); - const fullSize = size ? size.full : 0; - const maxOutput = size ? Math.max(...size.outputs) : 0; - const showSize = - size != null && (size.full > MAX_TX_BYTES - 1000 || maxOutput > MAX_VALUE_BYTES - 1000); - const network = getNetworkById(selectedWallet.networkId); - - const receiverWithHandle = signRequest instanceof HaskellShelleyTxSignRequest - ? signRequest.receiverWithHandle() - : null; - const receiver = { - address: receiverWithHandle?.address ?? signRequest.receivers(false)[0], - handle: receiverWithHandle?.handle, - } - - return ( - { - throw new Error('No explorer for wallet network'); - })() - } - getTokenInfo={getTokenInfo} - getCurrentPrice={getCurrentPrice} - amount={totalInput.joinSubtractCopy(fee)} - receiver={receiver} - totalAmount={totalInput} - transactionFee={fee} - transactionSize={ - showSize - ? `${fullSize}/${MAX_TX_BYTES} (Biggest output: ${maxOutput}/${MAX_VALUE_BYTES})` - : null - } - onSubmit={this.onSubmit} - isSubmitting={sendMoneyRequest.isExecuting} - unitOfAccountSetting={unitOfAccountSetting} - addressToDisplayString={addr => addressToDisplayString(addr, network)} - selectedNetwork={network} - isDefaultIncluded={this.props.isDefaultIncluded} - plannedTxInfoMap={this.props.plannedTxInfoMap} - minAda={this.props.minAda} - walletType={selectedWallet.type} - ledgerSendError={this.props.ledgerSendError} - trezorSendError={this.props.trezorSendError} - onUpdateStep={onUpdateStep} - /> - ); - } -} diff --git a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js index 3d1f225738..b0790fe3a4 100644 --- a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js +++ b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js @@ -1036,6 +1036,7 @@ export default class ConnectorStore extends Store { Number(config.ChainNetworkId), config.ByronNetworkId, s => ownAddressMap[s], + [], addressedUtxos, ); } catch (e) { @@ -1084,7 +1085,7 @@ export default class ConnectorStore extends Store { throw new Error('hash mismatch'); } - return buildSignedTrezorTransaction(rawTxHex, trezorSignTxResp.witnesses); + return buildSignedTrezorTransaction(rawTxHex, trezorSignTxResp.witnesses).txHex; } async ledgerSignTx( @@ -1115,6 +1116,7 @@ export default class ConnectorStore extends Store { Number(config.ChainNetworkId), config.ByronNetworkId, s => ownAddressMap[s], + [], addressedUtxos, additionalRequiredSigners, ); @@ -1173,7 +1175,7 @@ export default class ConnectorStore extends Store { rawTxHex, ledgerSignResult.witnesses, publicKeyInfo, - ); + ).txHex; } /** diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js index 46de335b11..44d8b88d9b 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js @@ -283,15 +283,16 @@ function SwapPage(props: StoresProps & Intl): Node { validateSignRequestAndUserPassword(passswordInput); setOpenedDialog('loadingOverlay'); + const password = userPasswordState?.value; - const baseBroadcastRequest = { wallet, signRequest }; - const broadcastRequest = isHardwareWallet - ? { [walletType]: baseBroadcastRequest } - : { normal: { ...baseBroadcastRequest, password: passswordInput } }; try { - const refreshWallet = () => stores.wallets.refreshWalletFromRemote(wallet.publicDeriverId); - // $FlowIgnore[incompatible-call] - await stores.substores.ada.wallets.adaSendAndRefresh({ broadcastRequest, refreshWallet }); + await stores.transactionProcessingStore.adaSendAndRefresh({ + wallet, + signRequest, + password, + callback: () => stores.wallets.refreshWalletFromRemote(wallet.publicDeriverId), + }); + setOrderStepValue(2); showTxResultModal(TransactionResult.SUCCESS); diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js index 579974d01c..341422c92b 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js +++ b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js @@ -284,7 +284,7 @@ export default function SwapOrdersPage(props: StoresProps): Node { const submitTx = async (passswordInput, cancelTxCbor, signedCollateralReorgTx, order: any) => { try { startLoadingTxReview(); - const { signedTxHex: signedCancelTx } = await props.stores.substores.ada.wallets.adaSignTransactionHexFromWallet({ + const { signedTxHex: signedCancelTx } = await props.stores.transactionProcessingStore.adaSignTransactionHexFromWallet({ wallet, transactionHex: cancelTxCbor, password: passswordInput, @@ -320,7 +320,7 @@ export default function SwapOrdersPage(props: StoresProps): Node { } try { - const { signedTxHex: signedCollateralReorgTx } = await props.stores.substores.ada.wallets.adaSignTransactionHexFromWallet({ + const { signedTxHex: signedCollateralReorgTx } = await props.stores.transactionProcessingStore.adaSignTransactionHexFromWallet({ wallet, transactionHex: collateralReorgTxObj.cbor, password: passswordInput, diff --git a/packages/yoroi-extension/app/containers/transfer/TransferSendPage.js b/packages/yoroi-extension/app/containers/transfer/TransferSendPage.js index b054abf360..02f125cfa6 100644 --- a/packages/yoroi-extension/app/containers/transfer/TransferSendPage.js +++ b/packages/yoroi-extension/app/containers/transfer/TransferSendPage.js @@ -25,6 +25,7 @@ import { genAddressLookup } from '../../stores/stateless/addressStores'; import { genLookupOrFail } from '../../stores/stateless/tokenHelpers'; import { getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks'; import type { StoresProps } from '../../stores'; +import { HaskellShelleyTxSignRequest } from '../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; // populated by ConfigWebpackPlugin declare var CONFIG: ConfigType; @@ -67,10 +68,8 @@ export default class TransferSendPage extends Component<{| ...StoresProps, ...Lo componentWillUnmount() { const { stores } = this.props; - stores.wallets.sendMoneyRequest.reset(); + stores.transactionProcessingStore.sendMoneyRequest.reset(); this.props.transactionRequest.reset(); - stores.substores.ada.ledgerSend.cancel(); - stores.substores.ada.trezorSend.cancel(); } submit: void => Promise = async () => { @@ -81,34 +80,30 @@ export default class TransferSendPage extends Component<{| ...StoresProps, ...Lo } const signRequest = this.props.transactionRequest.result; if (signRequest == null) return; - if (this.spendingPasswordForm == null) { - if (selected.type === 'trezor') { - await stores.substores.ada.trezorSend.sendUsingTrezor({ - params: { signRequest }, - wallet: selected, - }); - } - if (selected.type === 'ledger') { - await stores.substores.ada.ledgerSend.sendUsingLedgerWallet({ - params: { signRequest }, - wallet: selected, - }); - } - if (stores.wallets.sendMoneyRequest.error == null) { - this.props.onSubmit.trigger(); + + const send = (password) => { + if (!(signRequest instanceof HaskellShelleyTxSignRequest)) { + throw new Error('unexpected signRequest type'); } + + stores.transactionProcessingStore.adaSendAndRefresh({ + wallet: selected, + signRequest, + password, + callback: async () => { + if (stores.transactionProcessingStore.sendMoneyRequest.error == null) { + this.props.onSubmit.trigger(); + } + }, + }); + } + if (this.spendingPasswordForm == null) { + send(null); } else { + // why do we have to submit the form this.spendingPasswordForm.submit({ onSuccess: async (form) => { - const { walletPassword } = form.values(); - await stores.substores.ada.mnemonicSend.sendMoney({ - signRequest, - password: walletPassword, - wallet: selected, - }); - if (stores.wallets.sendMoneyRequest.error == null) { - this.props.onSubmit.trigger(); - } + send(form.values().walletPassword); }, onError: () => {} }); @@ -159,7 +154,7 @@ export default class TransferSendPage extends Component<{| ...StoresProps, ...Lo ? ( this.setSpendingPasswordForm(form)} - isSubmitting={this.props.stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={this.props.stores.transactionProcessingStore.sendMoneyRequest.isExecuting} /> ) : null; @@ -178,9 +173,9 @@ export default class TransferSendPage extends Component<{| ...StoresProps, ...Lo label: this.props.onSubmit.label, trigger: this.submit, }} - isSubmitting={this.props.stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={this.props.stores.transactionProcessingStore.sendMoneyRequest.isExecuting} onCancel={this.props.onClose} - error={this.props.stores.wallets.sendMoneyRequest.error} + error={this.props.stores.transactionProcessingStore.sendMoneyRequest.error} dialogTitle={intl.formatMessage(globalMessages.walletSendConfirmationDialogTitle)} getCurrentPrice={this.props.stores.coinPriceStore.getCurrentPrice} unitOfAccountSetting={this.props.stores.profile.unitOfAccount} diff --git a/packages/yoroi-extension/app/containers/transfer/UpgradeTxDialogContainer.js b/packages/yoroi-extension/app/containers/transfer/UpgradeTxDialogContainer.js index 42d9573dbc..143d6d2cb9 100644 --- a/packages/yoroi-extension/app/containers/transfer/UpgradeTxDialogContainer.js +++ b/packages/yoroi-extension/app/containers/transfer/UpgradeTxDialogContainer.js @@ -7,10 +7,8 @@ import { HaskellShelleyTxSignRequest } from '../../api/ada/transactions/shelley/ import globalMessages from '../../i18n/global-messages'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import { addressToDisplayString, } from '../../api/ada/lib/storage/bridge/utils'; -import type { - TransferTx, -} from '../../types/TransferTypes'; -import { genAddressLookup, genAddressingLookup, allAddressSubgroups } from '../../stores/stateless/addressStores'; +import type { TransferTx } from '../../types/TransferTypes'; +import { genAddressLookup, allAddressSubgroups } from '../../stores/stateless/addressStores'; import TransferSummaryPage from '../../components/transfer/TransferSummaryPage'; import Dialog from '../../components/widgets/Dialog'; import LegacyTransferLayout from '../../components/transfer/LegacyTransferLayout'; @@ -45,23 +43,6 @@ export default class UpgradeTxDialogContainer extends Component<{| ...StoresProp intl: intlShape.isRequired, }; - submit: {| - signRequest: HaskellShelleyTxSignRequest, - publicKey: {| - key: RustModule.WalletV4.Bip32PublicKey, - ...Addressing, - |}, - publicDeriverId: number, - addressingMap: string => (void | $PropertyType), - expectedSerial: string | void, - networkId: number, - |} => Promise = async (request) => { - await this.props.stores.substores.ada.ledgerSend.sendUsingLedgerKey({ - ...request, - }); - this.props.onSubmit(); - } - render(): Node { const { transferRequest } = this.props.stores.substores.ada.yoroiTransfer; @@ -153,8 +134,6 @@ export default class UpgradeTxDialogContainer extends Component<{| ...StoresProp ); - const expectedSerial = selected.hardwareWalletDeviceId || ''; - return ( await this.submit({ - publicDeriverId: selected.publicDeriverId, - addressingMap: genAddressingLookup( - selected.networkId, - this.props.stores.addresses.addressSubgroupMap - ), - ...tentativeTx, - expectedSerial, - networkId: tentativeTx.signRequest.networkSettingSnapshot.NetworkId, - }), + trigger: async () => { + if (selected.type !== 'ledger') { + throw new Error('unexpected wallet type'); + } + await this.props.stores.transactionProcessingStore.adaSendAndRefresh({ + signRequest: tentativeTx.signRequest, + wallet: { + ...selected, + // when transfering ledger wallet Byron Utxos to Shelley, we should use the + // Byron public key + publicKey: tentativeTx.publicKey.key.to_hex(), + pathToPublic: tentativeTx.publicKey.addressing.path, + }, + password: null, + callback: async () => {}, + }); + this.props.onSubmit(); + }, label: intl.formatMessage(globalMessages.upgradeLabel), }} - isSubmitting={this.props.stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={this.props.stores.transactionProcessingStore.sendMoneyRequest.isExecuting} onCancel={{ trigger: this.props.onClose, label: intl.formatMessage(globalMessages.skipLabel), }} - error={this.props.stores.wallets.sendMoneyRequest.error} + error={this.props.stores.transactionProcessingStore.sendMoneyRequest.error} dialogTitle={intl.formatMessage(globalMessages.walletSendConfirmationDialogTitle)} getCurrentPrice={this.props.stores.coinPriceStore.getCurrentPrice} unitOfAccountSetting={this.props.stores.profile.unitOfAccount} diff --git a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.js b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.js index bb4a354c86..93ed525454 100644 --- a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.js +++ b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.js @@ -156,7 +156,7 @@ export default class YoroiTransferPage extends Component { label: intl.formatMessage(globalMessages.nextButtonLabel), trigger: this.transferFunds, }} - isSubmitting={stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={stores.transactionProcessingStore.sendMoneyRequest.isExecuting} onCancel={{ label: intl.formatMessage(globalMessages.cancel), trigger: this.cancelTransferFunds diff --git a/packages/yoroi-extension/app/containers/wallet/CreateWalletPageContainer.js b/packages/yoroi-extension/app/containers/wallet/CreateWalletPageContainer.js index 523e7e5a3f..8f88e0d188 100644 --- a/packages/yoroi-extension/app/containers/wallet/CreateWalletPageContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/CreateWalletPageContainer.js @@ -21,8 +21,8 @@ export default class CreateWalletPageContainer extends Component { const createWalletPageComponent = ( stores.substores.ada.wallets.createWallet(request)} + genWalletRecoveryPhrase={stores.substores.ada.mnemonicWalletCreationStore.genWalletRecoveryPhrase} + createWallet={request => stores.substores.ada.mnemonicWalletCreationStore.createWallet(request)} selectedNetwork={stores.profile.selectedNetwork} openDialog={dialog => this.props.stores.uiDialogs.open({ dialog })} closeDialog={this.props.stores.uiDialogs.closeActiveDialog} diff --git a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js index 0a4947f656..5af81433ad 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js @@ -4,25 +4,14 @@ import { Component } from 'react'; import { observer } from 'mobx-react'; import { action, observable, runInAction } from 'mobx'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; -import { defineMessages, intlShape } from 'react-intl'; +import { intlShape } from 'react-intl'; import { ROUTES } from '../../routes-config'; - import WalletSendFormRevamp from '../../components/wallet/send/WalletSendFormRevamp'; - -// Web Wallet Confirmation -import WalletSendConfirmationDialogContainer from './dialogs/WalletSendConfirmationDialogContainer'; -import WalletSendConfirmationDialog from '../../components/wallet/send/WalletSendConfirmationDialog'; import MemoNoExternalStorageDialog from '../../components/wallet/memos/MemoNoExternalStorageDialog'; -import { HaskellShelleyTxSignRequest } from '../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import { addressToDisplayString } from '../../api/ada/lib/storage/bridge/utils'; import type { TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import { genLookupOrFail } from '../../stores/stateless/tokenHelpers'; import BigNumber from 'bignumber.js'; import TransactionSuccessDialog from '../../components/wallet/send/TransactionSuccessDialog'; - -// Hardware Wallet Confirmation -import HWSendConfirmationDialog from '../../components/wallet/send/HWSendConfirmationDialog'; -import globalMessages from '../../i18n/global-messages'; import AddNFTDialog from '../../components/wallet/send/WalletSendFormSteps/AddNFTDialog'; import AddTokenDialog from '../../components/wallet/send/WalletSendFormSteps/AddTokenDialog'; import { ampli } from '../../../ampli/index'; @@ -42,25 +31,6 @@ import { ModalManager } from '../../UI/components/modals/ModalManager'; // $FlowIgnore: suppressing this error import { CurrencyProvider } from '../../UI/context/CurrencyContext'; -const messages = defineMessages({ - txConfirmationLedgerNanoLine1: { - id: 'wallet.send.ledger.confirmationDialog.info.line.1', - defaultMessage: '!!!After connecting your Ledger device to your computer’s USB port, press the Send using Ledger button.', - }, - sendUsingLedgerNano: { - id: 'wallet.send.ledger.confirmationDialog.submit', - defaultMessage: '!!!Send using Ledger', - }, - txConfirmationTrezorTLine1: { - id: 'wallet.send.trezor.confirmationDialog.info.line.1', - defaultMessage: '!!!After connecting your Trezor device to your computer, press the Send using Trezor button.', - }, - sendUsingTrezorT: { - id: 'wallet.send.trezor.confirmationDialog.submit', - defaultMessage: '!!!Send using Trezor', - }, -}); - @observer export default class WalletSendPage extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { @@ -151,7 +121,7 @@ export default class WalletSendPage extends Component { const { hasAnyPending } = stores.transactions; // disallow sending when pending tx exists - if ((uiDialogs.isOpen(HWSendConfirmationDialog) || uiDialogs.isOpen(WalletSendConfirmationDialog)) && hasAnyPending) { + if (hasAnyPending) { stores.uiDialogs.closeActiveDialog(); } @@ -222,12 +192,6 @@ export default class WalletSendPage extends Component { maxSendableAmount={transactionBuilderStore.maxSendableAmount} signRequest={transactionBuilderStore.tentativeTx} staleTx={transactionBuilderStore.txMismatch} - sendMoneyRequest={stores.wallets.sendMoneyRequest} - sendMoney={stores.substores.ada.mnemonicSend.sendMoney} - ledgerSendError={stores.substores.ada.ledgerSend.error || null} - trezorSendError={stores.substores.ada.trezorSend.error || null} - ledgerSend={stores.substores.ada.ledgerSend} - trezorSend={stores.substores.ada.trezorSend} /> {this.renderDialog()} @@ -239,12 +203,6 @@ export default class WalletSendPage extends Component { renderDialog: () => Node = () => { const { uiDialogs } = this.props.stores; - if (uiDialogs.isOpen(WalletSendConfirmationDialog)) { - return this.webWalletDoConfirmation(); - } - if (uiDialogs.isOpen(HWSendConfirmationDialog)) { - return this.hardwareWalletDoConfirmation(); - } if (uiDialogs.isOpen(MemoNoExternalStorageDialog)) { return this.noCloudWarningDialog(); } @@ -264,130 +222,6 @@ export default class WalletSendPage extends Component { return ''; }; - /** Web Wallet Send Confirmation dialog - * Callback that creates a container to avoid the component knowing about actions/stores */ - webWalletDoConfirmation: () => Node = () => { - const { stores } = this.props; - const { selected } = this.props.stores.wallets; - if (!selected) throw new Error(`Active wallet required for ${nameof(this.webWalletDoConfirmation)}.`); - - const { transactionBuilderStore } = this.props.stores; - if (!transactionBuilderStore.tentativeTx) { - throw new Error(`${nameof(this.webWalletDoConfirmation)}::should never happen`); - } - const signRequest = transactionBuilderStore.tentativeTx; - - return ( - - ); - }; - - /** Hardware Wallet (Trezor or Ledger) Confirmation dialog - * Callback that creates a component to avoid the component knowing about actions/stores - * separate container is not needed, this container acts as container for Confirmation dialog */ - hardwareWalletDoConfirmation: () => Node = () => { - const { selected } = this.props.stores.wallets; - if (!selected) throw new Error(`Active wallet required for ${nameof(this.webWalletDoConfirmation)}.`); - const { transactionBuilderStore } = this.props.stores; - - if (!transactionBuilderStore.tentativeTx) { - throw new Error(`${nameof(this.hardwareWalletDoConfirmation)}::should never happen`); - } - const signRequest = transactionBuilderStore.tentativeTx; - - const totalInput = signRequest.totalInput(); - const fee = signRequest.fee(); - const receivers = signRequest.receivers(false); - - let hwSendConfirmationDialog: Node = null; - - if (!(signRequest instanceof HaskellShelleyTxSignRequest)) { - throw new Error(`${nameof(this.hardwareWalletDoConfirmation)} hw wallets only supported for Byron`); - } - const selectedExplorerForNetwork = - this.props.stores.explorers.selectedExplorer.get(selected.networkId) ?? - (() => { - throw new Error('No explorer for wallet network'); - })(); - - if (selected.type === 'ledger') { - const messagesLedgerNano = { - infoLine1: messages.txConfirmationLedgerNanoLine1, - infoLine2: globalMessages.txConfirmationLedgerNanoLine2, - sendUsingHWButtonLabel: messages.sendUsingLedgerNano, - }; - const ledgerSendStore = this.props.stores.substores.ada.ledgerSend; - ledgerSendStore.init(); - hwSendConfirmationDialog = ( - - ledgerSendStore.sendUsingLedgerWallet({ - params: { signRequest }, - onSuccess: this.openTransactionSuccessDialog, - wallet: selected, - }) - } - onCancel={ledgerSendStore.cancel} - unitOfAccountSetting={this.props.stores.profile.unitOfAccount} - addressToDisplayString={addr => addressToDisplayString(addr, getNetworkById(selected.networkId))} - /> - ); - } else if (selected.type === 'trezor') { - const messagesTrezor = { - infoLine1: messages.txConfirmationTrezorTLine1, - infoLine2: globalMessages.txConfirmationTrezorTLine2, - sendUsingHWButtonLabel: messages.sendUsingTrezorT, - }; - const trezorSendStore = this.props.stores.substores.ada.trezorSend; - hwSendConfirmationDialog = ( - - trezorSendStore.sendUsingTrezor({ - params: { signRequest }, - onSuccess: this.openTransactionSuccessDialog, - wallet: selected, - }) - } - onCancel={trezorSendStore.cancel} - unitOfAccountSetting={this.props.stores.profile.unitOfAccount} - addressToDisplayString={addr => addressToDisplayString(addr, getNetworkById(selected.networkId))} - /> - ); - } else { - throw new Error('Unsupported hardware wallet found at hardwareWalletDoConfirmation.'); - } - - return hwSendConfirmationDialog; - }; - showMemoDialog: ({| continuation: void => void, dialog: any, diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletBackupDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletBackupDialogContainer.js index 05ce2d9178..2f94d6a32f 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletBackupDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletBackupDialogContainer.js @@ -58,7 +58,7 @@ export default class WalletBackupDialogContainer extends Component<{| ...Props, onAcceptTermRecovery={stores.walletBackup.acceptWalletBackupTermRecovery} onAddWord={stores.walletBackup.addWordToWalletBackupVerification} onClear={stores.walletBackup.clearEnteredRecoveryPhrase} - onFinishBackup={stores.substores.ada.wallets.finishWalletBackup} + onFinishBackup={stores.substores.ada.mnemonicWalletCreationStore.finishWalletBackup} removeWord={() => { stores.walletBackup.removeOneMnemonicWord(); }} diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletCreateDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletCreateDialogContainer.js index 44a3c532bd..0dd6547d63 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletCreateDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletCreateDialogContainer.js @@ -14,7 +14,7 @@ export default class WalletCreateDialogContainer extends Component<{| ...StoresP render(): Node { return ( this.props.stores.substores.ada.wallets.startWalletCreation(request)} + onSubmit={request => this.props.stores.substores.ada.mnemonicWalletCreationStore.startWalletCreation(request)} onCancel={this.props.onClose} /> ); diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletRestoreDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletRestoreDialogContainer.js index 16d3aecca7..7c8d2d8a54 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletRestoreDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletRestoreDialogContainer.js @@ -248,7 +248,7 @@ export default class WalletRestoreDialogContainer extends Component<{| ...Stores label: intl.formatMessage(globalMessages.nextButtonLabel), trigger: walletRestore.transferFromLegacy, }} - isSubmitting={this.props.stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={this.props.stores.transactionProcessingStore.sendMoneyRequest.isExecuting} onCancel={{ label: intl.formatMessage(globalMessages.cancel), trigger: this.onCancel, diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js deleted file mode 100644 index e70c5a5730..0000000000 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js +++ /dev/null @@ -1,93 +0,0 @@ -// @flow -import type { Node } from 'react'; -import { Component } from 'react'; -import { observer } from 'mobx-react'; -import WalletSendConfirmationDialog from '../../../components/wallet/send/WalletSendConfirmationDialog'; -import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; -import { addressToDisplayString } from '../../../api/ada/lib/storage/bridge/utils'; -import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; -import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; -import { getNetworkById } from '../../../api/ada/lib/storage/database/prepackaged/networks'; -import type { StoresProps } from '../../../stores'; - -// TODO: unmagic the constants -const MAX_VALUE_BYTES = 5000; -const MAX_TX_BYTES = 16384; - -type LocalProps = {| - +signRequest: ISignRequest, - +staleTx: boolean, - +unitOfAccountSetting: UnitOfAccountSettingType, - +openTransactionSuccessDialog: () => void, -|}; - -type Props = {| ...StoresProps, ...LocalProps |}; - -@observer -export default class WalletSendConfirmationDialogContainer extends Component { - - componentWillUnmount() { - this.props.stores.wallets.sendMoneyRequest.reset(); - } - - render(): Node { - const { - signRequest, - unitOfAccountSetting, - openTransactionSuccessDialog, - } = this.props; - const { stores } = this.props; - const publicDeriver = stores.wallets.selected; - - if (publicDeriver == null) - throw new Error(`Active wallet required for ${nameof(WalletSendConfirmationDialogContainer)}`); - - const { sendMoneyRequest } = stores.wallets; - - const totalInput = signRequest.totalInput(); - const fee = signRequest.fee(); - const size = signRequest.size?.(); - const fullSize = size ? size.full : 0; - const maxOutput = size ? Math.max(...size.outputs) : 0; - const showSize = size != null && ( - size.full > (MAX_TX_BYTES - 1000) - || maxOutput > (MAX_VALUE_BYTES - 1000) - ); - const receivers = signRequest.receivers(false); - return ( - { throw new Error('No explorer for wallet network'); })() - } - getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} - getCurrentPrice={stores.coinPriceStore.getCurrentPrice} - amount={totalInput.joinSubtractCopy(fee)} - receivers={receivers} - totalAmount={totalInput} - transactionFee={fee} - transactionSize={showSize ? `${fullSize}/${MAX_TX_BYTES} (Biggest output: ${maxOutput}/${MAX_VALUE_BYTES})` : null} - onSubmit={async ({ password }) => { - await stores.substores.ada.mnemonicSend.sendMoney({ - signRequest, - password, - wallet: publicDeriver, - onSuccess: openTransactionSuccessDialog, - }); - }} - isSubmitting={sendMoneyRequest.isExecuting} - onCancel={() => { - stores.uiDialogs.closeActiveDialog(); - sendMoneyRequest.reset(); - }} - error={sendMoneyRequest.error} - unitOfAccountSetting={unitOfAccountSetting} - addressToDisplayString={ - addr => addressToDisplayString(addr, getNetworkById(publicDeriver.networkId)) - } - /> - ); - } -} diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/voting/TransactionDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/voting/TransactionDialogContainer.js index ba0f9590af..6d509dd7fb 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/voting/TransactionDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/voting/TransactionDialogContainer.js @@ -43,7 +43,7 @@ export default class TransactionDialogContainer extends Component { progressInfo={votingStore.progressInfo} staleTx={votingStore.isStale} transactionFee={votingRegTx.fee()} - isSubmitting={stores.wallets.sendMoneyRequest.isExecuting} + isSubmitting={stores.transactionProcessingStore.sendMoneyRequest.isExecuting} getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} onCancel={cancel} goBack={goBack} diff --git a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js index 7f1a806789..126e79f8cc 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js @@ -62,7 +62,7 @@ export default class AdaDelegationTransactionStore extends Store { wallet: WalletState, poolRequest?: string, drepCredential?: string, - |}) => Promise = async request => { + |}) => Promise = async request => { const { timeToSlot } = this.stores.substores.ada.time.getTimeCalcRequests(request.wallet).requests; const absSlotNumber = new BigNumber( @@ -83,20 +83,18 @@ export default class AdaDelegationTransactionStore extends Store { absSlotNumber, protocolParameters, }).promise; - if (delegationTxPromise == null) { throw new Error(`${nameof(this.createTransaction)} should never happen`); } await delegationTxPromise; this.markStale(false); - return delegationTxPromise; }; @action createWithdrawalTxForWallet: ({| wallet: WalletState, - |}) => Promise = async request => { + |}) => Promise = async request => { this.createWithdrawalTx.reset(); const { timeToSlot } = this.stores.substores.ada.time.getTimeCalcRequests(request.wallet).requests; @@ -127,8 +125,6 @@ export default class AdaDelegationTransactionStore extends Store { }).promise; if (unsignedTx == null) throw new Error(`Should never happen`); - - return unsignedTx; }; @action @@ -137,52 +133,19 @@ export default class AdaDelegationTransactionStore extends Store { password?: string, dialog?: any, |}) => Promise = async request => { - const result = this.createDelegationTx.result; - if (result == null) { + const signRequest = this.createDelegationTx.result?.signTxRequest; + if (signRequest == null) { throw new Error(`${nameof(this.signTransaction)} no tx to broadcast`); } - const refreshWallet = () => { - this.stores.delegation.disablePoolTransitionState(request.wallet); - return this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId); - }; try { - if (request.wallet.type === 'ledger') { - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - ledger: { - signRequest: result.signTxRequest, - wallet: request.wallet, - }, - }, - refreshWallet, - }); - return; - } - if (request.wallet.type === 'trezor') { - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - trezor: { - signRequest: result.signTxRequest, - wallet: request.wallet, - }, - }, - refreshWallet, - }); - return; - } - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this.signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - wallet: request.wallet, - password: request.password, - signRequest: result.signTxRequest, - }, + await this.stores.transactionProcessingStore.adaSendAndRefresh({ + wallet: request.wallet, + signRequest, + password: request.password, + callback: () => { + this.stores.delegation.disablePoolTransitionState(request.wallet); + return this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId); }, - refreshWallet, }); } catch (error) { runInAction(() => { @@ -205,7 +168,7 @@ export default class AdaDelegationTransactionStore extends Store { @action.bound reset(request: {| justTransaction: boolean |}): void { - this.stores.wallets.sendMoneyRequest.reset(); + this.stores.transactionProcessingStore.sendMoneyRequest.reset(); this.createDelegationTx.reset(); if (!request.justTransaction) { this.isStale = false; diff --git a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js b/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js deleted file mode 100644 index b144aa8576..0000000000 --- a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js +++ /dev/null @@ -1,285 +0,0 @@ -// @flow -import { observable } from 'mobx'; - -import Store from '../base/Store'; -import Request from '../lib/LocalizedRequest'; -import type { GenerateWalletRecoveryPhraseFunc } from '../../api/ada/index'; -import { HaskellShelleyTxSignRequest } from '../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import type { StoresMap } from '../index'; -import { HARD_DERIVATION_START } from '../../config/numbersConfig'; -import { createWallet, signTransaction } from '../../api/thunk'; -import type { - Addressing, - QueriedUtxo, -} from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; -import { fail, first, sorted } from '../../coreUtils'; -import BigNumber from 'bignumber.js'; -import type{ WalletState } from '../../../chrome/extension/background/types'; - -const MAX_PICKED_COLLATERAL_UTXO_ADA = 10_000_000; // 10 ADA - -export default class AdaWalletsStore extends Store { - // REQUESTS - - @observable - generateWalletRecoveryPhraseRequest: Request = new Request( - this.api.ada.generateWalletRecoveryPhrase - ); - - // =================== SEND MONEY ==================== // - - adaSendAndRefresh: ({| - broadcastRequest: - | {| - normal: {| - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - ... - }, - signRequest: HaskellShelleyTxSignRequest, - password: string, - |}, - |} - | {| - trezor: {| - signRequest: HaskellShelleyTxSignRequest, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - }, - |}, - |} - | {| - ledger: {| - signRequest: HaskellShelleyTxSignRequest, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - stakingAddressing: Addressing, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - } - |}, - |}, - refreshWallet: () => Promise, - |}) => Promise = async request => { - let broadcastRequest; - let publicDeriverId; - let plateTextPart; - - if (request.broadcastRequest.ledger) { - const { wallet, signRequest } = request.broadcastRequest.ledger; - broadcastRequest = async () => { - return await this.stores.substores.ada.ledgerSend.signAndBroadcastFromWallet({ - signRequest, - wallet, - }); - }; - publicDeriverId = wallet.publicDeriverId; - plateTextPart = wallet.plate.TextPart; - } else if (request.broadcastRequest.trezor) { - const { wallet, signRequest } = request.broadcastRequest.trezor; - broadcastRequest = async () => { - return await this.stores.substores.ada.trezorSend.signAndBroadcastFromWallet({ - signRequest, - wallet, - }); - }; - publicDeriverId = request.broadcastRequest.trezor.wallet.publicDeriverId; - plateTextPart = request.broadcastRequest.trezor.wallet.plate.TextPart; - } else if (request.broadcastRequest.normal) { - const { wallet, signRequest, password } = request.broadcastRequest.normal; - broadcastRequest = async () => { - return await this.stores.substores.ada.mnemonicSend.signAndBroadcast({ - signRequest, - password, - publicDeriverId: wallet.publicDeriverId, - }); - }; - publicDeriverId = wallet.publicDeriverId; - plateTextPart = wallet.plate.TextPart; - } else { - throw new Error( - `${nameof(AdaWalletsStore)}::${nameof(this.adaSendAndRefresh)} unhandled wallet type` - ); - }; - await this.stores.wallets.sendAndRefresh({ - publicDeriverId, - broadcastRequest, - refreshWallet: request.refreshWallet, - plateTextPart, - }); - }; - - adaSignTransactionHexFromWallet: ({| - transactionHex: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - networkId: number, - hardwareWalletDeviceId: ?string, - type: 'trezor' | 'ledger' | 'mnemonic', - isHardware: boolean, - ... - }, - password: string, - |}) => Promise<{| signedTxHex: string |}> = async ({ wallet, transactionHex, password }) => { - const walletType: string = wallet.type; - const baseSignRequest = { wallet, transactionHex }; - const signRequest = wallet.isHardware - ? { [walletType]: baseSignRequest } - : { normal: { ...baseSignRequest, password } }; - // $FlowIgnore[incompatible-call] - return this.adaSignTransactionHex({ signRequest }); - } - - adaSignTransactionHex: ({| - signRequest: - | {| - normal: {| - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - ... - }, - transactionHex: string, - password: string, - |}, - |} - | {| - trezor: {| - transactionHex: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - }, - |}, - |} - | {| - ledger: {| - transactionHex: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - stakingAddressing: Addressing, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - } - |}, - |}, - |}) => Promise<{| signedTxHex: string |}> = async request => { - if (request.signRequest.ledger) { - const { wallet, transactionHex } = request.signRequest.ledger; - return this.stores.substores.ada.ledgerSend - .signRawTxFromWallet({ rawTxHex: transactionHex, wallet }); - } - if (request.signRequest.trezor) { - const { wallet, transactionHex } = request.signRequest.trezor; - return this.stores.substores.ada.trezorSend - .signRawTxFromWallet({ rawTxHex: transactionHex, wallet }); - } - if (request.signRequest.normal) { - const { wallet, transactionHex, password } = request.signRequest.normal; - const signedTxHex = await signTransaction({ - publicDeriverId: wallet.publicDeriverId, - transactionHex, - password, - }); - return { signedTxHex }; - } - throw new Error( - `${nameof(AdaWalletsStore)}::${nameof(this.adaSignTransactionHex)} unhandled wallet type` - ); - }; - - // =================== WALLET RESTORATION ==================== // - - startWalletCreation: ({| - name: string, - password: string, - |}) => Promise = async params => { - const recoveryPhrase = await this.generateWalletRecoveryPhraseRequest.execute({}).promise; - if (recoveryPhrase == null) { - throw new Error(`${nameof(this.startWalletCreation)} failed to generate recovery phrase`); - } - this.stores.walletBackup.initiateWalletBackup({ - recoveryPhrase, - name: params.name, - password: params.password, - }); - }; - - genWalletRecoveryPhrase: void => Promise> = async () => { - const recoveryPhrase = await this.generateWalletRecoveryPhraseRequest.execute({}).promise; - - if (recoveryPhrase == null) { - throw new Error(`${nameof(this.startWalletCreation)} failed to generate recovery phrase`); - } - - return recoveryPhrase; - }; - - /** Create the wallet and go to wallet summary screen */ - finishWalletBackup: void => Promise = async () => { - await this.createWallet({ - recoveryPhrase: this.stores.walletBackup.recoveryPhrase, - walletPassword: this.stores.walletBackup.password, - walletName: this.stores.walletBackup.name, - }); - }; - - createWallet: {| - recoveryPhrase: Array, - walletPassword: string, - walletName: string, - |} => Promise = async (request) => { - const { selectedNetwork } = this.stores.profile; - if (selectedNetwork == null) throw new Error(`${nameof(this.finishWalletBackup)} no network selected`); - await this.stores.wallets.createWalletRequest.execute(async () => { - const wallet = await createWallet({ - walletName: request.walletName, - walletPassword: request.walletPassword, - recoveryPhrase: request.recoveryPhrase.join(' '), - networkId: selectedNetwork.NetworkId, - accountIndex: 0 + HARD_DERIVATION_START, - }); - return wallet; - }).promise; - }; - - pickCollateralUtxo: ({| wallet: WalletState |}) => Promise = async ({ wallet }) => { - const allUtxos = wallet.utxos; - if (allUtxos.length === 0) { - fail('Cannot pick a collateral utxo! No utxo available at all in the wallet!'); - } - const utxoDefaultCoinAmount = (u: QueriedUtxo): BigNumber => - new BigNumber(u.output.tokens.find(x => x.Token.Identifier === '')?.TokenList.Amount ?? 0); - const compareDefaultCoins = (a: QueriedUtxo, b: QueriedUtxo): number => - utxoDefaultCoinAmount(a).comparedTo(utxoDefaultCoinAmount(b)); - const smallPureUtxos = allUtxos - .filter(u => u.output.tokens.length === 1 && utxoDefaultCoinAmount(u).lte(MAX_PICKED_COLLATERAL_UTXO_ADA)); - return first(sorted(smallPureUtxos, compareDefaultCoins)); - } -} diff --git a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js index 9e74ade593..d16669a8d8 100644 --- a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js @@ -92,7 +92,7 @@ export default class LedgerConnectStore this.error = undefined; this.hwDeviceInfo = undefined; this.stores.substores.ada.yoroiTransfer.transferRequest.reset(); - this.stores.wallets.sendMoneyRequest.reset(); + this.stores.transactionProcessingStore.sendMoneyRequest.reset(); }; @action _openTransferDialog: void => void = () => { diff --git a/packages/yoroi-extension/app/stores/ada/MnemonicWalletCreationStore.js b/packages/yoroi-extension/app/stores/ada/MnemonicWalletCreationStore.js new file mode 100644 index 0000000000..5b5057515f --- /dev/null +++ b/packages/yoroi-extension/app/stores/ada/MnemonicWalletCreationStore.js @@ -0,0 +1,71 @@ +// @flow +import { observable } from 'mobx'; + +import Store from '../base/Store'; +import Request from '../lib/LocalizedRequest'; +import type { GenerateWalletRecoveryPhraseFunc } from '../../api/ada/index'; +import type { StoresMap } from '../index'; +import { HARD_DERIVATION_START } from '../../config/numbersConfig'; +import { createWallet } from '../../api/thunk'; + +export default class MnemonicWalletCreationStore extends Store { + // REQUESTS + + @observable + generateWalletRecoveryPhraseRequest: Request = new Request( + this.api.ada.generateWalletRecoveryPhrase + ); + + startWalletCreation: ({| + name: string, + password: string, + |}) => Promise = async params => { + const recoveryPhrase = await this.generateWalletRecoveryPhraseRequest.execute({}).promise; + if (recoveryPhrase == null) { + throw new Error(`${nameof(this.startWalletCreation)} failed to generate recovery phrase`); + } + this.stores.walletBackup.initiateWalletBackup({ + recoveryPhrase, + name: params.name, + password: params.password, + }); + }; + + genWalletRecoveryPhrase: void => Promise> = async () => { + const recoveryPhrase = await this.generateWalletRecoveryPhraseRequest.execute({}).promise; + + if (recoveryPhrase == null) { + throw new Error(`${nameof(this.startWalletCreation)} failed to generate recovery phrase`); + } + + return recoveryPhrase; + }; + + /** Create the wallet and go to wallet summary screen */ + finishWalletBackup: void => Promise = async () => { + await this.createWallet({ + recoveryPhrase: this.stores.walletBackup.recoveryPhrase, + walletPassword: this.stores.walletBackup.password, + walletName: this.stores.walletBackup.name, + }); + }; + + createWallet: {| + recoveryPhrase: Array, + walletPassword: string, + walletName: string, + |} => Promise = async (request) => { + const { selectedNetwork } = this.stores.profile; + if (selectedNetwork == null) throw new Error(`${nameof(this.finishWalletBackup)} no network selected`); + await this.stores.wallets.createWalletRequest.execute(async () => { + const wallet = await createWallet({ + walletName: request.walletName, + walletPassword: request.walletPassword, + recoveryPhrase: request.recoveryPhrase.join(' '), + networkId: selectedNetwork.NetworkId, + accountIndex: 0 + HARD_DERIVATION_START, + }); + return wallet; + }).promise; + }; +} diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js index 6c77dbf1ef..4e81f6e041 100644 --- a/packages/yoroi-extension/app/stores/ada/SwapStore.js +++ b/packages/yoroi-extension/app/stores/ada/SwapStore.js @@ -82,8 +82,7 @@ export default class SwapStore extends Store { getCollateralUtxoHexForCancel: ({| wallet: WalletState |}) => Promise = async ({ wallet, }) => { - const utxo: ?QueriedUtxo = await this.stores.substores.ada.wallets - .pickCollateralUtxo({ wallet }); + const utxo: ?QueriedUtxo = await this.api.ada.pickCollateralUtxo({ wallet }); return maybe(utxo, u => { const [addressedUtxo] = asAddressedUtxo([u]); return cardanoUtxoHexFromRemoteFormat(cast(addressedUtxo)); diff --git a/packages/yoroi-extension/app/stores/ada/VotingStore.js b/packages/yoroi-extension/app/stores/ada/VotingStore.js index 798bb81a6e..45978dafe3 100644 --- a/packages/yoroi-extension/app/stores/ada/VotingStore.js +++ b/packages/yoroi-extension/app/stores/ada/VotingStore.js @@ -304,48 +304,15 @@ export default class VotingStore extends Store { password?: string, wallet: WalletState, |}) => Promise = async request => { - const result = this.createVotingRegTx.result; - if (result == null) { + const signRequest = this.createVotingRegTx.result; + if (signRequest == null) { throw new Error(`${nameof(this.signTransaction)} no tx to broadcast`); } - if (request.wallet.type === 'ledger') { - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - ledger: { - signRequest: result, - wallet: request.wallet, - }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), - }); - return; - } - if (request.wallet.type === 'trezor') { - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - trezor: { - signRequest: result, - wallet: request.wallet, - }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), - }); - return; - } - - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this.signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - wallet: request.wallet, - password: request.password, - signRequest: result, - }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), + await this.stores.transactionProcessingStore.adaSendAndRefresh({ + wallet: request.wallet, + signRequest, + password: request.password, + callback: () => this.stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), }); }; @@ -379,7 +346,7 @@ export default class VotingStore extends Store { stepState: StepState.LOAD, }; this.error = null; - this.stores.wallets.sendMoneyRequest.reset(); + this.stores.transactionProcessingStore.sendMoneyRequest.reset(); this.createVotingRegTx.reset(); if (!request.justTransaction) { this.isStale = false; diff --git a/packages/yoroi-extension/app/stores/ada/index.js b/packages/yoroi-extension/app/stores/ada/index.js index 7a13797446..06f58ea108 100644 --- a/packages/yoroi-extension/app/stores/ada/index.js +++ b/packages/yoroi-extension/app/stores/ada/index.js @@ -3,20 +3,17 @@ // File is based the same pattern used for the non-ada-specific stores in our app. import { observable, action } from 'mobx'; -import AdaWalletsStore from './AdaWalletsStore'; +import MnemonicWalletCreationStore from './MnemonicWalletCreationStore'; import AdaTransactionsStore from './AdaTransactionsStore'; import AddressesStore from './AdaAddressesStore'; import AdaYoroiTransferStore from './AdaYoroiTransferStore'; import TrezorConnectStore from './TrezorConnectStore'; -import TrezorSendStore from './send/TrezorSendStore'; import LedgerConnectStore from './LedgerConnectStore'; -import LedgerSendStore from './send/LedgerSendStore'; import HWVerifyAddressStore from './HWVerifyAddressStore'; import AdaStateFetchStore from './AdaStateFetchStore'; import AdaWalletRestoreStore from './AdaWalletRestoreStore'; import AdaDelegationTransactionStore from './AdaDelegationTransactionStore'; import AdaDelegationStore from './AdaDelegationStore'; -import AdaMnemonicSendStore from './send/AdaMnemonicSendStore'; import VotingStore from './VotingStore'; import SwapStore from './SwapStore'; import type { Api } from '../../api/index'; @@ -24,61 +21,52 @@ import type { StoresMap } from '../index'; import BaseCardanoTimeStore from '../base/BaseCardanoTimeStore'; export const adaStoreClasses = Object.freeze({ - wallets: AdaWalletsStore, + mnemonicWalletCreationStore: MnemonicWalletCreationStore, transactions: AdaTransactionsStore, addresses: AddressesStore, yoroiTransfer: AdaYoroiTransferStore, trezorConnect: TrezorConnectStore, - trezorSend: TrezorSendStore, ledgerConnect: LedgerConnectStore, - ledgerSend: LedgerSendStore, hwVerifyAddress: HWVerifyAddressStore, stateFetchStore: AdaStateFetchStore, delegationTransaction: AdaDelegationTransactionStore, walletRestore: AdaWalletRestoreStore, delegation: AdaDelegationStore, time: BaseCardanoTimeStore, - mnemonicSend: AdaMnemonicSendStore, votingStore: VotingStore, swapStore: SwapStore, }); export type AdaStoresMap = {| - wallets: AdaWalletsStore, + mnemonicWalletCreationStore: MnemonicWalletCreationStore, transactions: AdaTransactionsStore, addresses: AddressesStore, yoroiTransfer: AdaYoroiTransferStore, trezorConnect: TrezorConnectStore, - trezorSend: TrezorSendStore, ledgerConnect: LedgerConnectStore, - ledgerSend: LedgerSendStore, hwVerifyAddress: HWVerifyAddressStore, stateFetchStore: AdaStateFetchStore, delegationTransaction: AdaDelegationTransactionStore, walletRestore: AdaWalletRestoreStore, delegation: AdaDelegationStore, time: BaseCardanoTimeStore, - mnemonicSend: AdaMnemonicSendStore, votingStore: VotingStore, swapStore: SwapStore, |}; const adaStores: WithNullableFields = observable({ - wallets: null, + mnemonicWalletCreationStore: null, transactions: null, addresses: null, yoroiTransfer: null, trezorConnect: null, - trezorSend: null, ledgerConnect: null, - ledgerSend: null, hwVerifyAddress: null, stateFetchStore: null, delegationTransaction: null, walletRestore: null, delegation: null, time: null, - mnemonicSend: null, votingStore: null, swapStore: null, }); diff --git a/packages/yoroi-extension/app/stores/ada/send/AdaMnemonicSendStore.js b/packages/yoroi-extension/app/stores/ada/send/AdaMnemonicSendStore.js deleted file mode 100644 index 47bc1cd043..0000000000 --- a/packages/yoroi-extension/app/stores/ada/send/AdaMnemonicSendStore.js +++ /dev/null @@ -1,65 +0,0 @@ -// @flow - -import Store from '../../base/Store'; -import { HaskellShelleyTxSignRequest } from '../../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import { - fullErrStr, - Logger, -} from '../../../utils/logging'; -import { ROUTES } from '../../../routes-config'; -import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; -import type { StoresMap } from '../../index'; -import { signAndBroadcastTransaction } from '../../../api/thunk'; - -export default class AdaMnemonicSendStore extends Store { - - /** Send money and then return to transaction screen */ - sendMoney: {| - signRequest: ISignRequest, - password: string, - +wallet: { - publicDeriverId: number, - +plate: { TextPart: string, ... }, - ... - }, - onSuccess?: void => void, - |} => Promise = async (request) => { - if (!(request.signRequest instanceof HaskellShelleyTxSignRequest)) { - throw new Error(`${nameof(this.sendMoney)} wrong tx sign request`); - } - - const { stores } = this; - await stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - wallet: request.wallet, - password: request.password, - signRequest: request.signRequest, - }, - }, - refreshWallet: () => stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), - }); - - this.stores.uiDialogs.closeActiveDialog(); - stores.wallets.sendMoneyRequest.reset(); - if (request.onSuccess) { - request.onSuccess(); - } else { - stores.app.goToRoute({ route: ROUTES.WALLETS.TRANSACTIONS }); - } - }; - - signAndBroadcast: {| - signRequest: HaskellShelleyTxSignRequest, - password: string, - publicDeriverId: number, - |} => Promise<{| txId: string |}> = async (request) => { - try { - const { txId } = await signAndBroadcastTransaction(request); - return { txId }; - } catch (error) { - Logger.error(`${nameof(AdaMnemonicSendStore)}::${nameof(this.signAndBroadcast)} error: ${fullErrStr(error)}` ); - throw error; - } - } -} diff --git a/packages/yoroi-extension/app/stores/ada/send/LedgerSendStore.js b/packages/yoroi-extension/app/stores/ada/send/LedgerSendStore.js deleted file mode 100644 index b1322d6f56..0000000000 --- a/packages/yoroi-extension/app/stores/ada/send/LedgerSendStore.js +++ /dev/null @@ -1,448 +0,0 @@ -// @flow -import { action, observable } from 'mobx'; - -import type { SignTransactionResponse as LedgerSignTxResponse } from '@cardano-foundation/ledgerjs-hw-app-cardano'; -import { TxAuxiliaryDataSupplementType } from '@cardano-foundation/ledgerjs-hw-app-cardano'; - -import Store from '../../base/Store'; - -import LocalizableError from '../../../i18n/LocalizableError'; - -import { convertToLocalizableError } from '../../../domain/LedgerLocalizedError'; - -import { Logger, stringifyData, stringifyError, } from '../../../utils/logging'; - -import { - buildConnectorSignedTransaction, - buildSignedTransaction, -} from '../../../api/ada/transactions/shelley/ledgerTx'; - -import { LedgerConnect } from '../../../utils/hwConnectHandler'; -import { ROUTES } from '../../../routes-config'; -import { RustModule } from '../../../api/ada/lib/cardanoCrypto/rustLoader'; -import { HaskellShelleyTxSignRequest } from '../../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import type { Addressing, } from '../../../api/ada/lib/storage/models/PublicDeriver/interfaces'; -import { genAddressingLookup } from '../../stateless/addressStores'; -import type { StoresMap } from '../../index'; -import { - generateCip15RegistrationMetadata, - generateRegistrationMetadata, -} from '../../../api/ada/lib/cardanoCrypto/catalyst'; -import { getNetworkById } from '../../../api/ada/lib/storage/database/prepackaged/networks.js'; -import { broadcastTransaction } from '../../../api/thunk'; -import { transactionHexToBodyHex, transactionHexToHash } from '../../../api/ada/lib/cardanoCrypto/utils'; -import { fail } from '../../../coreUtils'; -import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; - -export type SendUsingLedgerParams = {| - signRequest: ISignRequest, -|}; - -/** Note: Handles Ledger Signing */ -export default class LedgerSendStore extends Store { - // =================== VIEW RELATED =================== // - // TODO: consider getting rid of both of these - @observable isActionProcessing: boolean = false; - @observable error: ?LocalizableError; - // =================== VIEW RELATED =================== // - - /** setup() is called when stores are being created - * _init() is called when Confirmation dialog is about to show */ - init: void => void = () => { - Logger.debug(`${nameof(LedgerSendStore)}::${nameof(this.init)} called`); - } - - _reset(): void { - this._setActionProcessing(false); - this._setError(null); - } - - _preSendValidation: void => void = () => { - if (this.isActionProcessing) { - // this Error will be converted to LocalizableError() - throw new Error('Can\'t send another transaction if one transaction is in progress.'); - } - } - - sendUsingLedgerKey: {| - signRequest: HaskellShelleyTxSignRequest, - publicKey: {| - key: RustModule.WalletV4.Bip32PublicKey, - ...Addressing, - |}, - publicDeriverId: number, - addressingMap: string => (void | $PropertyType), - expectedSerial: string | void, - networkId: number, - |} => Promise = async (request) => { - await this.stores.wallets.sendAndRefresh({ - publicDeriverId: undefined, - plateTextPart: undefined, - broadcastRequest: async () => await this.signAndBroadcast(request), - refreshWallet: async () => {} - }) - } - - sendUsingLedgerWallet: {| - params: SendUsingLedgerParams, - onSuccess?: void => void, - onFail ?: void => void, - +wallet: { - publicDeriverId: number, - stakingAddressing: Addressing, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - +plate: { TextPart: string, ... }, - ... - }, - |} => Promise = async (request) => { - try { - if (this.isActionProcessing) { - // this Error will be converted to LocalizableError() - throw new Error('Can’t send another transaction if one transaction is in progress.'); - } - if (!(request.params.signRequest instanceof HaskellShelleyTxSignRequest)) { - throw new Error(`${nameof(this.sendUsingLedgerWallet)} wrong tx sign request`); - } - const { signRequest } = request.params; - - this._setError(null); - this._setActionProcessing(true); - - const { stores } = this; - await stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - ledger: { - signRequest, - wallet: request.wallet, - }, - }, - refreshWallet: () => stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), - }); - - this.stores.uiDialogs.closeActiveDialog(); - stores.wallets.sendMoneyRequest.reset(); - if (request.onSuccess) { - request.onSuccess(); - } else { - stores.app.goToRoute({ route: ROUTES.WALLETS.TRANSACTIONS }); - } - - Logger.info('SUCCESS: ADA sent using Ledger SignTx'); - } catch (e) { - this._setError(e); - if (request.onFail) { - request.onFail(); - } - } finally { - this._setActionProcessing(false); - } - } - - /** Generates a payload with Ledger format and tries Send ADA using Ledger signing */ - signAndBroadcastFromWallet: {| - signRequest: HaskellShelleyTxSignRequest, - +wallet: { - publicDeriverId: number, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - }, - |} => Promise<{| txId: string |}> = async (request) => { - try { - Logger.debug(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcastFromWallet)} called: ` + stringifyData(request)); - - const publicKeyInfo = { - key: RustModule.WalletV4.Bip32PublicKey.from_hex(request.wallet.publicKey), - addressing: { - startLevel: 1, - path: request.wallet.pathToPublic, - }, - }; - - const expectedSerial = request.wallet.hardwareWalletDeviceId || ''; - - const signRequest = request.signRequest; - - const addressingMap = genAddressingLookup( - request.wallet.networkId, - this.stores.addresses.addressSubgroupMap - ); - - return this.signAndBroadcast({ - signRequest, - publicKey: publicKeyInfo, - publicDeriverId: request.wallet.publicDeriverId, - addressingMap, - expectedSerial, - networkId: request.wallet.networkId, - }); - - } catch (error) { - Logger.error(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcastFromWallet)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - }; - - signAndBroadcast: {| - signRequest: HaskellShelleyTxSignRequest, - publicKey: {| - key: RustModule.WalletV4.Bip32PublicKey, - ...Addressing, - |}, - addressingMap: string => (void | $PropertyType), - publicDeriverId: number, - networkId: number, - expectedSerial: string | void, - |} => Promise<{| txId: string |}> = async (request) => { - let ledgerConnect: ?LedgerConnect; - try { - Logger.debug(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcast)} called: ` + stringifyData(request)); - - ledgerConnect = new LedgerConnect({ - locale: this.stores.profile.currentLocale, - }); - - let cip36: boolean = false; - if (request.signRequest.ledgerNanoCatalystRegistrationTxSignData) { - const getVersionResponse = await ledgerConnect.getVersion({ - serial: request.expectedSerial, - dontCloseTab: true, - }); - cip36 = getVersionResponse.compatibility.supportsCIP36Vote === true; - } - - const network = getNetworkById(request.networkId); - - const { ledgerSignTxPayload } = this.api.ada.createLedgerSignTxData({ - signRequest: request.signRequest, - network, - addressingMap: request.addressingMap, - cip36, - }); - - const ledgerSignTxResp: LedgerSignTxResponse = - await ledgerConnect.signTransaction({ - serial: request.expectedSerial, - params: ledgerSignTxPayload, - useOpenTab: true, - }); - - // There is no need of ledgerConnect after this line. - // UI was getting blocked for few seconds - // because _prepareAndBroadcastSignedTx takes time. - // Disposing here will fix the UI issue. - ledgerConnect.dispose(); - - let metadata; - - if (request.signRequest.ledgerNanoCatalystRegistrationTxSignData) { - const { - votingPublicKey, - stakingKey, - paymentAddress, - nonce, - } = request.signRequest.ledgerNanoCatalystRegistrationTxSignData; - - if ( - !ledgerSignTxResp.auxiliaryDataSupplement || - (ledgerSignTxResp.auxiliaryDataSupplement.type !== - TxAuxiliaryDataSupplementType.CIP36_REGISTRATION) - ) { - throw new Error(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcast)} unexpected Ledger sign transaction response`); - } - const { cip36VoteRegistrationSignatureHex } = - ledgerSignTxResp.auxiliaryDataSupplement; - - if (cip36) { - metadata = generateRegistrationMetadata( - votingPublicKey, - stakingKey, - paymentAddress, - nonce, - (_hashedMetadata) => { - return cip36VoteRegistrationSignatureHex; - }, - ); - } else { - metadata = generateCip15RegistrationMetadata( - votingPublicKey, - stakingKey, - paymentAddress, - nonce, - (_hashedMetadata) => { - return cip36VoteRegistrationSignatureHex; - }, - ); - } - // We can verify that - // Buffer.from( - // blake2b(256 / 8).update(metadata.to_bytes()).digest('binary') - // ).toString('hex') === - // ledgerSignTxResp.auxiliaryDataSupplement.auxiliaryDataHashaHex - } else { - metadata = request.signRequest.metadata; - } - - if (metadata) { - request.signRequest.self().set_auxiliary_data(metadata); - } - - const tx = request.signRequest.self().build_tx(); - const txId = transactionHexToHash(tx.to_hex()); - - const signedTx = buildSignedTransaction( - tx, - request.signRequest.senderUtxos, - ledgerSignTxResp.witnesses, - request.publicKey, - metadata, - ); - - await broadcastTransaction({ - publicDeriverId: request.publicDeriverId, - signedTxHex: signedTx.to_hex(), - networkId: request.networkId, - }); - - return { txId }; - } catch (error) { - Logger.error(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcast)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } finally { - if (ledgerConnect != null) { - ledgerConnect.dispose(); - } - } - }; - - - signRawTxFromWallet: {| - rawTxHex: string, - +wallet: { - publicDeriverId: number, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - ... - }, - |} => Promise<{| signedTxHex: string |}> = async (request) => { - try { - Logger.debug(`${nameof(LedgerSendStore)}::${nameof(this.signRawTxFromWallet)} called: ` + stringifyData(request)); - - const publicKeyInfo = { - key: RustModule.WalletV4.Bip32PublicKey.from_hex(request.wallet.publicKey), - addressing: { - startLevel: 1, - path: request.wallet.pathToPublic, - }, - }; - - const expectedSerial = request.wallet.hardwareWalletDeviceId || ''; - - const addressingMap = genAddressingLookup( - request.wallet.networkId, - this.stores.addresses.addressSubgroupMap, - ); - - return this.signRawTx({ - rawTxHex: request.rawTxHex, - publicKey: publicKeyInfo, - addressingMap, - expectedSerial, - networkId: request.wallet.networkId, - }); - - } catch (error) { - Logger.error(`${nameof(LedgerSendStore)}::${nameof(this.signRawTxFromWallet)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - } - - signRawTx: {| - rawTxHex: string, - publicKey: {| - key: RustModule.WalletV4.Bip32PublicKey, - ...Addressing, - |}, - addressingMap: string => (void | $PropertyType), - networkId: number, - expectedSerial: string | void, - |} => Promise<{| signedTxHex: string |}> = async (request) => { - - let ledgerConnect: ?LedgerConnect; - try { - Logger.debug(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcast)} called: ` + stringifyData(request)); - - ledgerConnect = new LedgerConnect({ - locale: this.stores.profile.currentLocale, - }); - - const { rawTxHex } = request; - - const network = getNetworkById(request.networkId); - - const txBodyHex = transactionHexToBodyHex(rawTxHex); - - const addressedUtxos = await this.stores.wallets.getAddressedUtxos(); - - const response = this.api.ada.createHwSignTxDataFromRawTx('ledger', { - txBodyHex, - network, - addressingMap: request.addressingMap, - senderUtxos: addressedUtxos, - }); - - const ledgerSignTxPayload = response.hw === 'ledger' ? response.result.ledgerSignTxPayload - : fail('Unecpected response type from `createHwSignTxDataFromRawTx` for ledger: ' + JSON.stringify(response)); - - const ledgerSignTxResp: LedgerSignTxResponse = - await ledgerConnect.signTransaction({ - serial: request.expectedSerial, - params: ledgerSignTxPayload, - useOpenTab: true, - }); - - // There is no need of ledgerConnect after this line. - // UI was getting blocked for few seconds - // because _prepareAndBroadcastSignedTx takes time. - // Disposing here will fix the UI issue. - ledgerConnect.dispose(); - - const signedTxHex = buildConnectorSignedTransaction( - rawTxHex, - ledgerSignTxResp.witnesses, - request.publicKey, - ); - - return { signedTxHex }; - } catch (error) { - Logger.error(`${nameof(LedgerSendStore)}::${nameof(this.signAndBroadcast)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } finally { - if (ledgerConnect != null) { - ledgerConnect.dispose(); - } - } - }; - - cancel: void => void = () => { - if (!this.isActionProcessing) { - this.stores.uiDialogs.closeActiveDialog(); - this._reset(); - } - } - - @action _setActionProcessing: boolean => void = (processing) => { - this.isActionProcessing = processing; - } - - @action _setError: ?LocalizableError => void = (error) => { - this.error = error; - } -} diff --git a/packages/yoroi-extension/app/stores/ada/send/TrezorSendStore.js b/packages/yoroi-extension/app/stores/ada/send/TrezorSendStore.js deleted file mode 100644 index fd0faae260..0000000000 --- a/packages/yoroi-extension/app/stores/ada/send/TrezorSendStore.js +++ /dev/null @@ -1,350 +0,0 @@ -// @flow -import { action, observable } from 'mobx'; - -import Store from '../../base/Store'; - -import { wrapWithFrame } from '../../lib/TrezorWrapper'; -import { Logger, stringifyData, stringifyError, } from '../../../utils/logging'; -import { convertToLocalizableError } from '../../../domain/TrezorLocalizedError'; -import LocalizableError from '../../../i18n/LocalizableError'; -import { ROUTES } from '../../../routes-config'; -import { HaskellShelleyTxSignRequest } from '../../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import type { StoresMap } from '../../index'; -import { - buildConnectorSignedTransaction, - buildSignedTransaction -} from '../../../api/ada/transactions/shelley/trezorTx'; -import { RustModule } from '../../../api/ada/lib/cardanoCrypto/rustLoader'; -import { generateRegistrationMetadata } from '../../../api/ada/lib/cardanoCrypto/catalyst'; -import { derivePublicByAddressing } from '../../../api/ada/lib/cardanoCrypto/deriveByAddressing'; -import type { Addressing } from '../../../api/ada/lib/storage/models/PublicDeriver/interfaces'; -import { getNetworkById } from '../../../api/ada/lib/storage/database/prepackaged/networks.js'; -import { broadcastTransaction } from '../../../api/thunk'; -import { transactionHexToBodyHex } from '../../../api/ada/lib/cardanoCrypto/utils'; -import { fail } from '../../../coreUtils'; -import { genAddressingLookup } from '../../stateless/addressStores'; -import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; - -export type SendUsingTrezorParams = {| - signRequest: ISignRequest, -|}; - -/** Note: Handles Trezor Signing */ -export default class TrezorSendStore extends Store { - // =================== VIEW RELATED =================== // - // TODO: consider getting rid of both of these - @observable isActionProcessing: boolean = false; - @observable error: ?LocalizableError; - // =================== VIEW RELATED =================== // - - // =================== API RELATED =================== // - - reset: void => void = () => { - this._setActionProcessing(false); - this._setError(null); - } - - sendUsingTrezor: {| - params: SendUsingTrezorParams, - onSuccess?: void => void, - onFail ?: void => void, - +wallet: { - publicDeriverId: number, - stakingAddressing: Addressing, - publicKey: string, - pathToPublic: Array, - networkId: number, - hardwareWalletDeviceId: ?string, - +plate: { TextPart: string, ... }, - ... - }, - |} => Promise = async (request) => { - try { - if (this.isActionProcessing) { - // this Error will be converted to LocalizableError() - throw new Error('Can’t send another transaction if one transaction is in progress.'); - } - if (!(request.params.signRequest instanceof HaskellShelleyTxSignRequest)) { - throw new Error(`${nameof(this.sendUsingTrezor)} wrong tx sign request`); - } - const { signRequest } = request.params; - - this._setError(null); - this._setActionProcessing(true); - - const { stores } = this; - await stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - trezor: { - signRequest, - wallet: request.wallet, - }, - }, - refreshWallet: () => stores.wallets.refreshWalletFromRemote(request.wallet.publicDeriverId), - }); - - this.stores.uiDialogs.closeActiveDialog(); - stores.wallets.sendMoneyRequest.reset(); - if (request.onSuccess) { - request.onSuccess(); - } else { - stores.app.goToRoute({ route: ROUTES.WALLETS.TRANSACTIONS }); - } - this.reset(); - - Logger.info('SUCCESS: ADA sent using Trezor SignTx'); - } catch (e) { - this._setError(e); - if (request.onFail) { - request.onFail(); - } - } finally { - this._setActionProcessing(false); - } - } - - signAndBroadcastFromWallet: {| - signRequest: HaskellShelleyTxSignRequest, - +wallet: { - publicDeriverId: number, - networkId: number, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - ... - }, - |} => Promise<{| txId: string |}> = async (request) => { - try { - Logger.debug(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcastFromWallet)} called: ` + stringifyData(request)); - - const { signRequest, wallet } = request; - - return this.signAndBroadcast({ - signRequest, - wallet, - }); - - } catch (error) { - Logger.error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcastFromWallet)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - } - - signAndBroadcast: {| - signRequest: HaskellShelleyTxSignRequest, - +wallet: { - publicDeriverId: number, - networkId: number, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - ... - }, - |} => Promise<{| txId: string |}> = async (request) => { - const { signRequest } = request; - try { - const network = getNetworkById(request.wallet.networkId); - const trezorSignTxDataResp = this.api.ada.createTrezorSignTxData({ - signRequest, - network, - }); - - const trezorSignTxResp = await wrapWithFrame(trezor => { - return trezor.cardanoSignTransaction( - JSON.parse(JSON.stringify({ ...trezorSignTxDataResp.trezorSignTxPayload })) - ); - }); - - if (trezorSignTxResp && trezorSignTxResp.payload && trezorSignTxResp.payload.error != null) { - // this Error will be converted to LocalizableError() - // noinspection ExceptionCaughtLocallyJS - throw new Error(trezorSignTxResp.payload.error); - } - if (!trezorSignTxResp.success) { - // noinspection ExceptionCaughtLocallyJS - throw new Error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcast)} should never happen`); - } - - const publicKeyInfo = { - key: RustModule.WalletV4.Bip32PublicKey.from_hex(request.wallet.publicKey), - addressing: { - startLevel: 1, - path: request.wallet.pathToPublic, - }, - }; - - const stakingKey = derivePublicByAddressing({ - addressing: request.wallet.stakingAddressing.addressing, - startingFrom: { - level: publicKeyInfo.addressing.startLevel + publicKeyInfo.addressing.path.length - 1, - key: publicKeyInfo.key, - } - }); - - let metadata; - - if (signRequest.trezorTCatalystRegistrationTxSignData) { - const { - votingPublicKey, - stakingKey: stakingKeyHex, - paymentAddress, - nonce, - } = signRequest.trezorTCatalystRegistrationTxSignData; - - const auxDataSupplement = trezorSignTxResp.payload.auxiliaryDataSupplement; - if ( - !auxDataSupplement - || auxDataSupplement.type !== 1 - || auxDataSupplement.governanceSignature == null - ) { - // noinspection ExceptionCaughtLocallyJS - throw new Error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcast)} unexpected Trezor sign transaction response`); - } - const catalystSignature = auxDataSupplement.governanceSignature; - - metadata = generateRegistrationMetadata( - votingPublicKey, - stakingKeyHex, - paymentAddress, - nonce, - (_hashedMetadata) => { - return catalystSignature; - }, - ); - // We can verify that - // Buffer.from( - // blake2b(256 / 8).update(metadata.to_bytes()).digest('binary') - // ).toString('hex') === - // trezorSignTxResp.payload.auxiliaryDataSupplement.auxiliaryDataHash - } else { - metadata = signRequest.metadata; - } - - if (metadata) { - signRequest.self().set_auxiliary_data(metadata); - } - - const tx = signRequest.self().build_tx(); - - const signedTx = buildSignedTransaction( - tx, - signRequest.senderUtxos, - trezorSignTxResp.payload.witnesses, - publicKeyInfo, - stakingKey, - metadata, - ); - - const txId = RustModule.WalletV4.FixedTransaction.from_hex(tx.to_hex()).transaction_hash().to_hex(); - - await broadcastTransaction({ - publicDeriverId: request.wallet.publicDeriverId, - signedTxHex: signedTx.to_hex(), - }); - return { txId }; - } catch (error) { - Logger.error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcast)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - } - - - signRawTxFromWallet: {| - rawTxHex: string, - +wallet: { - publicDeriverId: number, - networkId: number, - publicKey: string, - pathToPublic: Array, - stakingAddressing: Addressing, - ... - }, - |} => Promise<{| signedTxHex: string |}> = async (request) => { - try { - Logger.debug(`${nameof(TrezorSendStore)}::${nameof(this.signRawTxFromWallet)} called: ` + stringifyData(request)); - - const { rawTxHex, wallet } = request; - - const addressingMap = genAddressingLookup( - request.wallet.networkId, - this.stores.addresses.addressSubgroupMap, - ); - - return this.signRawTx({ - rawTxHex, - addressingMap, - networkId: wallet.networkId, - }); - - } catch (error) { - Logger.error(`${nameof(TrezorSendStore)}::${nameof(this.signRawTxFromWallet)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - } - - signRawTx: {| - rawTxHex: string, - addressingMap: string => (void | $PropertyType), - networkId: number, - |} => Promise<{| signedTxHex: string |}> = async (request) => { - const { rawTxHex } = request; - try { - const network = getNetworkById(request.networkId); - - const txBodyHex = transactionHexToBodyHex(rawTxHex); - - const addressedUtxos = await this.stores.wallets.getAddressedUtxos(); - - const response = this.api.ada.createHwSignTxDataFromRawTx('trezor', { - txBodyHex, - network, - addressingMap: request.addressingMap, - senderUtxos: addressedUtxos, - }); - - const trezorSignTxPayload = response.hw === 'trezor' ? response.result.trezorSignTxPayload - : fail('Unecpected response type from `createHwSignTxDataFromRawTx` for trezor: ' + JSON.stringify(response)); - - const trezorSignTxResp = await wrapWithFrame(trezor => { - return trezor.cardanoSignTransaction( - JSON.parse(JSON.stringify({ ...trezorSignTxPayload })) - ); - }); - - if (trezorSignTxResp && trezorSignTxResp.payload && trezorSignTxResp.payload.error != null) { - // this Error will be converted to LocalizableError() - // noinspection ExceptionCaughtLocallyJS - throw new Error(trezorSignTxResp.payload.error); - } - if (!trezorSignTxResp.success) { - // noinspection ExceptionCaughtLocallyJS - throw new Error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcast)} should never happen`); - } - - const signedTxHex = buildConnectorSignedTransaction( - rawTxHex, - trezorSignTxResp.payload.witnesses, - ); - - return { signedTxHex }; - } catch (error) { - Logger.error(`${nameof(TrezorSendStore)}::${nameof(this.signAndBroadcast)} error: ` + stringifyError(error)); - throw new convertToLocalizableError(error); - } - } - - cancel: void => void = () => { - if (!this.isActionProcessing) { - this.stores.uiDialogs.closeActiveDialog(); - this.reset(); - } - } - - @action _setActionProcessing: boolean => void = (processing) => { - this.isActionProcessing = processing; - } - - @action _setError: (?LocalizableError) => void = (error) => { - this.error = error; - } -} diff --git a/packages/yoroi-extension/app/stores/index.js b/packages/yoroi-extension/app/stores/index.js index 69946e357b..6125c5f44e 100644 --- a/packages/yoroi-extension/app/stores/index.js +++ b/packages/yoroi-extension/app/stores/index.js @@ -26,6 +26,7 @@ import ExplorerStore from './toplevel/ExplorerStore'; import ServerConnectionStore from './toplevel/ServerConnectionStore'; import ConnectorStore from './toplevel/DappConnectorStore' import ProtocolParametersStore from './toplevel/ProtocolParametersStore'; +import TransactionProcessingStore from './toplevel/TransactionProcessingStore'; /** Map of var name to class. Allows dynamic lookup of class so we can init all stores one loop */ const storeClasses = Object.freeze({ @@ -51,6 +52,7 @@ const storeClasses = Object.freeze({ explorers: ExplorerStore, connector: ConnectorStore, protocolParameters: ProtocolParametersStore, + transactionProcessingStore: TransactionProcessingStore, // note: purposely exclude substores and router }); @@ -82,6 +84,7 @@ export type StoresMap = {| // $FlowFixMe[value-as-type] router: RouterStore, protocolParameters: ProtocolParametersStore, + transactionProcessingStore: TransactionProcessingStore, |}; /** Constant that represents the stores across the lifetime of the application */ @@ -115,6 +118,7 @@ const stores: StoresMap = (observable({ router: null, connector: null, protocolParameters: null, + transactionProcessingStore: null, }): any); function initializeSubstore( diff --git a/packages/yoroi-extension/app/stores/toplevel/TransactionProcessingStore.js b/packages/yoroi-extension/app/stores/toplevel/TransactionProcessingStore.js new file mode 100644 index 0000000000..b94ba1a073 --- /dev/null +++ b/packages/yoroi-extension/app/stores/toplevel/TransactionProcessingStore.js @@ -0,0 +1,553 @@ +// @flow +import Store from '../base/Store'; +import type { StoresMap } from '../index'; +import { + HaskellShelleyTxSignRequest, + type LedgerNanoCatalystRegistrationTxSignData, + type TrezorTCatalystRegistrationTxSignData, +} from '../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; +import { + signAndBroadcastTransaction, + signTransaction, + broadcastTransaction, +} from '../../api/thunk'; +import { observable } from 'mobx'; +import Request from '../lib/LocalizedRequest'; +import { Logger, stringifyError, stringifyData, fullErrStr } from '../../utils/logging'; +import { + buildConnectorSignedTransaction as ledgerBuildConnectorSignedTransaction +} from '../../api/ada/transactions/shelley/ledgerTx'; +import { + buildConnectorSignedTransaction as trezorBuildConnectorSignedTransaction +} from '../../api/ada/transactions/shelley/trezorTx'; +import { convertToLocalizableError } from '../../domain/TrezorLocalizedError'; +import type { Addressing, Address, Value } from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; +import { + generateCip15RegistrationMetadata, + generateRegistrationMetadata, +} from '../../api/ada/lib/cardanoCrypto/catalyst'; +import { TxAuxiliaryDataSupplementType } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { fail } from '../../coreUtils'; +import { transactionHexToBodyHex } from '../../api/ada/lib/cardanoCrypto/utils'; +import { getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks.js'; +import { LedgerConnect } from '../../utils/hwConnectHandler'; +import { genAddressingLookup } from '../stateless/addressStores'; +import { RustModule } from '../../api/ada/lib/cardanoCrypto/rustLoader'; +import type { CardanoAddressedUtxo } from '../../api/ada/transactions/types'; +import { wrapWithFrame } from '../lib/TrezorWrapper'; +import type { WalletState } from '../../../chrome/extension/background/types'; + +export type SendMoneyRequest = Request>; + +export type SendUsingTrezorParams = {| + signRequest: HaskellShelleyTxSignRequest, +|}; + +export default class TransactionProcessingStore extends Store { + @observable sendMoneyRequest: SendMoneyRequest = new Request< + DeferredCall<{| txId: string |}> + >(request => request()); + + sendAndRefresh: ({| + publicDeriverId: void | number, + plateTextPart: void | string, + broadcastRequest: void => Promise<{| txId: string |}>, + refreshWallet: () => Promise, + |}) => Promise<{| txId: string |}> = async request => { + this.sendMoneyRequest.reset(); + const resp = await this.sendMoneyRequest.execute(async () => { + const result = await request.broadcastRequest(); + + if (request.publicDeriverId != null) { + const memo = this.stores.transactionBuilderStore.memo; + if (memo !== '' && memo !== undefined && request.plateTextPart) { + try { + await this.stores.memos.saveTxMemo({ + publicDeriverId: request.publicDeriverId, + plateTextPart: request.plateTextPart, + memo: { + Content: memo, + TransactionHash: result.txId, + LastUpdated: new Date(), + }, + }); + } catch (error) { + Logger.error( + `${nameof(TransactionProcessingStore)}::${nameof(this.sendAndRefresh)} error: ` + + stringifyError(error) + ); + throw new Error('An error has ocurred when saving the transaction memo.'); + } + } + } + try { + await request.refreshWallet(); + } catch (_e) { + // even if refreshing the wallet fails, we don't want to fail the tx + // otherwise user may try and re-send the tx + } + return result; + }).promise; + if (resp == null) throw new Error(`Should never happen`); + return resp; + }; + + adaSendAndRefresh: ({| + wallet: WalletState, + signRequest: HaskellShelleyTxSignRequest, + password: ?string, + callback: () => Promise, + |}) => Promise = async request => { + const { wallet, signRequest, password, callback } = request; + + let broadcastRequest; + if (wallet.type === 'ledger') { + broadcastRequest = async () => { + return await this.ledgerWalletSignAndBroadcast({ + signRequest, + wallet, + }); + }; + } else if (wallet.type === 'trezor') { + broadcastRequest = async () => { + return await this.trezorSignAndBroadcast({ + signRequest, + wallet, + }); + }; + } else if (wallet.type === 'mnemonic') { + if (!password) { + throw new Error('missing password for hardware wallet'); + } + broadcastRequest = async () => { + return await this.mnemonicWalletSignAndBroadcast({ + signRequest, + password, + publicDeriverId: wallet.publicDeriverId, + }); + }; + } else { + throw new Error( + `${nameof(TransactionProcessingStore)}::${nameof(this.adaSendAndRefresh)} unhandled wallet type` + ); + }; + await this.sendAndRefresh({ + publicDeriverId: wallet.publicDeriverId, + broadcastRequest, + refreshWallet: callback, + plateTextPart: wallet.plate.TextPart, + }); + }; + + adaSignTransactionHexFromWallet: ({| + transactionHex: string, + +wallet: { + publicDeriverId: number, + +plate: { TextPart: string, ... }, + publicKey: string, + pathToPublic: Array, + stakingAddressing: Addressing, + networkId: number, + hardwareWalletDeviceId: ?string, + type: 'trezor' | 'ledger' | 'mnemonic', + isHardware: boolean, + ... + }, + password: string, + |}) => Promise<{| signedTxHex: string |}> = async ({ wallet, transactionHex, password }) => { + let result; + if (wallet.type === 'mnemonic') { + const signedTxHex = await signTransaction({ + publicDeriverId: wallet.publicDeriverId, + transactionHex, + password, + }); + result = { signedTxHex }; + } else if (wallet.type === 'trezor') { + result = await this.trezorSignRawTx({ + rawTxHex: transactionHex, + wallet, + // by happenstance the use case of this function is not to send + // money while getting the change so there is no change address + changeAddrs: [], + }); + } else if (wallet.type === 'ledger') { + result = await this.ledgerWalletSignRawTx({ + rawTxHex: transactionHex, + wallet, + // by happenstance the use case of this function is not to send + // money while getting the change so there is no change address + changeAddrs: [], + }); + } else { + throw new Error( + `${nameof(TransactionProcessingStore)}::${nameof(this.adaSignTransactionHexFromWallet)} unhandled wallet type` + ); + } + return { signedTxHex: result.signedTxHex }; + }; + + /* + mnemonic + */ + mnemonicWalletSignAndBroadcast: {| + signRequest: HaskellShelleyTxSignRequest, + password: string, + publicDeriverId: number, + |} => Promise<{| txId: string |}> = async (request) => { + try { + const { txId } = await signAndBroadcastTransaction(request); + return { txId }; + } catch (error) { + Logger.error(`${nameof(TransactionProcessingStore)}::${nameof(this.mnemonicWalletSignAndBroadcast)} error: ${fullErrStr(error)}` ); + throw error; + } + } + + /* + trezor + */ + trezorSignAndBroadcast: {| + signRequest: HaskellShelleyTxSignRequest, + +wallet: { + publicDeriverId: number, + networkId: number, + publicKey: string, + pathToPublic: Array, + stakingAddressing: Addressing, + ... + }, + |} => Promise<{| txId: string |}> = async (request) => { + try { + Logger.debug(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignAndBroadcast)} called: ` + stringifyData(request)); + + const { signedTxHex, txId, metadata } = await this.trezorSignRawTx({ + rawTxHex: request.signRequest.self().build_tx().to_hex(), + wallet: request.wallet, + catalystData: request.signRequest.trezorTCatalystRegistrationTxSignData, + changeAddrs: request.signRequest.changeAddr, + }); + + if (metadata) { + request.signRequest.self().set_auxiliary_data(metadata); + } + + await broadcastTransaction({ + publicDeriverId: request.wallet.publicDeriverId, + signedTxHex, + }); + + return { txId }; + } catch (error) { + Logger.error(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignAndBroadcast)} error: ` + stringifyError(error)); + throw new convertToLocalizableError(error); + } + } + + trezorSignRawTx: {| + rawTxHex: string, + +wallet: { + publicDeriverId: number, + networkId: number, + publicKey: string, + pathToPublic: Array, + stakingAddressing: Addressing, + ... + }, + changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, + catalystData?: TrezorTCatalystRegistrationTxSignData, + |} => Promise<{| + signedTxHex: string, + txId: string, + metadata: ?RustModule.WalletV4.AuxiliaryData + |}> = async (request) => { + try { + Logger.debug(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignRawTx)} called: ` + stringifyData(request)); + + const addressingMap = genAddressingLookup( + request.wallet.networkId, + this.stores.addresses.addressSubgroupMap, + ); + + const network = getNetworkById(request.wallet.networkId); + + const txBodyHex = transactionHexToBodyHex(request.rawTxHex); + + const addressedUtxos = await this.stores.wallets.getAddressedUtxos(); + + const response = this.api.ada.createHwSignTxDataFromRawTx('trezor', { + txBodyHex, + network, + addressingMap, + senderUtxos: addressedUtxos, + catalystData: request.catalystData, + changeAddrs: request.changeAddrs, + }); + + const trezorSignTxPayload = response.hw === 'trezor' ? response.result.trezorSignTxPayload + : fail('Unecpected response type from `createHwSignTxDataFromRawTx` for trezor: ' + JSON.stringify(response)); + + const trezorSignTxResp = await wrapWithFrame(trezor => { + return trezor.cardanoSignTransaction( + JSON.parse(JSON.stringify({ ...trezorSignTxPayload })) + ); + }); + + if (trezorSignTxResp && trezorSignTxResp.payload && trezorSignTxResp.payload.error != null) { + // this Error will be converted to LocalizableError() + // noinspection ExceptionCaughtLocallyJS + throw new Error(trezorSignTxResp.payload.error); + } + if (!trezorSignTxResp.success) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignRawTx)} should never happen`); + } + + let metadata; + + if (request.catalystData) { + const { + votingPublicKey, + stakingKey: stakingKeyHex, + paymentAddress, + nonce, + } = request.catalystData; + + const auxDataSupplement = trezorSignTxResp.payload.auxiliaryDataSupplement; + if ( + !auxDataSupplement + || auxDataSupplement.type !== 1 + || auxDataSupplement.governanceSignature == null + ) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignRawTx)} unexpected Trezor sign transaction response`); + } + const catalystSignature = auxDataSupplement.governanceSignature; + + metadata = generateRegistrationMetadata( + votingPublicKey, + stakingKeyHex, + paymentAddress, + nonce, + (_hashedMetadata) => { + return catalystSignature; + }, + ); + // We can verify that + // Buffer.from( + // blake2b(256 / 8).update(metadata.to_bytes()).digest('binary') + // ).toString('hex') === + // trezorSignTxResp.payload.auxiliaryDataSupplement.auxiliaryDataHash + } + + const { txHex, txId } = trezorBuildConnectorSignedTransaction( + request.rawTxHex, + trezorSignTxResp.payload.witnesses, + metadata, + ); + + return { signedTxHex: txHex, txId, metadata }; + } catch (error) { + Logger.error(`${nameof(TransactionProcessingStore)}::${nameof(this.trezorSignRawTx)} error: ` + stringifyError(error)); + throw new convertToLocalizableError(error); + } + } + + /* + ledger + */ + ledgerWalletSignAndBroadcast: {| + signRequest: HaskellShelleyTxSignRequest, + +wallet: { + publicDeriverId: number, + publicKey: string, + pathToPublic: Array, + networkId: number, + hardwareWalletDeviceId: ?string, + ... + }, + |} => Promise<{| txId: string |}> = async (request) => { + try { + Logger.debug(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignAndBroadcast)} called: ` + stringifyData(request)); + + const { signedTxHex, txId, metadata } = await this.ledgerWalletSignRawTx({ + rawTxHex: request.signRequest.self().build_tx().to_hex(), + wallet: request.wallet, + catalystData: request.signRequest.ledgerNanoCatalystRegistrationTxSignData, + changeAddrs: request.signRequest.changeAddr, + additionalSenderUtxos: request.signRequest.senderUtxos, + }); + + if (metadata) { + request.signRequest.self().set_auxiliary_data(metadata); + } + + await broadcastTransaction({ + publicDeriverId: request.wallet.publicDeriverId, + signedTxHex, + networkId: request.wallet.networkId, + }); + + return { txId }; + } catch (error) { + Logger.error(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignAndBroadcast)} error: ` + stringifyError(error)); + throw new convertToLocalizableError(error); + } + }; + + ledgerWalletSignRawTx: {| + rawTxHex: string, + +wallet: { + publicDeriverId: number, + publicKey: string, + pathToPublic: Array, + networkId: number, + hardwareWalletDeviceId: ?string, + ... + }, + changeAddrs: Array<{| ...Address, ...Value, ...Addressing |}>, + // The purpose of this parameter is to support transfering from Byron address when initializing + // Ledger wallets. It is needed because the wallet's utxos property no longer contains Byron UTxOs. + additionalSenderUtxos?: Array, + catalystData?: LedgerNanoCatalystRegistrationTxSignData, + |} => Promise<{| + signedTxHex: string, + txId: string, + metadata: ?RustModule.WalletV4.AuxiliaryData + |}> = async (request) => { + try { + Logger.debug(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignRawTx)} called: ` + stringifyData(request)); + + const publicKeyInfo = { + key: RustModule.WalletV4.Bip32PublicKey.from_hex(request.wallet.publicKey), + addressing: { + startLevel: 1, + path: request.wallet.pathToPublic, + }, + }; + + const expectedSerial = request.wallet.hardwareWalletDeviceId || ''; + + const addressingMap = genAddressingLookup( + request.wallet.networkId, + this.stores.addresses.addressSubgroupMap, + ); + + Logger.debug(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignRawTx)} called: ` + stringifyData(request)); + + const ledgerConnect = new LedgerConnect({ + locale: this.stores.profile.currentLocale, + }); + + let ledgerSupportsCip36: boolean = false; + if (request.catalystData) { + const getVersionResponse = await ledgerConnect.getVersion({ + serial: expectedSerial, + dontCloseTab: true, + }); + ledgerSupportsCip36 = getVersionResponse.compatibility.supportsCIP36Vote === true; + } + + const { rawTxHex } = request; + + const network = getNetworkById(request.wallet.networkId); + + const txBodyHex = transactionHexToBodyHex(rawTxHex); + + const addressedUtxos = [ + ...await this.stores.wallets.getAddressedUtxos(), + ...(request.additionalSenderUtxos || []) + ]; + + const response = this.api.ada.createHwSignTxDataFromRawTx('ledger', { + txBodyHex, + network, + addressingMap, + senderUtxos: addressedUtxos, + ledgerSupportsCip36, + catalystData: request.catalystData, + changeAddrs: request.changeAddrs, + }); + + const ledgerSignTxPayload = response.hw === 'ledger' ? response.result.ledgerSignTxPayload + : fail('Unecpected response type from `createHwSignTxDataFromRawTx` for ledger: ' + JSON.stringify(response)); + + let ledgerSignTxResp; + try{ + ledgerSignTxResp = await ledgerConnect.signTransaction({ + serial: expectedSerial, + params: ledgerSignTxPayload, + useOpenTab: true, + }); + } finally { + // There is no need of ledgerConnect after this line. + // UI was getting blocked for few seconds + // because _prepareAndBroadcastSignedTx takes time. + // Disposing here will fix the UI issue. + ledgerConnect.dispose(); + } + + let metadata; + if (request.catalystData) { + const { + votingPublicKey, + stakingKey, + paymentAddress, + nonce, + } = request.catalystData; + + if ( + !ledgerSignTxResp.auxiliaryDataSupplement || + (ledgerSignTxResp.auxiliaryDataSupplement.type !== + TxAuxiliaryDataSupplementType.CIP36_REGISTRATION) + ) { + throw new Error(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignRawTx)} unexpected Ledger sign transaction response`); + } + const { cip36VoteRegistrationSignatureHex } = + ledgerSignTxResp.auxiliaryDataSupplement; + + if (ledgerSupportsCip36) { + metadata = generateRegistrationMetadata( + votingPublicKey, + stakingKey, + paymentAddress, + nonce, + (_hashedMetadata) => { + return cip36VoteRegistrationSignatureHex; + }, + ); + } else { + metadata = generateCip15RegistrationMetadata( + votingPublicKey, + stakingKey, + paymentAddress, + nonce, + (_hashedMetadata) => { + return cip36VoteRegistrationSignatureHex; + }, + ); + } + // We can verify that + // Buffer.from( + // blake2b(256 / 8).update(metadata.to_bytes()).digest('binary') + // ).toString('hex') === + // ledgerSignTxResp.auxiliaryDataSupplement.auxiliaryDataHashaHex + } + + const { txHex, txId } = ledgerBuildConnectorSignedTransaction( + rawTxHex, + ledgerSignTxResp.witnesses, + publicKeyInfo, + metadata, + new Map((request.additionalSenderUtxos || []).map( + ({ addressing, receiver }) => [addressing.path.join('/'), receiver] + )), + ); + + return { signedTxHex: txHex, txId, metadata }; + } catch (error) { + Logger.error(`${nameof(TransactionProcessingStore)}::${nameof(this.ledgerWalletSignRawTx)} error: ` + stringifyError(error)); + throw new convertToLocalizableError(error); + } + } +} diff --git a/packages/yoroi-extension/app/stores/toplevel/WalletStore.js b/packages/yoroi-extension/app/stores/toplevel/WalletStore.js index 0c6161b8ba..041f834156 100644 --- a/packages/yoroi-extension/app/stores/toplevel/WalletStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/WalletStore.js @@ -8,7 +8,6 @@ import config from '../../config'; import globalMessages from '../../i18n/global-messages'; import type { Notification } from '../../types/notification.types'; import type { IGetLastSyncInfoResponse } from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; -import { Logger, stringifyError } from '../../utils/logging'; import type { WalletChecksum } from '@emurgo/cip4-js'; import { createDebugWalletDialog } from '../../containers/wallet/dialogs/DebugWalletDialogContainer'; import { createProblematicWalletDialog } from '../../containers/wallet/dialogs/ProblematicWalletDialogContainer'; @@ -29,8 +28,6 @@ import { asAddressedUtxo } from '../../api/ada/transactions/utils'; declare var chrome; */ -export type SendMoneyRequest = Request>; - /** * The base wallet store that contains the shared logic * dealing with wallets / accounts. @@ -48,10 +45,6 @@ export default class WalletStore extends Store { @observable getInitialWallets: Request = new Request(getWallets); - @observable sendMoneyRequest: SendMoneyRequest = new Request< - DeferredCall<{| txId: string |}> - >(request => request()); - @observable createWalletRequest: Request< (() => Promise) => Promise > = new Request(async create => { @@ -381,50 +374,6 @@ export default class WalletStore extends Store { } }; - sendAndRefresh: ({| - publicDeriverId: void | number, - plateTextPart: void | string, - broadcastRequest: void => Promise<{| txId: string |}>, - refreshWallet: () => Promise, - |}) => Promise<{| txId: string |}> = async request => { - this.sendMoneyRequest.reset(); - const resp = await this.sendMoneyRequest.execute(async () => { - const result = await request.broadcastRequest(); - - if (request.publicDeriverId != null) { - const memo = this.stores.transactionBuilderStore.memo; - if (memo !== '' && memo !== undefined && request.plateTextPart) { - try { - await this.stores.memos.saveTxMemo({ - publicDeriverId: request.publicDeriverId, - plateTextPart: request.plateTextPart, - memo: { - Content: memo, - TransactionHash: result.txId, - LastUpdated: new Date(), - }, - }); - } catch (error) { - Logger.error( - `${nameof(WalletStore)}::${nameof(this.sendAndRefresh)} error: ` + - stringifyError(error) - ); - throw new Error('An error has ocurred when saving the transaction memo.'); - } - } - } - try { - await request.refreshWallet(); - } catch (_e) { - // even if refreshing the wallet fails, we don't want to fail the tx - // otherwise user may try and re-send the tx - } - return result; - }).promise; - if (resp == null) throw new Error(`Should never happen`); - return resp; - }; - @action onRenameSelectedWallet: (string) => void = (newName) => { this.selectedWalletName = newName; if (this.selectedIndex != null) { diff --git a/packages/yoroi-extension/app/stores/toplevel/YoroiTransferStore.js b/packages/yoroi-extension/app/stores/toplevel/YoroiTransferStore.js index aaed7def81..3c34a95e56 100644 --- a/packages/yoroi-extension/app/stores/toplevel/YoroiTransferStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/YoroiTransferStore.js @@ -194,7 +194,7 @@ export default class YoroiTransferStore extends Store { const { next } = payload; try { - await this.stores.wallets.sendAndRefresh({ + await this.stores.transactionProcessingStore.sendAndRefresh({ publicDeriverId: undefined, plateTextPart: undefined, broadcastRequest: async () => { @@ -256,7 +256,7 @@ export default class YoroiTransferStore extends Store { this.status = TransferStatus.UNINITIALIZED; this.error = null; this.transferTx = null; - this.stores.wallets.sendMoneyRequest.reset(); + this.stores.transactionProcessingStore.sendMoneyRequest.reset(); this.recoveryPhrase = ''; if (this.stores.profile.selectedNetwork != null) { diff --git a/packages/yoroi-extension/app/types/WalletSendTypes.js b/packages/yoroi-extension/app/types/WalletSendTypes.js index 0ffcdce713..b970700abb 100644 --- a/packages/yoroi-extension/app/types/WalletSendTypes.js +++ b/packages/yoroi-extension/app/types/WalletSendTypes.js @@ -3,5 +3,4 @@ export const SEND_FORM_STEP = { RECEIVER: 1, AMOUNT: 2, - PREVIEW: 3, }