diff --git a/packages/yoroi-connector/example-cardano/index.js b/packages/yoroi-connector/example-cardano/index.js index 74de971823..5055a317c0 100644 --- a/packages/yoroi-connector/example-cardano/index.js +++ b/packages/yoroi-connector/example-cardano/index.js @@ -482,6 +482,17 @@ signTx.addEventListener("click", () => { // add a keyhash input - for ADA held in a Shelley-era normal address (Base, Enterprise, Pointer) const utxo = utxos[0]; + const assets = CardanoWasm.MultiAsset.new(); + for (const asset of utxo.assets) { + const policyId = CardanoWasm.ScriptHash.from_hex(asset.policyId); + const policyContent = assets.get(policyId) || CardanoWasm.Assets.new(); + policyContent.insert( + CardanoWasm.AssetName.new(Buffer.from(asset.name, 'hex')), + CardanoWasm.BigNum.from_str(asset.amount) + ); + assets.insert(policyId, policyContent); + } + const addr = CardanoWasm.Address.from_bech32(utxo.receiver); const baseAddr = CardanoWasm.BaseAddress.from_address(addr); @@ -492,7 +503,10 @@ signTx.addEventListener("click", () => { CardanoWasm.TransactionHash.from_bytes(hexToBytes(utxo.tx_hash)), // tx hash utxo.tx_index // index ), - CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(utxo.amount)) + CardanoWasm.Value.new_with_assets( + CardanoWasm.BigNum.from_str(utxo.amount), + assets + ) ); const shelleyOutputAddress = diff --git a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/traitUtils.js b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/traitUtils.js index 8241ef9b11..b1312f5da4 100644 --- a/packages/yoroi-extension/app/api/ada/lib/storage/bridge/traitUtils.js +++ b/packages/yoroi-extension/app/api/ada/lib/storage/bridge/traitUtils.js @@ -166,6 +166,86 @@ export async function getAllAddressesForDisplay( ); } +type AddressWithDerivationPath = {| + +address: string, + +path: Array, +|}; + +export async function getAllAddressesWithPaths( + publicDeriver: IPublicDeriver, +): Promise<{| + utxoAddresses: Array<$ReadOnly>, + accountingAddresses: Array<$ReadOnly>, +|}> { + const withLevels = asHasLevels(publicDeriver); + if (!withLevels) { + throw new Error(`${nameof(getAllAddressesWithPaths)} publicDerviver traits missing`); + } + const derivationTables = withLevels.getParent().getDerivationTables(); + const deps = Object.freeze({ + GetPathWithSpecific, + GetAddress, + GetDerivationSpecific, + }); + const depTables = Object.keys(deps) + .map(key => deps[key]) + .flatMap(table => getAllSchemaTables(publicDeriver.getDb(), table)); + + return await raii( + publicDeriver.getDb(), + [ + ...depTables, + ...mapToTables(publicDeriver.getDb(), derivationTables), + ], + async dbTx => { + const utxoAddresses = []; + const accountingAddresses = []; + const withUtxos = asGetAllUtxos(publicDeriver); + if (withUtxos != null) { + const addrResponse = await withUtxos.rawGetAllUtxoAddresses( + dbTx, + { + GetPathWithSpecific: deps.GetPathWithSpecific, + GetAddress: deps.GetAddress, + GetDerivationSpecific: deps.GetDerivationSpecific, + }, + undefined, + derivationTables, + ); + for (const family of addrResponse) { + for (const addr of family.addrs) { + utxoAddresses.push({ address: addr.Hash, path: family.addressing.path }); + } + } + } + const withAccounting = asGetAllAccounting(publicDeriver); + if (withAccounting != null) { + const addrResponse = await withAccounting.rawGetAllAccountingAddresses( + dbTx, + { + GetPathWithSpecific: deps.GetPathWithSpecific, + GetAddress: deps.GetAddress, + GetDerivationSpecific: deps.GetDerivationSpecific, + }, + undefined, + derivationTables, + ); + for (const family of addrResponse) { + for (const addr of family.addrs) { + accountingAddresses.push({ address: addr.Hash, path: family.addressing.path }); + } + } + } + + return { + utxoAddresses, + accountingAddresses, + }; + + }, + ); +} + export async function rawGetAddressRowsForWallet( tx: lf$Transaction, deps: {| 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 5b7c5ff835..456e456df3 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/ledgerTx.js @@ -26,6 +26,7 @@ import { TxAuxiliaryDataType, StakeCredentialParamsType, CIP36VoteRegistrationFormat, + TxRequiredSignerType, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { RustModule } from '../../lib/cardanoCrypto/rustLoader'; import { toHexOrBase58 } from '../../lib/storage/bridge/utils'; @@ -232,6 +233,7 @@ function _transformToLedgerOutputs(request: {| addressingMap: string => (void | $PropertyType), |}): Array { const result = []; + for (let i = 0; i < request.txOutputs.len(); i++) { const output = request.txOutputs.get(i); const address = output.address(); @@ -272,7 +274,7 @@ function _transformToLedgerOutputs(request: {| function formatLedgerWithdrawals( withdrawals: RustModule.WalletV4.Withdrawals, - addressingMap: string => (void | $PropertyType), + addressingMap: string => (void | { +path: Array, ... }), ): Array { const result = []; @@ -302,7 +304,7 @@ function formatLedgerWithdrawals( function formatLedgerCertificates( networkId: number, certificates: RustModule.WalletV4.Certificates, - addressingMap: string => (void | $PropertyType), + addressingMap: string => (void | { +path: Array, ... }), ): Array { const getPath = ( stakeCredential: RustModule.WalletV4.StakeCredential @@ -592,3 +594,279 @@ export function buildSignedTransaction( metadata ); } + +type AddressMap = { [addressHex: string]: Array }; + +// Convert connector sign tx input into request to Ledger. +// Note this function has some overlaps in functionality with above functions but +// this function is more generic because above functions deal only with Yoroi +// extension "send" transactions. +export function toLedgerSignRequest( + txBody: RustModule.WalletV4.TransactionBody, + networkId: number, + protocolMagic: number, + ownUtxoAddressMap: AddressMap, + ownStakeAddressMap: AddressMap, + addressedUtxos: Array, +): SignTransactionRequest { + function formatInputs(inputs: RustModule.WalletV4.TransactionInputs): Array { + const formatted = []; + for (let i = 0; i < inputs.len(); i++) { + const input = inputs.get(i); + const hash = input.transaction_id().to_hex(); + const index = input.index(); + const ownUtxo = addressedUtxos.find(utxo => + utxo.tx_hash === hash && utxo.tx_index === index + ); + formatted.push({ + txHashHex: hash, + outputIndex: index, + path: ownUtxo ? ownUtxo.addressing.path : null, + }); + } + return formatted.sort(compareInputs); + } + + function formatOutput(output: RustModule.WalletV4.TransactionOutput): TxOutput { + const addr = output.address(); + let destination; + + // Yoroi doesn't have Byron addresses or pointer addresses. + // If the address is one of these, it's not a wallet address. + const byronAddr = RustModule.WalletV4.ByronAddress.from_address(addr); + const pointerAddr = RustModule.WalletV4.PointerAddress.from_address(addr); + if (byronAddr || pointerAddr) { + destination = { + type: TxOutputDestinationType.THIRD_PARTY, + params: { + addressHex: addr.to_hex(), + }, + }; + } + + const enterpriseAddr = RustModule.WalletV4.EnterpriseAddress.from_address(addr); + if (enterpriseAddr) { + const ownAddressPath = ownUtxoAddressMap[addr.to_hex()]; + if (ownAddressPath) { + destination = { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.ENTERPRISE_KEY, + params: { + spendingPath: ownAddressPath, + }, + }, + }; + } else { + destination = { + type: TxOutputDestinationType.THIRD_PARTY, + params: { + addressHex: addr.to_hex(), + }, + }; + } + } + + const baseAddr = RustModule.WalletV4.BaseAddress.from_address(addr); + if (baseAddr) { + const paymentAddress = RustModule.WalletV4.EnterpriseAddress.new( + networkId, + baseAddr.payment_cred() + ).to_address().to_hex(); + const ownPaymentPath = ownUtxoAddressMap[paymentAddress]; + if (ownPaymentPath) { + const stake = baseAddr.stake_cred(); + const stakeAddr = RustModule.WalletV4.RewardAddress.new( + networkId, + stake, + ).to_address().to_hex(); + const ownStakePath = ownStakeAddressMap[stakeAddr]; + if (ownStakePath) { + // stake address is ours + destination = { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: ownPaymentPath, + stakingPath: ownStakePath, + }, + } + }; + } else { + const keyHash = stake.to_keyhash(); + const scriptHash = stake.to_scripthash(); + if (keyHash) { + // stake address is foreign key hash + destination = { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: ownPaymentPath, + stakingKeyHashHex: keyHash.to_hex(), + }, + } + }; + } else if (scriptHash) { + // stake address is script hash + destination = { + type: TxOutputDestinationType.DEVICE_OWNED, + params: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_SCRIPT, + params: { + spendingPath: ownPaymentPath, + stakingScriptHashHex: scriptHash.to_hex(), + }, + } + }; + } else { + throw new Error('unexpected stake credential type in base address'); + } + } + // not having BASE_PAYMENT_SCRIPT_ because payment script is + // treated as third party address + } else { // payment address is not ours + destination = { + type: TxOutputDestinationType.THIRD_PARTY, + params: { + addressHex: addr.to_hex(), + }, + }; + } + } + + // we do not allow payment to RewardAddresses + if (!destination) { + throw new Error('not expecting to pay to reward address'); + } + + const outputDataHash = output.data_hash(); + + // TODO: Babbage-era output support + return { + amount: output.amount().coin().to_str(), + destination, + tokenBundle: toLedgerTokenBundle(output.amount().multiasset()), + datumHashHex: outputDataHash ? outputDataHash.to_hex() : null, + }; + } + + const outputs = []; + for (let i = 0; i < txBody.outputs().len(); i++) { + outputs.push(formatOutput(txBody.outputs().get(i))); + } + + const formattedRequiredSigners = []; + const requiredSigners = txBody.required_signers(); + if (requiredSigners) { + for (let i = 0; i < requiredSigners.len(); i++) { + const hash = requiredSigners.get(i); + const address = RustModule.WalletV4.EnterpriseAddress.new( + networkId, + RustModule.WalletV4.StakeCredential.from_keyhash(hash), + ).to_address().to_hex(); + const ownAddressPath = ownUtxoAddressMap[address]; + if (ownAddressPath) { + formattedRequiredSigners.push({ + type: TxRequiredSignerType.PATH, + path: ownAddressPath, + }); + } else { + formattedRequiredSigners.push({ + type: TxRequiredSignerType.HASH, + hashHex: hash.to_hex(), + }); + } + } + } + + function addressingMap(addr: string): void | {| +path: Array |} { + const path = ownUtxoAddressMap[addr] || ownStakeAddressMap[addr]; + if (path) { + return { path }; + } + return undefined; + } + + let formattedCertificates = null; + const certificates = txBody.certs(); + if (certificates) { + formattedCertificates = formatLedgerCertificates( + networkId, + certificates, + addressingMap, + ); + } + + let formattedWithdrawals = null; + const withdrawals = txBody.withdrawals(); + if (withdrawals) { + formattedWithdrawals = formatLedgerWithdrawals( + withdrawals, + addressingMap, + ); + } + + // TODO: support CIP36 aux data + let formattedAuxiliaryData = null; + const auxiliaryDataHash = txBody.auxiliary_data_hash(); + if (auxiliaryDataHash) { + formattedAuxiliaryData = { + type: TxAuxiliaryDataType.ARBITRARY_HASH, + params: { + hashHex: auxiliaryDataHash.to_hex(), + } + }; + } + + let formattedCollateral = null; + const collateral = txBody.collateral(); + if (collateral) { + formattedCollateral = formatInputs(collateral); + } + + let formattedCollateralReturn = null; + const collateralReturn = txBody.collateral_return(); + if (collateralReturn) { + formattedCollateralReturn = formatOutput(collateralReturn); + } + + let formattedReferenceInputs = null; + const referenceInputs = txBody.reference_inputs(); + if (referenceInputs) { + formattedReferenceInputs = formatInputs(referenceInputs); + } + + return { + signingMode: TransactionSigningMode.ORDINARY_TRANSACTION, + tx: { + network: { + networkId, + protocolMagic, + }, + inputs: formatInputs(txBody.inputs()), + outputs, + fee: txBody.fee().to_str(), + ttl: txBody.ttl(), + certificates: formattedCertificates, + withdrawals: formattedWithdrawals, + auxiliaryData: formattedAuxiliaryData, + validityIntervalStart: txBody.validity_start_interval_bignum()?.to_str() ?? null, + mint: txBody.mint()?.to_js_value().map(([policyIdHex, assets]) => ({ + policyIdHex, + tokens: Object.keys(assets).map(assetNameHex => ( + { assetNameHex, amount: assets[assetNameHex] } + )), + })) ?? null, + scriptDataHashHex: txBody.script_data_hash()?.to_hex() ?? null, + collateralInputs: formattedCollateral, + requiredSigners: requiredSigners ? formattedRequiredSigners : null, + includeNetworkId: txBody.network_id() != null, + collateralOutput: formattedCollateralReturn, + totalCollateral: txBody.total_collateral()?.to_str() ?? null, + referenceInputs: formattedReferenceInputs, + }, + additionalWitnessPaths: [], + }; +} 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 be5ff5e5e6..88aff17f8d 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/trezorTx.js @@ -484,3 +484,340 @@ export function buildSignedTransaction( metadata ); } + +type AddressMap = { [addressHex: string]: Array }; + +// Convert connector sign tx input into request to Trezor. +// Note this function has some overlaps in functionality with above functions but +// this function is more generic because above functions deal only with Yoroi +// extension "send" transactions. +export function toTrezorSignRequest( + txBody: RustModule.WalletV4.TransactionBody, + networkId: number, + protocolMagic: number, + ownUtxoAddressMap: AddressMap, + ownStakeAddressMap: AddressMap, + addressedUtxos: Array, +): $Exact { + function formatInputs(inputs: RustModule.WalletV4.TransactionInputs): Array { + const formatted = []; + for (let i = 0; i < inputs.len(); i++) { + const input = inputs.get(i); + const hash = input.transaction_id().to_hex(); + const index = input.index(); + const ownUtxo = addressedUtxos.find(utxo => + utxo.tx_hash === hash && utxo.tx_index === index + ); + formatted.push({ + prev_hash: hash, + prev_index: index, + ...(ownUtxo ? { path: ownUtxo.addressing.path } : {}) + }); + } + return formatted.sort(compareInputs); + } + + function formatOutput(output: RustModule.WalletV4.TransactionOutput): CardanoOutput { + const amount = output.amount().coin().to_str(); + const tokenBundle = toTrezorTokenBundle(output.amount().multiasset()); + const outputDataHash = output.data_hash(); + const datumHash = outputDataHash ? { datumHash: outputDataHash.to_hex() } : {}; + + const addr = output.address(); + let result; + // Yoroi doesn't have Byron addresses or pointer addresses. + // If the address is one of these, it's not a wallet address. + const byronAddr = RustModule.WalletV4.ByronAddress.from_address(addr); + const pointerAddr = RustModule.WalletV4.PointerAddress.from_address(addr); + if (byronAddr || pointerAddr) { + result = { + address: addr.to_bech32(), + amount, + ...tokenBundle, + ...datumHash, + }; + } + + const enterpriseAddr = RustModule.WalletV4.EnterpriseAddress.from_address(addr); + if (enterpriseAddr) { + const ownAddressPath = ownUtxoAddressMap[addr.to_bech32()]; + if (ownAddressPath) { + result = { + addressParameters: { + addressType: ADDRESS_TYPE.Enterprise, + path: ownAddressPath, + }, + amount, + ...tokenBundle, + ...datumHash, + }; + } else { + result = { + address: addr.to_bech32(), + amount, + ...tokenBundle, + ...datumHash, + }; + } + } + + const baseAddr = RustModule.WalletV4.BaseAddress.from_address(addr); + if (baseAddr) { + const paymentAddress = RustModule.WalletV4.EnterpriseAddress.new( + networkId, + baseAddr.payment_cred() + ).to_address().to_hex(); + const ownPaymentPath = ownUtxoAddressMap[paymentAddress]; + if (ownPaymentPath) { + const stake = baseAddr.stake_cred(); + const stakeAddr = RustModule.WalletV4.RewardAddress.new( + networkId, + stake, + ).to_address().to_hex(); + const ownStakePath = ownStakeAddressMap[stakeAddr]; + if (ownStakePath) { + // stake address is ours + result = { + addressParameters: { + addressType: ADDRESS_TYPE.Base, + path: ownPaymentPath, + stakingPath: ownStakePath, + }, + amount, + ...tokenBundle, + ...datumHash, + }; + } else { + const keyHash = stake.to_keyhash(); + const scriptHash = stake.to_scripthash(); + if (keyHash) { + // stake address is foreign key hash + result = { + addressParameters: { + addressType: ADDRESS_TYPE.Base, + path: ownPaymentPath, + stakingKeyHas: keyHash.to_hex(), + }, + amount, + ...tokenBundle, + ...datumHash, + }; + } else if (scriptHash) { + // stake address is script hash + result = { + addressParameters: { + addressType: ADDRESS_TYPE.Base, + path: ownPaymentPath, + stakingScriptHash: scriptHash.to_hex(), + }, + amount, + ...tokenBundle, + ...datumHash, + }; + } else { + throw new Error('unexpected stake credential type in base address'); + } + } + // not having BASE_PAYMENT_SCRIPT_ because payment script is + // treated as third party address + } else { // payment address is not ours + result = { + address: addr.to_bech32(), + amount, + ...tokenBundle, + ...datumHash, + }; + } + } + + // we do not allow payment to RewardAddresses + if (!result) { + throw new Error('not expecting to pay to reward address'); + } + + return result; + } + + const outputs = []; + for (let i = 0; i < txBody.outputs().len(); i++) { + outputs.push(formatOutput(txBody.outputs().get(i))); + } + + const formattedRequiredSigners = []; + const requiredSigners = txBody.required_signers(); + if (requiredSigners) { + for (let i = 0; i < requiredSigners.len(); i++) { + const hash = requiredSigners.get(i); + const address = RustModule.WalletV4.EnterpriseAddress.new( + networkId, + RustModule.WalletV4.StakeCredential.from_keyhash(hash), + ).to_address().to_hex(); + const ownAddressPath = ownUtxoAddressMap[address]; + if (ownAddressPath) { + formattedRequiredSigners.push({ + keyPath: ownAddressPath, + }); + } else { + formattedRequiredSigners.push({ + keyHash: hash.to_hex(), + }); + } + } + } + + let formattedCertificates = null; + const certificates = txBody.certs(); + if (certificates) { + const getPath = ( + stakeCredential: RustModule.WalletV4.StakeCredential + ): Array => { + const rewardAddr = RustModule.WalletV4.RewardAddress.new( + networkId, + stakeCredential + ); + const addressPayload = Buffer.from(rewardAddr.to_address().to_bytes()).toString('hex'); + const addressing = ownStakeAddressMap[addressPayload]; + if (addressing == null) { + throw new Error('not own address in certificate'); + } + return addressing; + }; + + const result = []; + for (let i = 0; i < certificates.len(); i++) { + const cert = certificates.get(i); + + const registrationCert = cert.as_stake_registration(); + if (registrationCert != null) { + result.push({ + type: CERTIFICATE_TYPE.StakeRegistration, + path: getPath(registrationCert.stake_credential()), + }); + continue; + } + const deregistrationCert = cert.as_stake_deregistration(); + if (deregistrationCert != null) { + result.push({ + type: CERTIFICATE_TYPE.StakeDeregistration, + path: getPath(deregistrationCert.stake_credential()), + }); + continue; + } + const delegationCert = cert.as_stake_delegation(); + if (delegationCert != null) { + result.push({ + type: CERTIFICATE_TYPE.StakeDelegation, + path: getPath(delegationCert.stake_credential()), + pool:delegationCert.pool_keyhash().to_hex(), + }); + continue; + } + throw new Error(`unsupported certificate type`); + } + formattedCertificates = result; + } + + let formattedWithdrawals = null; + const withdrawals = txBody.withdrawals(); + if (withdrawals) { + const result = []; + + const withdrawalKeys = withdrawals.keys(); + for (let i = 0; i < withdrawalKeys.len(); i++) { + const rewardAddress = withdrawalKeys.get(i); + const withdrawalAmount = withdrawals.get(rewardAddress); + if (withdrawalAmount == null) { + throw new Error('missing withdraw amount should never happen'); + } + + const rewardAddressPayload = rewardAddress.to_address().to_hex(); + const path = ownStakeAddressMap[rewardAddressPayload]; + if (path == null) { + throw new Error('foreign withdrawal reward address'); + } + result.push({ + amount: withdrawalAmount.to_str(), + path, + }); + } + formattedWithdrawals = result; + } + + // TODO: support CIP36 aux data + let formattedAuxiliaryData = null; + const auxiliaryDataHash = txBody.auxiliary_data_hash(); + if (auxiliaryDataHash) { + formattedAuxiliaryData = { + hash: auxiliaryDataHash.to_hex(), + }; + } + + let formattedCollateral = null; + const collateral = txBody.collateral(); + if (collateral) { + // eslint-disable-next-line no-unused-vars + formattedCollateral = formatInputs(collateral); + } + + let formattedCollateralReturn = null; + const collateralReturn = txBody.collateral_return(); + if (collateralReturn) { + formattedCollateralReturn = formatOutput(collateralReturn); + } + + let formattedReferenceInputs = null; + const referenceInputs = txBody.reference_inputs(); + if (referenceInputs) { + // eslint-disable-next-line no-unused-vars + formattedReferenceInputs = formatInputs(referenceInputs); + } + + const validityIntervalStart = txBody.validity_start_interval_bignum()?.to_str() ?? null; + // temp workaround for buggy Mint.to_js_value() + const formattedMint = JSON.parse(txBody.mint()?.to_json() ?? 'null')?.map(([policyId, assets]) => ({ + policyId, + tokenAmounts: Object.keys(assets).map(assetNameBytes => ( + { assetNameBytes, mintAmount: assets[assetNameBytes] } + )), + })); + + const scriptDataHash = txBody.script_data_hash()?.to_hex(); + + const result: $Exact = { + signingMode: CardanoTxSigningMode.ORDINARY_TRANSACTION, + inputs: formatInputs(txBody.inputs()), + outputs, + fee: txBody.fee().to_str(), + protocolMagic, + networkId, + ttl: String(txBody.ttl()), + //includeNetworkId: txBody.network_id() != null, + }; + + if (validityIntervalStart) { + result.validityIntervalStart = validityIntervalStart; + } + if (formattedCertificates) { + result.certificates = formattedCertificates; + } + if (formattedWithdrawals) { + result.withdrawals = formattedWithdrawals; + } + if (formattedAuxiliaryData) { + result.auxiliaryData = formattedAuxiliaryData; + } + if (formattedMint) { + result.mint = formattedMint; + } + if (scriptDataHash) { + //result.scriptDataHash = scriptDataHash; + } + if (formattedCollateralReturn) { + //result.collateralInputs = formattedCollateral; + } + if (requiredSigners) { + //result.requiredSigners = formattedRequiredSigners; + } + // connector API doesn't support additionalWitnessRequests + return result; +} diff --git a/packages/yoroi-extension/app/connector/actions/connector-actions.js b/packages/yoroi-extension/app/connector/actions/connector-actions.js index 75b524636b..93bdaed0aa 100644 --- a/packages/yoroi-extension/app/connector/actions/connector-actions.js +++ b/packages/yoroi-extension/app/connector/actions/connector-actions.js @@ -15,6 +15,6 @@ export default class ConnectorActions { url: string, protocol: string, |}> = new AsyncAction(); - confirmSignInTx: Action = new Action(); + confirmSignInTx: AsyncAction = new AsyncAction(); cancelSignInTx: Action = new Action(); } diff --git a/packages/yoroi-extension/app/connector/components/signin/AddCollateralPage.js b/packages/yoroi-extension/app/connector/components/signin/AddCollateralPage.js index 83b067c599..5620cc452e 100644 --- a/packages/yoroi-extension/app/connector/components/signin/AddCollateralPage.js +++ b/packages/yoroi-extension/app/connector/components/signin/AddCollateralPage.js @@ -28,6 +28,8 @@ import { signTxMessages } from './SignTxPage'; import { WrongPassphraseError } from '../../../api/ada/lib/cardanoCrypto/cryptoErrors'; import { LoadingButton } from '@mui/lab'; import { ReactComponent as AddCollateralIcon } from '../../../assets/images/dapp-connector/add-collateral.inline.svg'; +import type LocalizableError from '../../../i18n/LocalizableError'; +import ErrorBlock from '../../../components/widgets/ErrorBlock'; type Props = {| +txData: ?CardanoConnectorSignRequest, @@ -36,6 +38,8 @@ type Props = {| +getTokenInfo: ($ReadOnly>) => ?$ReadOnly, +selectedExplorer: SelectedExplorer, +submissionError: ?SignSubmissionErrorType, + +walletType: 'ledger' | 'trezor' | 'web', + +hwWalletError: ?LocalizableError, |}; const messages = defineMessages({ @@ -106,27 +110,34 @@ class AddCollateralPage extends Component { ); submit(): void { - this.form.submit({ - onSuccess: form => { - const { walletPassword } = form.values(); - this.setState({ isSubmitting: true }); - this.props - .onConfirm(walletPassword) - .finally(() => { - this.setState({ isSubmitting: false }); - }) - .catch(error => { - if (error instanceof WrongPassphraseError) { - this.form - .$('walletPassword') - .invalidate(this.context.intl.formatMessage(messages.incorrectWalletPasswordError)); - } else { - throw error; - } - }); - }, - onError: () => {}, - }); + if (this.props.walletType === 'web') { + this.form.submit({ + onSuccess: form => { + const { walletPassword } = form.values(); + this.setState({ isSubmitting: true }); + this.props + .onConfirm(walletPassword) + .finally(() => { + this.setState({ isSubmitting: false }); + }) + .catch(error => { + if (error instanceof WrongPassphraseError) { + this.form + .$('walletPassword') + .invalidate(this.context.intl.formatMessage(messages.incorrectWalletPasswordError)); + } else { + throw error; + } + }); + }, + onError: () => {}, + }); + } else { + this.setState({ isSubmitting: true }); + this.props.onConfirm('').finally(() => { + this.setState({ isSubmitting: false }); + }).catch(error => { throw error; }); + } } getTicker: ($ReadOnly) => Node = tokenInfo => { @@ -223,6 +234,20 @@ class AddCollateralPage extends Component { ); + const { walletType } = this.props; + let confirmButtonLabel; + switch (walletType) { + case 'ledger': + confirmButtonLabel = globalMessages.confirmOnLedger; + break; + case 'trezor': + confirmButtonLabel = globalMessages.confirmOnTrezor; + break; + default: + confirmButtonLabel = globalMessages.confirm; + break; + } + return ( @@ -283,17 +308,20 @@ class AddCollateralPage extends Component { - - - {submissionError === 'SEND_TX_ERROR' && ( - {intl.formatMessage(messages.sendError)} - )} - + {walletType === 'web' && ( + + + {submissionError === 'SEND_TX_ERROR' && ( + {intl.formatMessage(messages.sendError)} + )} + + )} + { sx={{ minWidth: 'auto' }} variant="primary" fullWidth - disabled={!walletPasswordField.isValid} + disabled={walletType === 'web' && !walletPasswordField.isValid} onClick={this.submit.bind(this)} loading={isSubmitting} > - {intl.formatMessage(globalMessages.confirm)} + {intl.formatMessage(confirmButtonLabel)} diff --git a/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js b/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js index c9afadb1d6..db38c50ee9 100644 --- a/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js +++ b/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js @@ -44,6 +44,8 @@ import { signTxMessages } from './SignTxPage'; import { WrongPassphraseError } from '../../../api/ada/lib/cardanoCrypto/cryptoErrors'; import { LoadingButton } from '@mui/lab'; import { ReactComponent as NoDappIcon } from '../../../assets/images/dapp-connector/no-dapp.inline.svg'; +import type LocalizableError from '../../../i18n/LocalizableError'; +import ErrorBlock from '../../../components/widgets/ErrorBlock'; type Props = {| +txData: ?CardanoConnectorSignRequest, @@ -63,6 +65,10 @@ type Props = {| +connectedWebsite: ?WhitelistEntry, +submissionError: ?SignSubmissionErrorType, +signData: ?{| address: string, payload: string |}, + +walletType: 'ledger' | 'trezor' | 'web', + +hwWalletError: ?LocalizableError, + +isHwWalletErrorRecoverable: ?boolean, + +tx: ?string, |}; const messages = defineMessages({ @@ -124,27 +130,34 @@ class SignTxPage extends Component { ); submit(): void { - this.form.submit({ - onSuccess: form => { - const { walletPassword } = form.values(); - this.setState({ isSubmitting: true }); - this.props - .onConfirm(walletPassword) - .finally(() => { - this.setState({ isSubmitting: false }); - }) - .catch(error => { - if (error instanceof WrongPassphraseError) { - this.form - .$('walletPassword') - .invalidate(this.context.intl.formatMessage(messages.incorrectWalletPasswordError)); - } else { - throw error; - } - }); - }, - onError: () => {}, - }); + if (this.props.walletType === 'web') { + this.form.submit({ + onSuccess: form => { + const { walletPassword } = form.values(); + this.setState({ isSubmitting: true }); + this.props + .onConfirm(walletPassword) + .finally(() => { + this.setState({ isSubmitting: false }); + }) + .catch(error => { + if (error instanceof WrongPassphraseError) { + this.form + .$('walletPassword') + .invalidate(this.context.intl.formatMessage(messages.incorrectWalletPasswordError)); + } else { + throw error; + } + }); + }, + onError: () => {}, + }); + } else { + this.setState({ isSubmitting: true }); + this.props.onConfirm('').finally(() => { + this.setState({ isSubmitting: false }); + }).catch(error => { throw error; }); + } } getTicker: ($ReadOnly) => Node = tokenInfo => { @@ -335,6 +348,27 @@ class SignTxPage extends Component { const url = connectedWebsite?.url ?? ''; const faviconUrl = connectedWebsite?.image ?? ''; + const { walletType, hwWalletError, isHwWalletErrorRecoverable } = this.props; + + if (hwWalletError && isHwWalletErrorRecoverable === false) { + return ( + <> + + {this.props.tx && ( + + Transaction: +