From 1ed70233fe0ca1e7d81f63dbe44caa220c67b11e Mon Sep 17 00:00:00 2001 From: Javier Bueno Date: Tue, 17 Sep 2024 19:05:50 +0200 Subject: [PATCH 01/23] feature(tx-review): initial commit --- .../src/components/Icon/Direction.tsx | 7 +- .../src/components/Warning/Warning.tsx | 16 +- .../ReviewTransaction/ReviewTransaction.tsx | 26 +- .../useCases/ReviewTransaction/types.ts | 846 +++++++++++++++++ .../ReviewTransactionNavigator.tsx | 39 + .../ReviewTransaction/common/Divider.tsx | 21 + .../common/formattedTransaction.tsx | 122 +++ .../ReviewTransaction/common/mocks.ts | 148 +++ .../ReviewTransaction/common/types.ts | 855 ++++++++++++++++++ .../Overview/OverviewTab.tsx | 469 ++++++++++ .../ReviewTransactionScreen.tsx | 59 ++ .../useCases/ConfirmTx/ConfirmTxScreen.tsx | 8 + .../Transactions/TxHistoryNavigator.tsx | 5 +- apps/wallet-mobile/src/kernel/navigation.tsx | 10 + .../src/yoroi-wallets/hooks/index.ts | 15 + .../messages/src/WalletNavigator.json | 48 +- .../Transactions/TxHistoryNavigator.json | 176 ++-- .../theme/src/base-palettes/dark-palette.ts | 1 + .../theme/src/base-palettes/light-palette.ts | 1 + packages/theme/src/types.ts | 1 + 20 files changed, 2736 insertions(+), 137 deletions(-) create mode 100644 apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx diff --git a/apps/wallet-mobile/src/components/Icon/Direction.tsx b/apps/wallet-mobile/src/components/Icon/Direction.tsx index a704140b92..45c37b73ea 100644 --- a/apps/wallet-mobile/src/components/Icon/Direction.tsx +++ b/apps/wallet-mobile/src/components/Icon/Direction.tsx @@ -1,6 +1,6 @@ import {ThemedPalette, useTheme} from '@yoroi/theme' import React from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet, View, ViewStyle} from 'react-native' import {TransactionDirection, TransactionInfo} from '../../yoroi-wallets/types' import {Received} from '../Icon/Received' @@ -11,9 +11,10 @@ import {MultiParty} from './MultiParty' type Props = { transaction: TransactionInfo size?: number + containerStyle?: ViewStyle } -export const Direction = ({transaction, size = defaultSize}: Props) => { +export const Direction = ({transaction, size = defaultSize, containerStyle}: Props) => { const {color} = useTheme() const {direction} = transaction @@ -21,7 +22,7 @@ export const Direction = ({transaction, size = defaultSize}: Props) => { const IconComponent = iconMap[direction] return ( - + ) diff --git a/apps/wallet-mobile/src/components/Warning/Warning.tsx b/apps/wallet-mobile/src/components/Warning/Warning.tsx index 320ee76732..44c27f3558 100644 --- a/apps/wallet-mobile/src/components/Warning/Warning.tsx +++ b/apps/wallet-mobile/src/components/Warning/Warning.tsx @@ -5,14 +5,18 @@ import {StyleSheet, Text, View} from 'react-native' import {Icon} from '../Icon' import {Space} from '../Space/Space' -type Props = {content: ReactNode; iconSize?: number} +type Props = { + content: ReactNode + iconSize?: number + blue?: boolean +} -export const Warning = ({content, iconSize = 30}: Props) => { +export const Warning = ({content, iconSize = 30, blue = false}: Props) => { const {styles, colors} = useStyles() return ( - - + + @@ -29,6 +33,9 @@ const useStyles = () => { padding: 12, borderRadius: 8, }, + blueNotice: { + backgroundColor: color.sys_cyan_100, + }, text: { ...atoms.body_2_md_regular, color: color.gray_max, @@ -37,6 +44,7 @@ const useStyles = () => { const colors = { yellow: color.sys_orange_500, + blue: color.primary_500, } return {colors, styles} diff --git a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx index a56aa691b5..ea9f350d73 100644 --- a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx +++ b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx @@ -22,6 +22,7 @@ import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelected import {useConfirmHWConnectionModal} from '../../common/ConfirmHWConnectionModal' import {usePromptRootKey} from '../../common/hooks' import {useStrings} from '../../common/useStrings' +import {TransactionBodyJSON, TransactionJSON} from './types' export type ReviewTransactionParams = | { @@ -45,7 +46,10 @@ export const ReviewTransaction = () => { const [outputsOpen, setOutputsOpen] = React.useState(true) const [scrollbarShown, setScrollbarShown] = React.useState(false) const strings = useStrings() - const formattedTX = useFormattedTransaction(params.cbor) + const {data} = useTxDetails(params.cbor) + + if (!data) throw new Error('') + const formattedTX = useFormattedTransaction(data.body) const {styles} = useStyles() @@ -163,16 +167,7 @@ const paramsSchema = z.union([ const isParams = createTypeGuardFromSchema(paramsSchema) -type TxDetails = { - body: { - inputs: Array<{transaction_id: string; index: number}> - outputs: Array<{address: string; amount: {coin: number; multiasset: null | Record>}}> - fee: string - ttl: string - } -} - -const getTxDetails = async (cbor: string): Promise => { +const getTxDetails = async (cbor: string): Promise => { const {csl, release} = wrappedCsl() try { const tx = await csl.Transaction.fromHex(cbor) @@ -187,12 +182,11 @@ const useTxDetails = (cbor: string) => { return useQuery({queryFn: () => getTxDetails(cbor), useErrorBoundary: true, queryKey: ['useTxDetails', cbor]}) } -const useFormattedTransaction = (cbor: string) => { +export const useFormattedTransaction = (data: TransactionBodyJSON) => { const {wallet} = useSelectedWallet() - const {data} = useTxDetails(cbor) - const inputs = data?.body.inputs ?? [] - const outputs = data?.body.outputs ?? [] + const inputs = data?.inputs ?? [] + const outputs = data?.outputs ?? [] const getUtxoByTxIdAndIndex = (txId: string, index: number) => { return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) @@ -265,7 +259,7 @@ const useFormattedTransaction = (cbor: string) => { return {assets, address, ownAddress: address != null && isOwnedAddress(address)} }) - const formattedFee = formatAdaWithText(asQuantity(data?.body?.fee ?? '0'), wallet.primaryToken) + const formattedFee = formatAdaWithText(asQuantity(data?.fee ?? '0'), wallet.primaryToken) return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} } diff --git a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts new file mode 100644 index 0000000000..c65b394738 --- /dev/null +++ b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts @@ -0,0 +1,846 @@ +export type AddressJSON = string +export type URLJSON = string + +export interface AnchorJSON { + anchor_data_hash: string + anchor_url: URLJSON +} +export type AnchorDataHashJSON = string +export type AssetNameJSON = string +export type AssetNamesJSON = string[] +export interface AssetsJSON { + [k: string]: string +} +export type NativeScriptJSON = + | { + ScriptPubkey: ScriptPubkeyJSON + } + | { + ScriptAll: ScriptAllJSON + } + | { + ScriptAny: ScriptAnyJSON + } + | { + ScriptNOfK: ScriptNOfKJSON + } + | { + TimelockStart: TimelockStartJSON + } + | { + TimelockExpiry: TimelockExpiryJSON + } +export type NativeScriptsJSON = NativeScriptJSON[] +export type PlutusScriptsJSON = string[] + +export interface AuxiliaryDataJSON { + metadata?: { + [k: string]: string + } | null + native_scripts?: NativeScriptsJSON | null + plutus_scripts?: PlutusScriptsJSON | null + prefer_alonzo_format: boolean +} +export interface ScriptPubkeyJSON { + addr_keyhash: string +} +export interface ScriptAllJSON { + native_scripts: NativeScriptsJSON +} +export interface ScriptAnyJSON { + native_scripts: NativeScriptsJSON +} +export interface ScriptNOfKJSON { + n: number + native_scripts: NativeScriptsJSON +} +export interface TimelockStartJSON { + slot: string +} +export interface TimelockExpiryJSON { + slot: string +} +export type AuxiliaryDataHashJSON = string +export interface AuxiliaryDataSetJSON { + [k: string]: AuxiliaryDataJSON +} +export type BigIntJSON = string +export type BigNumJSON = string +export type VkeyJSON = string +export type HeaderLeaderCertEnumJSON = + | { + /** + * @minItems 2 + * @maxItems 2 + */ + NonceAndLeader: [VRFCertJSON, VRFCertJSON] + } + | { + VrfResult: VRFCertJSON + } +export type CertificateJSON = + | { + StakeRegistration: StakeRegistrationJSON + } + | { + StakeDeregistration: StakeDeregistrationJSON + } + | { + StakeDelegation: StakeDelegationJSON + } + | { + PoolRegistration: PoolRegistrationJSON + } + | { + PoolRetirement: PoolRetirementJSON + } + | { + GenesisKeyDelegation: GenesisKeyDelegationJSON + } + | { + MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCertJSON + } + | { + CommitteeHotAuth: CommitteeHotAuthJSON + } + | { + CommitteeColdResign: CommitteeColdResignJSON + } + | { + DRepDeregistration: DRepDeregistrationJSON + } + | { + DRepRegistration: DRepRegistrationJSON + } + | { + DRepUpdate: DRepUpdateJSON + } + | { + StakeAndVoteDelegation: StakeAndVoteDelegationJSON + } + | { + StakeRegistrationAndDelegation: StakeRegistrationAndDelegationJSON + } + | { + StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegationJSON + } + | { + VoteDelegation: VoteDelegationJSON + } + | { + VoteRegistrationAndDelegation: VoteRegistrationAndDelegationJSON + } +export type CredTypeJSON = + | { + Key: string + } + | { + Script: string + } +export type RelayJSON = + | { + SingleHostAddr: SingleHostAddrJSON + } + | { + SingleHostName: SingleHostNameJSON + } + | { + MultiHostName: MultiHostNameJSON + } +/** + * @minItems 4 + * @maxItems 4 + */ +export type Ipv4JSON = [number, number, number, number] +/** + * @minItems 16 + * @maxItems 16 + */ +export type Ipv6JSON = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, +] +export type DNSRecordAorAAAAJSON = string +export type DNSRecordSRVJSON = string +export type RelaysJSON = RelayJSON[] +export type MIRPotJSON = 'Reserves' | 'Treasury' +export type MIREnumJSON = + | { + ToOtherPot: string + } + | { + ToStakeCredentials: StakeToCoinJSON[] + } +export type DRepJSON = + | ('AlwaysAbstain' | 'AlwaysNoConfidence') + | { + KeyHash: string + } + | { + ScriptHash: string + } +export type DataOptionJSON = + | { + DataHash: string + } + | { + Data: string + } +export type ScriptRefJSON = + | { + NativeScript: NativeScriptJSON + } + | { + PlutusScript: string + } +export type MintJSON = [string, MintAssetsJSON][] +export type NetworkIdJSON = 'Testnet' | 'Mainnet' +export type TransactionOutputsJSON = TransactionOutputJSON[] +export type CostModelJSON = string[] +export type VoterJSON = + | { + ConstitutionalCommitteeHotCred: CredTypeJSON + } + | { + DRep: CredTypeJSON + } + | { + StakingPool: string + } +export type VoteKindJSON = 'No' | 'Yes' | 'Abstain' +export type GovernanceActionJSON = + | { + ParameterChangeAction: ParameterChangeActionJSON + } + | { + HardForkInitiationAction: HardForkInitiationActionJSON + } + | { + TreasuryWithdrawalsAction: TreasuryWithdrawalsActionJSON + } + | { + NoConfidenceAction: NoConfidenceActionJSON + } + | { + UpdateCommitteeAction: UpdateCommitteeActionJSON + } + | { + NewConstitutionAction: NewConstitutionActionJSON + } + | { + InfoAction: InfoActionJSON + } +/** + * @minItems 0 + * @maxItems 0 + */ +export type InfoActionJSON = [] +export type TransactionBodiesJSON = TransactionBodyJSON[] +export type RedeemerTagJSON = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' +export type TransactionWitnessSetsJSON = TransactionWitnessSetJSON[] + +export interface BlockJSON { + auxiliary_data_set: { + [k: string]: AuxiliaryDataJSON + } + header: HeaderJSON + invalid_transactions: number[] + transaction_bodies: TransactionBodiesJSON + transaction_witness_sets: TransactionWitnessSetsJSON +} +export interface HeaderJSON { + body_signature: string + header_body: HeaderBodyJSON +} +export interface HeaderBodyJSON { + block_body_hash: string + block_body_size: number + block_number: number + issuer_vkey: VkeyJSON + leader_cert: HeaderLeaderCertEnumJSON + operational_cert: OperationalCertJSON + prev_hash?: string | null + protocol_version: ProtocolVersionJSON + slot: string + vrf_vkey: string +} +export interface VRFCertJSON { + output: number[] + proof: number[] +} +export interface OperationalCertJSON { + hot_vkey: string + kes_period: number + sequence_number: number + sigma: string +} +export interface ProtocolVersionJSON { + major: number + minor: number +} +export interface TransactionBodyJSON { + auxiliary_data_hash?: string | null + certs?: CertificateJSON[] | null + collateral?: TransactionInputJSON[] | null + collateral_return?: TransactionOutputJSON | null + current_treasury_value?: string | null + donation?: string | null + fee: string + inputs: TransactionInputJSON[] + mint?: MintJSON | null + network_id?: NetworkIdJSON | null + outputs: TransactionOutputsJSON + reference_inputs?: TransactionInputJSON[] | null + required_signers?: string[] | null + script_data_hash?: string | null + total_collateral?: string | null + ttl?: string | null + update?: UpdateJSON | null + validity_start_interval?: string | null + voting_procedures?: VoterVotesJSON[] | null + voting_proposals?: VotingProposalJSON[] | null + withdrawals?: { + [k: string]: string + } | null +} +export interface StakeRegistrationJSON { + coin?: string | null + stake_credential: CredTypeJSON +} +export interface StakeDeregistrationJSON { + coin?: string | null + stake_credential: CredTypeJSON +} +export interface StakeDelegationJSON { + pool_keyhash: string + stake_credential: CredTypeJSON +} +export interface PoolRegistrationJSON { + pool_params: PoolParamsJSON +} +export interface PoolParamsJSON { + cost: string + margin: UnitIntervalJSON + operator: string + pledge: string + pool_metadata?: PoolMetadataJSON | null + pool_owners: string[] + relays: RelaysJSON + reward_account: string + vrf_keyhash: string +} +export interface UnitIntervalJSON { + denominator: string + numerator: string +} +export interface PoolMetadataJSON { + pool_metadata_hash: string + url: URLJSON +} +export interface SingleHostAddrJSON { + ipv4?: Ipv4JSON | null + ipv6?: Ipv6JSON | null + port?: number | null +} +export interface SingleHostNameJSON { + dns_name: DNSRecordAorAAAAJSON + port?: number | null +} +export interface MultiHostNameJSON { + dns_name: DNSRecordSRVJSON +} +export interface PoolRetirementJSON { + epoch: number + pool_keyhash: string +} +export interface GenesisKeyDelegationJSON { + genesis_delegate_hash: string + genesishash: string + vrf_keyhash: string +} +export interface MoveInstantaneousRewardsCertJSON { + move_instantaneous_reward: MoveInstantaneousRewardJSON +} +export interface MoveInstantaneousRewardJSON { + pot: MIRPotJSON + variant: MIREnumJSON +} +export interface StakeToCoinJSON { + amount: string + stake_cred: CredTypeJSON +} +export interface CommitteeHotAuthJSON { + committee_cold_credential: CredTypeJSON + committee_hot_credential: CredTypeJSON +} +export interface CommitteeColdResignJSON { + anchor?: AnchorJSON | null + committee_cold_credential: CredTypeJSON +} +export interface DRepDeregistrationJSON { + coin: string + voting_credential: CredTypeJSON +} +export interface DRepRegistrationJSON { + anchor?: AnchorJSON | null + coin: string + voting_credential: CredTypeJSON +} +export interface DRepUpdateJSON { + anchor?: AnchorJSON | null + voting_credential: CredTypeJSON +} +export interface StakeAndVoteDelegationJSON { + drep: DRepJSON + pool_keyhash: string + stake_credential: CredTypeJSON +} +export interface StakeRegistrationAndDelegationJSON { + coin: string + pool_keyhash: string + stake_credential: CredTypeJSON +} +export interface StakeVoteRegistrationAndDelegationJSON { + coin: string + drep: DRepJSON + pool_keyhash: string + stake_credential: CredTypeJSON +} +export interface VoteDelegationJSON { + drep: DRepJSON + stake_credential: CredTypeJSON +} +export interface VoteRegistrationAndDelegationJSON { + coin: string + drep: DRepJSON + stake_credential: CredTypeJSON +} +export interface TransactionInputJSON { + index: number + transaction_id: string +} +export interface TransactionOutputJSON { + address: string + amount: ValueJSON + plutus_data?: DataOptionJSON | null + script_ref?: ScriptRefJSON | null +} +export interface ValueJSON { + coin: string + multiasset?: MultiAssetJSON | null +} +export interface MultiAssetJSON { + [k: string]: AssetsJSON +} +export interface MintAssetsJSON { + [k: string]: string +} +export interface UpdateJSON { + epoch: number + proposed_protocol_parameter_updates: { + [k: string]: ProtocolParamUpdateJSON + } +} +export interface ProtocolParamUpdateJSON { + ada_per_utxo_byte?: string | null + collateral_percentage?: number | null + committee_term_limit?: number | null + cost_models?: CostmdlsJSON | null + d?: UnitIntervalJSON | null + drep_deposit?: string | null + drep_inactivity_period?: number | null + drep_voting_thresholds?: DRepVotingThresholdsJSON | null + execution_costs?: ExUnitPricesJSON | null + expansion_rate?: UnitIntervalJSON | null + extra_entropy?: NonceJSON | null + governance_action_deposit?: string | null + governance_action_validity_period?: number | null + key_deposit?: string | null + max_block_body_size?: number | null + max_block_ex_units?: ExUnitsJSON | null + max_block_header_size?: number | null + max_collateral_inputs?: number | null + max_epoch?: number | null + max_tx_ex_units?: ExUnitsJSON | null + max_tx_size?: number | null + max_value_size?: number | null + min_committee_size?: number | null + min_pool_cost?: string | null + minfee_a?: string | null + minfee_b?: string | null + n_opt?: number | null + pool_deposit?: string | null + pool_pledge_influence?: UnitIntervalJSON | null + pool_voting_thresholds?: PoolVotingThresholdsJSON | null + protocol_version?: ProtocolVersionJSON | null + ref_script_coins_per_byte?: UnitIntervalJSON | null + treasury_growth_rate?: UnitIntervalJSON | null +} +export interface CostmdlsJSON { + [k: string]: CostModelJSON +} +export interface DRepVotingThresholdsJSON { + committee_no_confidence: UnitIntervalJSON + committee_normal: UnitIntervalJSON + hard_fork_initiation: UnitIntervalJSON + motion_no_confidence: UnitIntervalJSON + pp_economic_group: UnitIntervalJSON + pp_governance_group: UnitIntervalJSON + pp_network_group: UnitIntervalJSON + pp_technical_group: UnitIntervalJSON + treasury_withdrawal: UnitIntervalJSON + update_constitution: UnitIntervalJSON +} +export interface ExUnitPricesJSON { + mem_price: UnitIntervalJSON + step_price: UnitIntervalJSON +} +export interface NonceJSON { + /** + * @minItems 32 + * @maxItems 32 + */ + hash?: + | [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ] + | null +} +export interface ExUnitsJSON { + mem: string + steps: string +} +export interface PoolVotingThresholdsJSON { + committee_no_confidence: UnitIntervalJSON + committee_normal: UnitIntervalJSON + hard_fork_initiation: UnitIntervalJSON + motion_no_confidence: UnitIntervalJSON + security_relevant_threshold: UnitIntervalJSON +} +export interface VoterVotesJSON { + voter: VoterJSON + votes: VoteJSON[] +} +export interface VoteJSON { + action_id: GovernanceActionIdJSON + voting_procedure: VotingProcedureJSON +} +export interface GovernanceActionIdJSON { + index: number + transaction_id: string +} +export interface VotingProcedureJSON { + anchor?: AnchorJSON | null + vote: VoteKindJSON +} +export interface VotingProposalJSON { + anchor: AnchorJSON + deposit: string + governance_action: GovernanceActionJSON + reward_account: string +} +export interface ParameterChangeActionJSON { + gov_action_id?: GovernanceActionIdJSON | null + policy_hash?: string | null + protocol_param_updates: ProtocolParamUpdateJSON +} +export interface HardForkInitiationActionJSON { + gov_action_id?: GovernanceActionIdJSON | null + protocol_version: ProtocolVersionJSON +} +export interface TreasuryWithdrawalsActionJSON { + policy_hash?: string | null + withdrawals: TreasuryWithdrawalsJSON +} +export interface TreasuryWithdrawalsJSON { + [k: string]: string +} +export interface NoConfidenceActionJSON { + gov_action_id?: GovernanceActionIdJSON | null +} +export interface UpdateCommitteeActionJSON { + committee: CommitteeJSON + gov_action_id?: GovernanceActionIdJSON | null + members_to_remove: CredTypeJSON[] +} +export interface CommitteeJSON { + members: CommitteeMemberJSON[] + quorum_threshold: UnitIntervalJSON +} +export interface CommitteeMemberJSON { + stake_credential: CredTypeJSON + term_limit: number +} +export interface NewConstitutionActionJSON { + constitution: ConstitutionJSON + gov_action_id?: GovernanceActionIdJSON | null +} +export interface ConstitutionJSON { + anchor: AnchorJSON + script_hash?: string | null +} +export interface TransactionWitnessSetJSON { + bootstraps?: BootstrapWitnessJSON[] | null + native_scripts?: NativeScriptsJSON | null + plutus_data?: PlutusListJSON | null + plutus_scripts?: PlutusScriptsJSON | null + redeemers?: RedeemerJSON[] | null + vkeys?: VkeywitnessJSON[] | null +} +export interface BootstrapWitnessJSON { + attributes: number[] + chain_code: number[] + signature: string + vkey: VkeyJSON +} +export interface PlutusListJSON { + definite_encoding?: boolean | null + elems: string[] +} +export interface RedeemerJSON { + data: string + ex_units: ExUnitsJSON + index: string + tag: RedeemerTagJSON +} +export interface VkeywitnessJSON { + signature: string + vkey: VkeyJSON +} +export type BlockHashJSON = string +export type BootstrapWitnessesJSON = BootstrapWitnessJSON[] + +export type CertificateEnumJSON = + | { + StakeRegistration: StakeRegistrationJSON + } + | { + StakeDeregistration: StakeDeregistrationJSON + } + | { + StakeDelegation: StakeDelegationJSON + } + | { + PoolRegistration: PoolRegistrationJSON + } + | { + PoolRetirement: PoolRetirementJSON + } + | { + GenesisKeyDelegation: GenesisKeyDelegationJSON + } + | { + MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCertJSON + } + | { + CommitteeHotAuth: CommitteeHotAuthJSON + } + | { + CommitteeColdResign: CommitteeColdResignJSON + } + | { + DRepDeregistration: DRepDeregistrationJSON + } + | { + DRepRegistration: DRepRegistrationJSON + } + | { + DRepUpdate: DRepUpdateJSON + } + | { + StakeAndVoteDelegation: StakeAndVoteDelegationJSON + } + | { + StakeRegistrationAndDelegation: StakeRegistrationAndDelegationJSON + } + | { + StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegationJSON + } + | { + VoteDelegation: VoteDelegationJSON + } + | { + VoteRegistrationAndDelegation: VoteRegistrationAndDelegationJSON + } +export type CertificatesJSON = CertificateJSON[] + +export type CredentialJSON = CredTypeJSON +export type CredentialsJSON = CredTypeJSON[] +export type DRepEnumJSON = + | ('AlwaysAbstain' | 'AlwaysNoConfidence') + | { + KeyHash: string + } + | { + ScriptHash: string + } +export type DataHashJSON = string +export type Ed25519KeyHashJSON = string +export type Ed25519KeyHashesJSON = string[] +export type Ed25519SignatureJSON = string +export interface GeneralTransactionMetadataJSON { + [k: string]: string +} +export type GenesisDelegateHashJSON = string +export type GenesisHashJSON = string +export type GenesisHashesJSON = string[] +export type GovernanceActionEnumJSON = + | { + ParameterChangeAction: ParameterChangeActionJSON + } + | { + HardForkInitiationAction: HardForkInitiationActionJSON + } + | { + TreasuryWithdrawalsAction: TreasuryWithdrawalsActionJSON + } + | { + NoConfidenceAction: NoConfidenceActionJSON + } + | { + UpdateCommitteeAction: UpdateCommitteeActionJSON + } + | { + NewConstitutionAction: NewConstitutionActionJSON + } + | { + InfoAction: InfoActionJSON + } +export type GovernanceActionIdsJSON = GovernanceActionIdJSON[] + +export type IntJSON = string +/** + * @minItems 4 + * @maxItems 4 + */ +export type KESVKeyJSON = string +export type LanguageJSON = LanguageKindJSON +export type LanguageKindJSON = 'PlutusV1' | 'PlutusV2' | 'PlutusV3' +export type LanguagesJSON = LanguageJSON[] +export type MIRToStakeCredentialsJSON = StakeToCoinJSON[] + +export type MintsAssetsJSON = MintAssetsJSON[] + +export type NetworkIdKindJSON = 'Testnet' | 'Mainnet' +export type PlutusScriptJSON = string +export type PoolMetadataHashJSON = string +export interface ProposedProtocolParameterUpdatesJSON { + [k: string]: ProtocolParamUpdateJSON +} +export type PublicKeyJSON = string +export type RedeemerTagKindJSON = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' +export type RedeemersJSON = RedeemerJSON[] + +export type RelayEnumJSON = + | { + SingleHostAddr: SingleHostAddrJSON + } + | { + SingleHostName: SingleHostNameJSON + } + | { + MultiHostName: MultiHostNameJSON + } +/** + * @minItems 4 + * @maxItems 4 + */ +export type RewardAddressJSON = string +export type RewardAddressesJSON = string[] +export type ScriptDataHashJSON = string +export type ScriptHashJSON = string +export type ScriptHashesJSON = string[] +export type ScriptRefEnumJSON = + | { + NativeScript: NativeScriptJSON + } + | { + PlutusScript: string + } +export interface TransactionJSON { + auxiliary_data?: AuxiliaryDataJSON | null + body: TransactionBodyJSON + is_valid: boolean + witness_set: TransactionWitnessSetJSON +} +export type TransactionHashJSON = string +export type TransactionInputsJSON = TransactionInputJSON[] + +export type TransactionMetadatumJSON = string +export interface TransactionUnspentOutputJSON { + input: TransactionInputJSON + output: TransactionOutputJSON +} +export type TransactionUnspentOutputsJSON = TransactionUnspentOutputJSON[] + +export type VRFKeyHashJSON = string +export type VRFVKeyJSON = string +export interface VersionedBlockJSON { + block: BlockJSON + era_code: number +} +export type VkeywitnessesJSON = VkeywitnessJSON[] + +export type VoterEnumJSON = + | { + ConstitutionalCommitteeHotCred: CredTypeJSON + } + | { + DRep: CredTypeJSON + } + | { + StakingPool: string + } +export type VotersJSON = VoterJSON[] +export type VotingProceduresJSON = VoterVotesJSON[] + +export type VotingProposalsJSON = VotingProposalJSON[] + +export interface WithdrawalsJSON { + [k: string]: string +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx new file mode 100644 index 0000000000..c2c50d55b4 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx @@ -0,0 +1,39 @@ +import {createStackNavigator} from '@react-navigation/stack' +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet} from 'react-native' + +import {KeyboardAvoidingView} from '../../components' +import {defaultStackNavigationOptions, ReviewTransactionRoutes} from '../../kernel/navigation' +import {ReviewTransactionScreen} from './useCases/ReviewTransactionScreen/ReviewTransactionScreen' + +const Stack = createStackNavigator() + +export const ReviewTransactionNavigator = () => { + const {atoms, color} = useTheme() + const styles = useStyles() + + return ( + + + + + + ) +} + +const useStyles = () => { + const {color, atoms} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.flex_1, + backgroundColor: color.bg_color_max, + }, + }) + return styles +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx new file mode 100644 index 0000000000..c53f95c9cd --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx @@ -0,0 +1,21 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, View} from 'react-native' + +export const Divider = () => { + const {styles} = useStyles() + return +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + divider: { + height: 1, + ...atoms.align_stretch, + backgroundColor: color.gray_200, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx new file mode 100644 index 0000000000..8f185bff89 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx @@ -0,0 +1,122 @@ +import {isNonNullable} from '@yoroi/common' +import * as _ from 'lodash' + +import {useTokenInfos} from '../../../yoroi-wallets/hooks' +import {asQuantity} from '../../../yoroi-wallets/utils' +import {formatAdaWithText, formatTokenWithText} from '../../../yoroi-wallets/utils/format' +import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' +import {TransactionBody} from './types' + +export const useFormattedTransaction = (data: TransactionBody) => { + const {wallet} = useSelectedWallet() + + const inputs = data?.inputs ?? [] + const outputs = data?.outputs ?? [] + + const getUtxoByTxIdAndIndex = (txId: string, index: number) => { + return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) + } + + const isOwnedAddress = (bech32Address: string) => { + return wallet.internalAddresses.includes(bech32Address) || wallet.externalAddresses.includes(bech32Address) + } + + const inputTokenIds = inputs.flatMap((i) => { + const receiveUTxO = getUtxoByTxIdAndIndex(i.transaction_id, i.index) + return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}`) ?? [] + }) + + const outputTokenIds = outputs.flatMap((o) => { + if (!o.amount.multiasset) return [] + const policyIds = Object.keys(o.amount.multiasset) + const tokenIds = policyIds.flatMap((policyId) => { + const assetIds = Object.keys(o.amount.multiasset?.[policyId] ?? {}) + return assetIds.map((assetId) => `${policyId}.${assetId}`) + }) + return tokenIds + }) + + const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) + const tokenInfos = useTokenInfos({wallet, tokenIds}) + + const formattedInputs = inputs.map((input) => { + const receiveUTxO = getUtxoByTxIdAndIndex(input.transaction_id, input.index) + const address = receiveUTxO?.receiver + const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null + const coinText = coin != null ? formatAdaWithText(coin, wallet.primaryToken) : null + + const primaryAssets = + coinText != null + ? [ + { + label: coinText, + quantity: coin, + isPrimary: true, + }, + ] + : [] + + const multiAssets = + receiveUTxO?.assets + .map((a) => { + const tokenInfo = tokenInfos.find((t) => t.id === a.assetId) + if (!tokenInfo) return null + const quantity = asQuantity(a.amount) + return { + label: formatTokenWithText(quantity, tokenInfo), + quantity, + isPrimary: false, + } + }) + .filter(Boolean) ?? [] + + return { + assets: [...primaryAssets, ...multiAssets].filter(isNonNullable), + address, + ownAddress: address != null && isOwnedAddress(address), + txIndex: input.index, + txHash: input.transaction_id, + } + }) + + const formattedOutputs = outputs.map((output) => { + const address = output.address + const coin = asQuantity(output.amount.coin) + const coinText = formatAdaWithText(coin, wallet.primaryToken) + + const primaryAssets = + coinText != null + ? [ + { + label: coinText, + quantity: coin, + isPrimary: true, + }, + ] + : [] + + const multiAssets = output.amount.multiasset + ? Object.entries(output.amount.multiasset).map(([policyId, assets]) => { + return Object.entries(assets).map(([assetId, amount]) => { + const tokenInfo = tokenInfos.find((t) => t.id === `${policyId}.${assetId}`) + if (tokenInfo == null) return null + const quantity = asQuantity(amount) + return { + label: formatTokenWithText(quantity, tokenInfo), + quantity, + isPrimary: false, + } + }) + }) + : [] + + const assets = [...primaryAssets, ...multiAssets.flat()].filter(isNonNullable) + return {assets, address, ownAddress: address != null && isOwnedAddress(address)} + }) + + const formattedFee = formatAdaWithText(asQuantity(data?.fee ?? '0'), wallet.primaryToken) + + return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} +} + +export type formattedTx = ReturnType diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts new file mode 100644 index 0000000000..311b526bcf --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts @@ -0,0 +1,148 @@ +import {TransactionBody} from './types' + +export const adaTransactionSingleReceiver: TransactionBody = { + inputs: [ + { + transaction_id: '46fe71d85a733d970fe7bb8e6586624823803936d18c7e14601713d05b5b287a', + index: 0, + }, + { + transaction_id: '9638640d421875f068d10a0125023601bbd7e83e7f17b721c9c06c97cc29ff66', + index: 1, + }, + ], + outputs: [ + { + address: + 'addr1qyf4x8lvcyrwcxzkyz3lykyzfu7s7x307dlafgsu89qzge8lfl229ahk888cgakug24y86qtduvn065c3gw7dg5002cqdskm74', + amount: { + coin: '12000000', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '23464562', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + ], + fee: '174345', + ttl: '220373661', + certs: null, + withdrawals: null, + update: null, + auxiliary_data_hash: null, + validity_start_interval: null, + mint: null, + script_data_hash: null, + collateral: null, + required_signers: null, + network_id: null, + collateral_return: null, + total_collateral: null, + reference_inputs: null, + voting_procedures: null, + voting_proposals: null, + donation: null, + current_treasury_value: null, +} + +export const multiAssetsOneReceiver: TransactionBody = { + inputs: [ + { + transaction_id: '46fe71d85a733d970fe7bb8e6586624823803936d18c7e14601713d05b5b287a', + index: 0, + }, + { + transaction_id: 'bddd3e0b43b9b93f6d49190a9d4d55c3cd28e3d270b0f1bbc0f83b8ecc3e373a', + index: 1, + }, + ], + outputs: [ + { + address: + 'addr1qyf4x8lvcyrwcxzkyz3lykyzfu7s7x307dlafgsu89qzge8lfl229ahk888cgakug24y86qtduvn065c3gw7dg5002cqdskm74', + amount: { + coin: '10000000', + multiasset: { + cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { + '43415354': '10', + }, + f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a: { + '000de1406a6176696275656e6f': '1', + }, + }, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '2228270', + multiasset: { + '2441ab3351c3b80213a98f4e09ddcf7dabe4879c3c94cc4e7205cb63': { + '46495245': '2531', + }, + '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f': { + '534e454b': '204', + }, + '4cb48d60d1f7823d1307c61b9ecf472ff78cf22d1ccc5786d59461f8': { + '4144414d4f4f4e': '4983996', + }, + a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235: { + '484f534b59': '115930085', + }, + cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { + '43415354': '4498', + }, + e0c4c2d7c4a0ed2cf786753fd845dee82c45512cee03e92adfd3fb8d: { + '6a6176696275656e6f2e616461': '1', + }, + fc411f546d01e88a822200243769bbc1e1fbdde8fa0f6c5179934edb: { + '6a6176696275656e6f': '1', + }, + }, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '2300311', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + ], + fee: '189349', + ttl: '220396208', + certs: null, + withdrawals: null, + update: null, + auxiliary_data_hash: null, + validity_start_interval: null, + mint: null, + script_data_hash: null, + collateral: null, + required_signers: null, + network_id: null, + collateral_return: null, + total_collateral: null, + reference_inputs: null, + voting_procedures: null, + voting_proposals: null, + donation: null, + current_treasury_value: null, +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts b/apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts new file mode 100644 index 0000000000..0c6847d747 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts @@ -0,0 +1,855 @@ +export type TransactionDetails = { + id: string + walletPlate: React.ReactNode + walletName: string + createdBy: string | null + fee: string + txBody: TransactionBody +} + +export type Address = string +export type URL = string + +export interface Anchor { + anchor_data_hash: string + anchor_url: URL +} +export type AnchorDataHash = string +export type AssetName = string +export type AssetNames = string[] +export interface Assets { + [k: string]: string +} +export type NativeScript = + | { + ScriptPubkey: ScriptPubkey + } + | { + ScriptAll: ScriptAll + } + | { + ScriptAny: ScriptAny + } + | { + ScriptNOfK: ScriptNOfK + } + | { + TimelockStart: TimelockStart + } + | { + TimelockExpiry: TimelockExpiry + } +export type NativeScripts = NativeScript[] +export type PlutusScripts = string[] + +export interface AuxiliaryData { + metadata?: { + [k: string]: string + } | null + native_scripts?: NativeScripts | null + plutus_scripts?: PlutusScripts | null + prefer_alonzo_format: boolean +} +export interface ScriptPubkey { + addr_keyhash: string +} +export interface ScriptAll { + native_scripts: NativeScripts +} +export interface ScriptAny { + native_scripts: NativeScripts +} +export interface ScriptNOfK { + n: number + native_scripts: NativeScripts +} +export interface TimelockStart { + slot: string +} +export interface TimelockExpiry { + slot: string +} +export type AuxiliaryDataHash = string +export interface AuxiliaryDataSet { + [k: string]: AuxiliaryData +} +export type BigInt = string +export type BigNum = string +export type Vkey = string +export type HeaderLeaderCertEnum = + | { + /** + * @minItems 2 + * @maxItems 2 + */ + NonceAndLeader: [VRFCert, VRFCert] + } + | { + VrfResult: VRFCert + } +export type Certificate = + | { + StakeRegistration: StakeRegistration + } + | { + StakeDeregistration: StakeDeregistration + } + | { + StakeDelegation: StakeDelegation + } + | { + PoolRegistration: PoolRegistration + } + | { + PoolRetirement: PoolRetirement + } + | { + GenesisKeyDelegation: GenesisKeyDelegation + } + | { + MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCert + } + | { + CommitteeHotAuth: CommitteeHotAuth + } + | { + CommitteeColdResign: CommitteeColdResign + } + | { + DRepDeregistration: DRepDeregistration + } + | { + DRepRegistration: DRepRegistration + } + | { + DRepUpdate: DRepUpdate + } + | { + StakeAndVoteDelegation: StakeAndVoteDelegation + } + | { + StakeRegistrationAndDelegation: StakeRegistrationAndDelegation + } + | { + StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegation + } + | { + VoteDelegation: VoteDelegation + } + | { + VoteRegistrationAndDelegation: VoteRegistrationAndDelegation + } +export type CredType = + | { + Key: string + } + | { + Script: string + } +export type Relay = + | { + SingleHostAddr: SingleHostAddr + } + | { + SingleHostName: SingleHostName + } + | { + MultiHostName: MultiHostName + } +/** + * @minItems 4 + * @maxItems 4 + */ +export type Ipv4 = [number, number, number, number] +/** + * @minItems 16 + * @maxItems 16 + */ +export type Ipv6 = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, +] +export type DNSRecordAorAAAA = string +export type DNSRecordSRV = string +export type Relays = Relay[] +export type MIRPot = 'Reserves' | 'Treasury' +export type MIREnum = + | { + ToOtherPot: string + } + | { + ToStakeCredentials: StakeToCoin[] + } +export type DRep = + | ('AlwaysAbstain' | 'AlwaysNoConfidence') + | { + KeyHash: string + } + | { + ScriptHash: string + } +export type DataOption = + | { + DataHash: string + } + | { + Data: string + } +export type ScriptRef = + | { + NativeScript: NativeScript + } + | { + PlutusScript: string + } +export type Mint = [string, MintAssets][] +export type NetworkId = 'Testnet' | 'Mainnet' +export type TransactionOutputs = TransactionOutput[] +export type CostModel = string[] +export type Voter = + | { + ConstitutionalCommitteeHotCred: CredType + } + | { + DRep: CredType + } + | { + StakingPool: string + } +export type VoteKind = 'No' | 'Yes' | 'Abstain' +export type GovernanceAction = + | { + ParameterChangeAction: ParameterChangeAction + } + | { + HardForkInitiationAction: HardForkInitiationAction + } + | { + TreasuryWithdrawalsAction: TreasuryWithdrawalsAction + } + | { + NoConfidenceAction: NoConfidenceAction + } + | { + UpdateCommitteeAction: UpdateCommitteeAction + } + | { + NewConstitutionAction: NewConstitutionAction + } + | { + InfoAction: InfoAction + } +/** + * @minItems 0 + * @maxItems 0 + */ +export type InfoAction = [] +export type TransactionBodies = TransactionBody[] +export type RedeemerTag = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' +export type TransactionWitnessSets = TransactionWitnessSet[] + +export interface Block { + auxiliary_data_set: { + [k: string]: AuxiliaryData + } + header: Header + invalid_transactions: number[] + transaction_bodies: TransactionBodies + transaction_witness_sets: TransactionWitnessSets +} +export interface Header { + body_signature: string + header_body: HeaderBody +} +export interface HeaderBody { + block_body_hash: string + block_body_size: number + block_number: number + issuer_vkey: Vkey + leader_cert: HeaderLeaderCertEnum + operational_cert: OperationalCert + prev_hash?: string | null + protocol_version: ProtocolVersion + slot: string + vrf_vkey: string +} +export interface VRFCert { + output: number[] + proof: number[] +} +export interface OperationalCert { + hot_vkey: string + kes_period: number + sequence_number: number + sigma: string +} +export interface ProtocolVersion { + major: number + minor: number +} +export interface TransactionBody { + auxiliary_data_hash?: string | null + certs?: Certificate[] | null + collateral?: TransactionInput[] | null + collateral_return?: TransactionOutput | null + current_treasury_value?: string | null + donation?: string | null + fee: string + inputs: TransactionInput[] + mint?: Mint | null + network_id?: NetworkId | null + outputs: TransactionOutputs + reference_inputs?: TransactionInput[] | null + required_signers?: string[] | null + script_data_hash?: string | null + total_collateral?: string | null + ttl?: string | null + update?: Update | null + validity_start_interval?: string | null + voting_procedures?: VoterVotes[] | null + voting_proposals?: VotingProposal[] | null + withdrawals?: { + [k: string]: string + } | null +} +export interface StakeRegistration { + coin?: string | null + stake_credential: CredType +} +export interface StakeDeregistration { + coin?: string | null + stake_credential: CredType +} +export interface StakeDelegation { + pool_keyhash: string + stake_credential: CredType +} +export interface PoolRegistration { + pool_params: PoolParams +} +export interface PoolParams { + cost: string + margin: UnitInterval + operator: string + pledge: string + pool_metadata?: PoolMetadata | null + pool_owners: string[] + relays: Relays + reward_account: string + vrf_keyhash: string +} +export interface UnitInterval { + denominator: string + numerator: string +} +export interface PoolMetadata { + pool_metadata_hash: string + url: URL +} +export interface SingleHostAddr { + ipv4?: Ipv4 | null + ipv6?: Ipv6 | null + port?: number | null +} +export interface SingleHostName { + dns_name: DNSRecordAorAAAA + port?: number | null +} +export interface MultiHostName { + dns_name: DNSRecordSRV +} +export interface PoolRetirement { + epoch: number + pool_keyhash: string +} +export interface GenesisKeyDelegation { + genesis_delegate_hash: string + genesishash: string + vrf_keyhash: string +} +export interface MoveInstantaneousRewardsCert { + move_instantaneous_reward: MoveInstantaneousReward +} +export interface MoveInstantaneousReward { + pot: MIRPot + variant: MIREnum +} +export interface StakeToCoin { + amount: string + stake_cred: CredType +} +export interface CommitteeHotAuth { + committee_cold_credential: CredType + committee_hot_credential: CredType +} +export interface CommitteeColdResign { + anchor?: Anchor | null + committee_cold_credential: CredType +} +export interface DRepDeregistration { + coin: string + voting_credential: CredType +} +export interface DRepRegistration { + anchor?: Anchor | null + coin: string + voting_credential: CredType +} +export interface DRepUpdate { + anchor?: Anchor | null + voting_credential: CredType +} +export interface StakeAndVoteDelegation { + drep: DRep + pool_keyhash: string + stake_credential: CredType +} +export interface StakeRegistrationAndDelegation { + coin: string + pool_keyhash: string + stake_credential: CredType +} +export interface StakeVoteRegistrationAndDelegation { + coin: string + drep: DRep + pool_keyhash: string + stake_credential: CredType +} +export interface VoteDelegation { + drep: DRep + stake_credential: CredType +} +export interface VoteRegistrationAndDelegation { + coin: string + drep: DRep + stake_credential: CredType +} +export interface TransactionInput { + index: number + transaction_id: string +} +export interface TransactionOutput { + address: string + amount: Value + plutus_data?: DataOption | null + script_ref?: ScriptRef | null +} +export interface Value { + coin: string + multiasset?: MultiAsset | null +} +export interface MultiAsset { + [k: string]: Assets +} +export interface MintAssets { + [k: string]: string +} +export interface Update { + epoch: number + proposed_protocol_parameter_updates: { + [k: string]: ProtocolParamUpdate + } +} +export interface ProtocolParamUpdate { + ada_per_utxo_byte?: string | null + collateral_percentage?: number | null + committee_term_limit?: number | null + cost_models?: Costmdls | null + d?: UnitInterval | null + drep_deposit?: string | null + drep_inactivity_period?: number | null + drep_voting_thresholds?: DRepVotingThresholds | null + execution_costs?: ExUnitPrices | null + expansion_rate?: UnitInterval | null + extra_entropy?: Nonce | null + governance_action_deposit?: string | null + governance_action_validity_period?: number | null + key_deposit?: string | null + max_block_body_size?: number | null + max_block_ex_units?: ExUnits | null + max_block_header_size?: number | null + max_collateral_inputs?: number | null + max_epoch?: number | null + max_tx_ex_units?: ExUnits | null + max_tx_size?: number | null + max_value_size?: number | null + min_committee_size?: number | null + min_pool_cost?: string | null + minfee_a?: string | null + minfee_b?: string | null + n_opt?: number | null + pool_deposit?: string | null + pool_pledge_influence?: UnitInterval | null + pool_voting_thresholds?: PoolVotingThresholds | null + protocol_version?: ProtocolVersion | null + ref_script_coins_per_byte?: UnitInterval | null + treasury_growth_rate?: UnitInterval | null +} +export interface Costmdls { + [k: string]: CostModel +} +export interface DRepVotingThresholds { + committee_no_confidence: UnitInterval + committee_normal: UnitInterval + hard_fork_initiation: UnitInterval + motion_no_confidence: UnitInterval + pp_economic_group: UnitInterval + pp_governance_group: UnitInterval + pp_network_group: UnitInterval + pp_technical_group: UnitInterval + treasury_withdrawal: UnitInterval + update_constitution: UnitInterval +} +export interface ExUnitPrices { + mem_price: UnitInterval + step_price: UnitInterval +} +export interface Nonce { + /** + * @minItems 32 + * @maxItems 32 + */ + hash?: + | [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ] + | null +} +export interface ExUnits { + mem: string + steps: string +} +export interface PoolVotingThresholds { + committee_no_confidence: UnitInterval + committee_normal: UnitInterval + hard_fork_initiation: UnitInterval + motion_no_confidence: UnitInterval + security_relevant_threshold: UnitInterval +} +export interface VoterVotes { + voter: Voter + votes: Vote[] +} +export interface Vote { + action_id: GovernanceActionId + voting_procedure: VotingProcedure +} +export interface GovernanceActionId { + index: number + transaction_id: string +} +export interface VotingProcedure { + anchor?: Anchor | null + vote: VoteKind +} +export interface VotingProposal { + anchor: Anchor + deposit: string + governance_action: GovernanceAction + reward_account: string +} +export interface ParameterChangeAction { + gov_action_id?: GovernanceActionId | null + policy_hash?: string | null + protocol_param_updates: ProtocolParamUpdate +} +export interface HardForkInitiationAction { + gov_action_id?: GovernanceActionId | null + protocol_version: ProtocolVersion +} +export interface TreasuryWithdrawalsAction { + policy_hash?: string | null + withdrawals: TreasuryWithdrawals +} +export interface TreasuryWithdrawals { + [k: string]: string +} +export interface NoConfidenceAction { + gov_action_id?: GovernanceActionId | null +} +export interface UpdateCommitteeAction { + committee: Committee + gov_action_id?: GovernanceActionId | null + members_to_remove: CredType[] +} +export interface Committee { + members: CommitteeMember[] + quorum_threshold: UnitInterval +} +export interface CommitteeMember { + stake_credential: CredType + term_limit: number +} +export interface NewConstitutionAction { + constitution: Constitution + gov_action_id?: GovernanceActionId | null +} +export interface Constitution { + anchor: Anchor + script_hash?: string | null +} +export interface TransactionWitnessSet { + bootstraps?: BootstrapWitness[] | null + native_scripts?: NativeScripts | null + plutus_data?: PlutusList | null + plutus_scripts?: PlutusScripts | null + redeemers?: Redeemer[] | null + vkeys?: Vkeywitness[] | null +} +export interface BootstrapWitness { + attributes: number[] + chain_code: number[] + signature: string + vkey: Vkey +} +export interface PlutusList { + definite_encoding?: boolean | null + elems: string[] +} +export interface Redeemer { + data: string + ex_units: ExUnits + index: string + tag: RedeemerTag +} +export interface Vkeywitness { + signature: string + vkey: Vkey +} +export type BlockHash = string +export type BootstrapWitnesses = BootstrapWitness[] + +export type CertificateEnum = + | { + StakeRegistration: StakeRegistration + } + | { + StakeDeregistration: StakeDeregistration + } + | { + StakeDelegation: StakeDelegation + } + | { + PoolRegistration: PoolRegistration + } + | { + PoolRetirement: PoolRetirement + } + | { + GenesisKeyDelegation: GenesisKeyDelegation + } + | { + MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCert + } + | { + CommitteeHotAuth: CommitteeHotAuth + } + | { + CommitteeColdResign: CommitteeColdResign + } + | { + DRepDeregistration: DRepDeregistration + } + | { + DRepRegistration: DRepRegistration + } + | { + DRepUpdate: DRepUpdate + } + | { + StakeAndVoteDelegation: StakeAndVoteDelegation + } + | { + StakeRegistrationAndDelegation: StakeRegistrationAndDelegation + } + | { + StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegation + } + | { + VoteDelegation: VoteDelegation + } + | { + VoteRegistrationAndDelegation: VoteRegistrationAndDelegation + } +export type Certificates = Certificate[] + +export type Credential = CredType +export type Credentials = CredType[] +export type DRepEnum = + | ('AlwaysAbstain' | 'AlwaysNoConfidence') + | { + KeyHash: string + } + | { + ScriptHash: string + } +export type DataHash = string +export type Ed25519KeyHash = string +export type Ed25519KeyHashes = string[] +export type Ed25519Signature = string +export interface GeneralTransactionMetadata { + [k: string]: string +} +export type GenesisDelegateHash = string +export type GenesisHash = string +export type GenesisHashes = string[] +export type GovernanceActionEnum = + | { + ParameterChangeAction: ParameterChangeAction + } + | { + HardForkInitiationAction: HardForkInitiationAction + } + | { + TreasuryWithdrawalsAction: TreasuryWithdrawalsAction + } + | { + NoConfidenceAction: NoConfidenceAction + } + | { + UpdateCommitteeAction: UpdateCommitteeAction + } + | { + NewConstitutionAction: NewConstitutionAction + } + | { + InfoAction: InfoAction + } +export type GovernanceActionIds = GovernanceActionId[] + +export type Int = string +/** + * @minItems 4 + * @maxItems 4 + */ +export type KESVKey = string +export type Language = LanguageKind +export type LanguageKind = 'PlutusV1' | 'PlutusV2' | 'PlutusV3' +export type Languages = Language[] +export type MIRToStakeCredentials = StakeToCoin[] + +export type MintsAssets = MintAssets[] + +export type NetworkIdKind = 'Testnet' | 'Mainnet' +export type PlutusScript = string +export type PoolMetadataHash = string +export interface ProposedProtocolParameterUpdates { + [k: string]: ProtocolParamUpdate +} +export type PublicKey = string +export type RedeemerTagKind = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' +export type Redeemers = Redeemer[] + +export type RelayEnum = + | { + SingleHostAddr: SingleHostAddr + } + | { + SingleHostName: SingleHostName + } + | { + MultiHostName: MultiHostName + } +/** + * @minItems 4 + * @maxItems 4 + */ +export type RewardAddress = string +export type RewardAddresses = string[] +export type ScriptDataHash = string +export type ScriptHash = string +export type ScriptHashes = string[] +export type ScriptRefEnum = + | { + NativeScript: NativeScript + } + | { + PlutusScript: string + } +export interface Transaction { + auxiliary_data?: AuxiliaryData | null + body: TransactionBody + is_valid: boolean + witness_set: TransactionWitnessSet +} +export type TransactionHash = string +export type TransactionInputs = TransactionInput[] + +export type TransactionMetadatum = string +export interface TransactionUnspentOutput { + input: TransactionInput + output: TransactionOutput +} +export type TransactionUnspentOutputs = TransactionUnspentOutput[] + +export type VRFKeyHash = string +export type VRFVKey = string +export interface VersionedBlock { + block: Block + era_code: number +} +export type Vkeywitnesses = Vkeywitness[] + +export type VoterEnum = + | { + ConstitutionalCommitteeHotCred: CredType + } + | { + DRep: CredType + } + | { + StakingPool: string + } +export type Voters = Voter[] +export type VotingProcedures = VoterVotes[] + +export type VotingProposals = VotingProposal[] + +export interface Withdrawals { + [k: string]: string +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx new file mode 100644 index 0000000000..9fe38a4708 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx @@ -0,0 +1,469 @@ +import {Blockies} from '@yoroi/identicon' +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import Svg, {Defs, Image, Pattern, Rect, SvgProps, Use} from 'react-native-svg' + +import {Icon} from '../../../../../components' +import {Space} from '../../../../../components/Space/Space' +import {Warning} from '../../../../../components/Warning' +import {useCopy} from '../../../../../hooks/useCopy' +import {useRewardAddress} from '../../../../../yoroi-wallets/hooks' +import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' +import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' +import {Divider} from '../../../common/Divider' +import {formattedTx} from '../../../common/formattedTransaction' + +export const OverviewTab = ({tx, createdBy}: {tx: formattedTx; createdBy?: React.ReactNode}) => { + const {styles} = useStyles() + + return ( + + + + + + + + {createdBy !== undefined && ( + <> + + + + + )} + + + + + + + + + + + + + + + + + + + + ) +} + +const WalletInfoItem = () => { + const {styles} = useStyles() + const {wallet, meta} = useSelectedWallet() + const {walletManager} = useWalletManager() + const {plate, seed} = walletManager.checksum(wallet.publicKeyHex) + + return ( + + Wallet + + + + + + + {`${plate} | ${meta.name}`} + + + ) +} + +const FeeInfoItem = ({fee}: {fee: string}) => { + const {styles} = useStyles() + + return ( + + Fee + + {fee} + + ) +} + +// TODO (for dapps) +const CreatedByInfoItem = () => { + const {styles} = useStyles() + + return ( + + Created By + + + + + + + Linking.openURL('https://google.com')}> + dapp.org + + + + ) +} + +const SenderTokensSection = ({tx}: {tx: formattedTx}) => { + console.log(tx) + const {wallet} = useSelectedWallet() + const rewardAddress = useRewardAddress(wallet) + + return ( + + + +
+ + + + + + ) +} + +const Address = ({address}: {address: string}) => { + const {styles, colors} = useStyles() + const [, copy] = useCopy() + + return ( + + + {address} + + + copy(address)} activeOpacity={0.5}> + + + + ) +} + +const SenderTokensItems = () => { + const {styles} = useStyles() + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +const SenderTokensSectionLabel = () => { + const {styles, colors} = useStyles() + + return ( + + + + + + Send + + ) +} + +const ReceiverTokensSectionLabel = () => { + const {styles, colors} = useStyles() + + return ( + + + + + + Receive + + ) +} + +const ReceiverTokensSection = () => { + const {styles, colors} = useStyles() + + const isRegularAdress = true + const isMultiReceiver = true + + if (isMultiReceiver) { + return ( + <> + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } + + return ( + <> + + + + {isRegularAdress ? `To` : 'To script'}: + + + stake1u948jr02falxxqphnv3g3rkd3mdzqmtqq3x0tjl39m7dqngqg0fxp + + + + + + ) +} + +const TokenItem = ({ + isPrimaryToken = true, + isSent = true, + value, +}: { + isPrimaryToken?: boolean + isSent?: boolean + value: string +}) => { + const {styles} = useStyles() + + if (!isSent) + return ( + + + {value} + + + ) + + return ( + + {value} + + ) +} + +const CollapsibleSection = ({label, children}: {label: string; children: React.ReactNode}) => { + const {styles, colors} = useStyles() + const [isOpen, setIsOpen] = React.useState(false) + + return ( + <> + + {label} + + setIsOpen((isOpen) => !isOpen)}> + + + + + {isOpen && children} + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.px_lg, + }, + infoItem: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + infoLabel: { + ...atoms.body_2_md_regular, + color: color.gray_600, + }, + walletInfoText: { + ...atoms.body_2_md_medium, + color: color.text_primary_medium, + }, + plate: { + ...atoms.flex_row, + ...atoms.align_center, + }, + fee: { + color: color.gray_900, + ...atoms.body_2_md_regular, + }, + link: { + color: color.text_primary_medium, + ...atoms.body_2_md_medium, + }, + sectionHeader: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + myWalletAddress: { + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.flex_row, + ...atoms.justify_between, + }, + myWalletAddressText: { + ...atoms.flex_1, + ...atoms.body_2_md_regular, + color: color.gray_900, + }, + sectionHeaderText: { + ...atoms.body_1_lg_medium, + color: color.gray_900, + }, + tokenSectionLabel: { + ...atoms.body_2_md_regular, + color: color.gray_900, + }, + sentTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.primary_500, + }, + receivedTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.secondary_300, + }, + senderTokenItems: { + ...atoms.flex_wrap, + ...atoms.flex_row, + ...atoms.justify_end, + ...atoms.flex_1, + gap: 8, + }, + tokenSentItemText: { + ...atoms.body_2_md_regular, + color: color.white_static, + }, + tokenReceivedItemText: { + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + notPrimarySentTokenItem: { + backgroundColor: color.primary_100, + }, + notPrimaryReceivedTokenItem: { + backgroundColor: color.secondary_100, + }, + notPrimarySentTokenItemText: { + color: color.text_primary_medium, + }, + notPrimaryReceivedTokenItemText: { + color: color.secondary_700, + }, + tokensSection: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + tokensSectionLabel: { + ...atoms.flex_row, + ...atoms.align_center, + }, + walletChecksum: { + width: 24, + height: 24, + }, + }) + + const colors = { + copy: color.gray_900, + chevron: color.gray_900, + send: color.primary_500, + received: color.green_static, + } + + return {styles, colors} as const +} + +function SvgComponent(props: SvgProps) { + return ( + + + + + + + + + + + + ) +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx new file mode 100644 index 0000000000..38833abb88 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx @@ -0,0 +1,59 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet} from 'react-native' + +import {SafeArea} from '../../../../components/SafeArea' +import {Tab, Tabs} from '../../../../components/Tabs' +import {Divider} from '../../common/Divider' +import {useFormattedTransaction} from '../../common/formattedTransaction' +import {multiAssetsOneReceiver} from '../../common/mocks' +import {OverviewTab} from './Overview/OverviewTab' + +export const ReviewTransactionScreen = () => { + const {styles} = useStyles() + const [activeTab, setActiveTab] = React.useState('overview') + const formatedTx = useFormattedTransaction(multiAssetsOneReceiver) + + console.log('tx', JSON.stringify(formatedTx, null, 2)) + + const renderTabs = React.useMemo(() => { + return ( + + setActiveTab('overview')} + label="Overview" + /> + + setActiveTab('utxos')} label="UTxOs" /> + + ) + }, [activeTab, setActiveTab, styles.tab, styles.tabs]) + + return ( + + {renderTabs} + + + + {activeTab === 'overview' && } + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + tabs: { + ...atoms.px_lg, + ...atoms.gap_lg, + backgroundColor: color.bg_color_max, + }, + tab: { + flex: 0, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx index 2ed1e978d7..fc4375a5b6 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx @@ -73,6 +73,14 @@ export const ConfirmTxScreen = () => { if (yoroiUnsignedTx === undefined) throw new Error('Missing yoroiUnsignedTx') + React.useEffect(() => { + const test = async () => { + console.log('txBody', await yoroiUnsignedTx.unsignedTx.txBody.toJson()) + } + + test() + }, [yoroiUnsignedTx.unsignedTx.txBody]) + return ( diff --git a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx index e186ab34a7..88f1d7149e 100644 --- a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx @@ -27,6 +27,7 @@ import {ReceiveProvider} from '../Receive/common/ReceiveProvider' import {DescribeSelectedAddressScreen} from '../Receive/useCases/DescribeSelectedAddressScreen' import {ListMultipleAddressesScreen} from '../Receive/useCases/ListMultipleAddressesScreen' import {RequestSpecificAmountScreen} from '../Receive/useCases/RequestSpecificAmountScreen' +import {ReviewTransactionNavigator} from '../ReviewTransaction/ReviewTransactionNavigator' import {CodeScannerButton} from '../Scan/common/CodeScannerButton' import {ScanCodeScreen} from '../Scan/useCases/ScanCodeScreen' import {ShowCameraPermissionDeniedScreen} from '../Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen' @@ -38,7 +39,7 @@ import {SelectTokenFromListScreen} from '../Send/useCases/ListAmountsToSend/AddT import {EditAmountScreen} from '../Send/useCases/ListAmountsToSend/EditAmount/EditAmountScreen' import {StartMultiTokenTxScreen} from '../Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen' import {NetworkTag} from '../Settings/ChangeNetwork/NetworkTag' -import {SwapTabNavigator} from '../Swap/SwapNavigator' +// import {SwapTabNavigator} from '../Swap/SwapNavigator' import { ConfirmTxScreen as ConfirmTxSwapScreen, EditSlippageScreen, @@ -220,7 +221,7 @@ export const TxHistoryNavigator = () => { + 'review-transaction-routes': NavigatorScreenParams 'nft-details-routes': NavigatorScreenParams settings: NavigatorScreenParams 'voting-registration': NavigatorScreenParams @@ -314,6 +315,15 @@ export type Portfolio2Routes = { history: NavigatorScreenParams } +export type ReviewTransactionRoutes = { + 'review-transaction': NavigatorScreenParams +} + +export type ReviewTransactionTabRoutes = { + overview: undefined + utxos: undefined +} + export type PortfolioTokenListTabRoutes = { 'wallet-token': undefined 'dapps-token': undefined diff --git a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts index ffe38c81a5..c35bc6b89e 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts @@ -30,6 +30,7 @@ import {TRANSACTION_DIRECTION, TRANSACTION_STATUS, YoroiSignedTx, YoroiUnsignedT import {TipStatusResponse, TxSubmissionStatus} from '../types/other' import {delay} from '../utils/timeUtils' import {Amounts, Quantities, Utxos} from '../utils/utils' +import {CardanoMobile} from '../wallets' const crashReportsStorageKey = 'sendCrashReports' @@ -123,6 +124,20 @@ export const useStakingKey = (wallet: YoroiWallet) => { return result.data } +export const useRewardAddress = (wallet: YoroiWallet) => { + const result = useQuery( + [wallet.id, 'useRewardAddress'], + async () => { + const rewardAddress = await CardanoMobile.Address.fromBytes(Buffer.from(wallet.rewardAddressHex, 'hex')) + const bech32RewardAddress = await rewardAddress.toBech32(undefined) + return bech32RewardAddress + }, + {suspense: true}, + ) + if (!result.data) throw new Error('invalid state') + return result.data +} + export const useKeyHashes = ({address}: {address: string}) => { const [spendingData, stakingData] = useQueries([ { diff --git a/apps/wallet-mobile/translations/messages/src/WalletNavigator.json b/apps/wallet-mobile/translations/messages/src/WalletNavigator.json index 40622d7b4e..22035b6b29 100644 --- a/apps/wallet-mobile/translations/messages/src/WalletNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/WalletNavigator.json @@ -6,12 +6,12 @@ "start": { "line": 305, "column": 22, - "index": 10540 + "index": 10548 }, "end": { "line": 308, "column": 3, - "index": 10643 + "index": 10651 } }, { @@ -21,12 +21,12 @@ "start": { "line": 309, "column": 14, - "index": 10659 + "index": 10667 }, "end": { "line": 312, "column": 3, - "index": 10758 + "index": 10766 } }, { @@ -36,12 +36,12 @@ "start": { "line": 313, "column": 17, - "index": 10777 + "index": 10785 }, "end": { "line": 316, "column": 3, - "index": 10882 + "index": 10890 } }, { @@ -51,12 +51,12 @@ "start": { "line": 317, "column": 19, - "index": 10903 + "index": 10911 }, "end": { "line": 320, "column": 3, - "index": 11000 + "index": 11008 } }, { @@ -66,12 +66,12 @@ "start": { "line": 321, "column": 18, - "index": 11020 + "index": 11028 }, "end": { "line": 324, "column": 3, - "index": 11115 + "index": 11123 } }, { @@ -81,12 +81,12 @@ "start": { "line": 325, "column": 16, - "index": 11133 + "index": 11141 }, "end": { "line": 328, "column": 3, - "index": 11231 + "index": 11239 } }, { @@ -96,12 +96,12 @@ "start": { "line": 329, "column": 17, - "index": 11250 + "index": 11258 }, "end": { "line": 332, "column": 3, - "index": 11315 + "index": 11323 } }, { @@ -111,12 +111,12 @@ "start": { "line": 333, "column": 14, - "index": 11331 + "index": 11339 }, "end": { "line": 336, "column": 3, - "index": 11425 + "index": 11433 } }, { @@ -126,12 +126,12 @@ "start": { "line": 337, "column": 14, - "index": 11441 + "index": 11449 }, "end": { "line": 340, "column": 3, - "index": 11493 + "index": 11501 } }, { @@ -141,12 +141,12 @@ "start": { "line": 341, "column": 18, - "index": 11513 + "index": 11521 }, "end": { "line": 344, "column": 3, - "index": 11602 + "index": 11610 } }, { @@ -156,12 +156,12 @@ "start": { "line": 345, "column": 31, - "index": 11635 + "index": 11643 }, "end": { "line": 348, "column": 3, - "index": 11744 + "index": 11752 } }, { @@ -171,12 +171,12 @@ "start": { "line": 349, "column": 19, - "index": 11765 + "index": 11773 }, "end": { "line": 352, "column": 3, - "index": 11834 + "index": 11842 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json index 7d93064caa..eb3d4f5724 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Receive", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 417, + "line": 418, "column": 16, - "index": 15219 + "index": 15323 }, "end": { - "line": 420, + "line": 421, "column": 3, - "index": 15308 + "index": 15412 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Address details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 421, + "line": 422, "column": 32, - "index": 15342 + "index": 15446 }, "end": { - "line": 424, + "line": 425, "column": 3, - "index": 15455 + "index": 15559 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Swap", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 425, + "line": 426, "column": 13, - "index": 15470 + "index": 15574 }, "end": { - "line": 428, + "line": 429, "column": 3, - "index": 15543 + "index": 15647 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Swap from", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 429, + "line": 430, "column": 17, - "index": 15562 + "index": 15666 }, "end": { - "line": 432, + "line": 433, "column": 3, - "index": 15639 + "index": 15743 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Swap to", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 433, + "line": 434, "column": 15, - "index": 15656 + "index": 15760 }, "end": { - "line": 436, + "line": 437, "column": 3, - "index": 15729 + "index": 15833 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Slippage Tolerance", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 437, + "line": 438, "column": 21, - "index": 15752 + "index": 15856 }, "end": { - "line": 440, + "line": 441, "column": 3, - "index": 15847 + "index": 15951 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Select pool", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 441, + "line": 442, "column": 14, - "index": 15863 + "index": 15967 }, "end": { - "line": 444, + "line": 445, "column": 3, - "index": 15944 + "index": 16048 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Send", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 445, + "line": 446, "column": 13, - "index": 15959 + "index": 16063 }, "end": { - "line": 448, + "line": 449, "column": 3, - "index": 16039 + "index": 16143 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Scan QR code address", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 449, + "line": 450, "column": 18, - "index": 16059 + "index": 16163 }, "end": { - "line": 452, + "line": 453, "column": 3, - "index": 16160 + "index": 16264 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Select asset", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 453, + "line": 454, "column": 20, - "index": 16182 + "index": 16286 }, "end": { - "line": 456, + "line": 457, "column": 3, - "index": 16271 + "index": 16375 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Assets added", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 457, + "line": 458, "column": 26, - "index": 16299 + "index": 16403 }, "end": { - "line": 460, + "line": 461, "column": 3, - "index": 16400 + "index": 16504 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Edit amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 461, + "line": 462, "column": 19, - "index": 16421 + "index": 16525 }, "end": { - "line": 464, + "line": 465, "column": 3, - "index": 16514 + "index": 16618 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Confirm", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 465, + "line": 466, "column": 16, - "index": 16532 + "index": 16636 }, "end": { - "line": 468, + "line": 469, "column": 3, - "index": 16618 + "index": 16722 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 469, + "line": 470, "column": 19, - "index": 16639 + "index": 16743 }, "end": { - "line": 475, + "line": 476, "column": 3, - "index": 16877 + "index": 16981 } }, { @@ -214,14 +214,14 @@ "defaultMessage": "!!!Confirm transaction", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 476, + "line": 477, "column": 27, - "index": 16906 + "index": 17010 }, "end": { - "line": 479, + "line": 480, "column": 3, - "index": 16999 + "index": 17103 } }, { @@ -229,14 +229,14 @@ "defaultMessage": "!!!Please scan a QR code", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 480, + "line": 481, "column": 13, - "index": 17014 + "index": 17118 }, "end": { - "line": 483, + "line": 484, "column": 3, - "index": 17089 + "index": 17193 } }, { @@ -244,14 +244,14 @@ "defaultMessage": "!!!Success", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 484, + "line": 485, "column": 25, - "index": 17116 + "index": 17220 }, "end": { - "line": 487, + "line": 488, "column": 3, - "index": 17190 + "index": 17294 } }, { @@ -259,14 +259,14 @@ "defaultMessage": "!!!Request specific amount", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 488, + "line": 489, "column": 18, - "index": 17210 + "index": 17314 }, "end": { - "line": 491, + "line": 492, "column": 3, - "index": 17324 + "index": 17428 } }, { @@ -274,14 +274,14 @@ "defaultMessage": "!!!Buy/Sell ADA", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 492, + "line": 493, "column": 28, - "index": 17354 + "index": 17458 }, "end": { - "line": 495, + "line": 496, "column": 3, - "index": 17450 + "index": 17554 } }, { @@ -289,14 +289,14 @@ "defaultMessage": "!!!Buy provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 496, + "line": 497, "column": 29, - "index": 17481 + "index": 17585 }, "end": { - "line": 499, + "line": 500, "column": 3, - "index": 17589 + "index": 17693 } }, { @@ -304,14 +304,14 @@ "defaultMessage": "!!!Sell provider", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 500, + "line": 501, "column": 30, - "index": 17621 + "index": 17725 }, "end": { - "line": 503, + "line": 504, "column": 3, - "index": 17731 + "index": 17835 } }, { @@ -319,14 +319,14 @@ "defaultMessage": "!!!Tx Details", "file": "src/features/Transactions/TxHistoryNavigator.tsx", "start": { - "line": 504, + "line": 505, "column": 18, - "index": 17751 + "index": 17855 }, "end": { - "line": 507, + "line": 508, "column": 3, - "index": 17845 + "index": 17949 } } ] \ No newline at end of file diff --git a/packages/theme/src/base-palettes/dark-palette.ts b/packages/theme/src/base-palettes/dark-palette.ts index 2d5582e877..8374a43683 100644 --- a/packages/theme/src/base-palettes/dark-palette.ts +++ b/packages/theme/src/base-palettes/dark-palette.ts @@ -39,6 +39,7 @@ export const darkPalette: BasePalette = { black_static: '#000000', white_static: '#FFFFFF', + green_static: '#08C29D', sys_magenta_700: '#FFC0D0', sys_magenta_600: '#FB9CB5', diff --git a/packages/theme/src/base-palettes/light-palette.ts b/packages/theme/src/base-palettes/light-palette.ts index dbddef3e92..5dedefc337 100644 --- a/packages/theme/src/base-palettes/light-palette.ts +++ b/packages/theme/src/base-palettes/light-palette.ts @@ -36,6 +36,7 @@ export const lightPalette: BasePalette = { black_static: '#000000', white_static: '#FFFFFF', + green_static: '#08C29D', sys_magenta_700: '#CF053A', sys_magenta_600: '#E80742', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index a8031ec031..f8dd1776e6 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -59,6 +59,7 @@ export type BasePalette = { black_static: HexColor white_static: HexColor + green_static: HexColor sys_magenta_700: HexColor sys_magenta_600: HexColor From 22159a850e6a90a19db86e16471cf18abe26704d Mon Sep 17 00:00:00 2001 From: Javier Bueno Date: Wed, 18 Sep 2024 09:57:20 +0200 Subject: [PATCH 02/23] feat(tx-review): improvements --- .../useCases/ReviewTransaction/types.ts | 846 ------------------ .../ReviewTransactionNavigator.tsx | 39 - .../common/formattedTransaction.tsx | 58 +- .../ReviewTransaction/common/mocks.ts | 6 +- .../Overview/OverviewTab.tsx | 63 +- .../ReviewTransactionScreen.tsx | 2 +- .../Transactions/TxHistoryNavigator.tsx | 4 +- .../src/yoroi-wallets/hooks/index.ts | 6 +- .../Transactions/TxHistoryNavigator.json | 88 +- 9 files changed, 120 insertions(+), 992 deletions(-) delete mode 100644 apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts delete mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx diff --git a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts deleted file mode 100644 index c65b394738..0000000000 --- a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/types.ts +++ /dev/null @@ -1,846 +0,0 @@ -export type AddressJSON = string -export type URLJSON = string - -export interface AnchorJSON { - anchor_data_hash: string - anchor_url: URLJSON -} -export type AnchorDataHashJSON = string -export type AssetNameJSON = string -export type AssetNamesJSON = string[] -export interface AssetsJSON { - [k: string]: string -} -export type NativeScriptJSON = - | { - ScriptPubkey: ScriptPubkeyJSON - } - | { - ScriptAll: ScriptAllJSON - } - | { - ScriptAny: ScriptAnyJSON - } - | { - ScriptNOfK: ScriptNOfKJSON - } - | { - TimelockStart: TimelockStartJSON - } - | { - TimelockExpiry: TimelockExpiryJSON - } -export type NativeScriptsJSON = NativeScriptJSON[] -export type PlutusScriptsJSON = string[] - -export interface AuxiliaryDataJSON { - metadata?: { - [k: string]: string - } | null - native_scripts?: NativeScriptsJSON | null - plutus_scripts?: PlutusScriptsJSON | null - prefer_alonzo_format: boolean -} -export interface ScriptPubkeyJSON { - addr_keyhash: string -} -export interface ScriptAllJSON { - native_scripts: NativeScriptsJSON -} -export interface ScriptAnyJSON { - native_scripts: NativeScriptsJSON -} -export interface ScriptNOfKJSON { - n: number - native_scripts: NativeScriptsJSON -} -export interface TimelockStartJSON { - slot: string -} -export interface TimelockExpiryJSON { - slot: string -} -export type AuxiliaryDataHashJSON = string -export interface AuxiliaryDataSetJSON { - [k: string]: AuxiliaryDataJSON -} -export type BigIntJSON = string -export type BigNumJSON = string -export type VkeyJSON = string -export type HeaderLeaderCertEnumJSON = - | { - /** - * @minItems 2 - * @maxItems 2 - */ - NonceAndLeader: [VRFCertJSON, VRFCertJSON] - } - | { - VrfResult: VRFCertJSON - } -export type CertificateJSON = - | { - StakeRegistration: StakeRegistrationJSON - } - | { - StakeDeregistration: StakeDeregistrationJSON - } - | { - StakeDelegation: StakeDelegationJSON - } - | { - PoolRegistration: PoolRegistrationJSON - } - | { - PoolRetirement: PoolRetirementJSON - } - | { - GenesisKeyDelegation: GenesisKeyDelegationJSON - } - | { - MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCertJSON - } - | { - CommitteeHotAuth: CommitteeHotAuthJSON - } - | { - CommitteeColdResign: CommitteeColdResignJSON - } - | { - DRepDeregistration: DRepDeregistrationJSON - } - | { - DRepRegistration: DRepRegistrationJSON - } - | { - DRepUpdate: DRepUpdateJSON - } - | { - StakeAndVoteDelegation: StakeAndVoteDelegationJSON - } - | { - StakeRegistrationAndDelegation: StakeRegistrationAndDelegationJSON - } - | { - StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegationJSON - } - | { - VoteDelegation: VoteDelegationJSON - } - | { - VoteRegistrationAndDelegation: VoteRegistrationAndDelegationJSON - } -export type CredTypeJSON = - | { - Key: string - } - | { - Script: string - } -export type RelayJSON = - | { - SingleHostAddr: SingleHostAddrJSON - } - | { - SingleHostName: SingleHostNameJSON - } - | { - MultiHostName: MultiHostNameJSON - } -/** - * @minItems 4 - * @maxItems 4 - */ -export type Ipv4JSON = [number, number, number, number] -/** - * @minItems 16 - * @maxItems 16 - */ -export type Ipv6JSON = [ - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, -] -export type DNSRecordAorAAAAJSON = string -export type DNSRecordSRVJSON = string -export type RelaysJSON = RelayJSON[] -export type MIRPotJSON = 'Reserves' | 'Treasury' -export type MIREnumJSON = - | { - ToOtherPot: string - } - | { - ToStakeCredentials: StakeToCoinJSON[] - } -export type DRepJSON = - | ('AlwaysAbstain' | 'AlwaysNoConfidence') - | { - KeyHash: string - } - | { - ScriptHash: string - } -export type DataOptionJSON = - | { - DataHash: string - } - | { - Data: string - } -export type ScriptRefJSON = - | { - NativeScript: NativeScriptJSON - } - | { - PlutusScript: string - } -export type MintJSON = [string, MintAssetsJSON][] -export type NetworkIdJSON = 'Testnet' | 'Mainnet' -export type TransactionOutputsJSON = TransactionOutputJSON[] -export type CostModelJSON = string[] -export type VoterJSON = - | { - ConstitutionalCommitteeHotCred: CredTypeJSON - } - | { - DRep: CredTypeJSON - } - | { - StakingPool: string - } -export type VoteKindJSON = 'No' | 'Yes' | 'Abstain' -export type GovernanceActionJSON = - | { - ParameterChangeAction: ParameterChangeActionJSON - } - | { - HardForkInitiationAction: HardForkInitiationActionJSON - } - | { - TreasuryWithdrawalsAction: TreasuryWithdrawalsActionJSON - } - | { - NoConfidenceAction: NoConfidenceActionJSON - } - | { - UpdateCommitteeAction: UpdateCommitteeActionJSON - } - | { - NewConstitutionAction: NewConstitutionActionJSON - } - | { - InfoAction: InfoActionJSON - } -/** - * @minItems 0 - * @maxItems 0 - */ -export type InfoActionJSON = [] -export type TransactionBodiesJSON = TransactionBodyJSON[] -export type RedeemerTagJSON = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' -export type TransactionWitnessSetsJSON = TransactionWitnessSetJSON[] - -export interface BlockJSON { - auxiliary_data_set: { - [k: string]: AuxiliaryDataJSON - } - header: HeaderJSON - invalid_transactions: number[] - transaction_bodies: TransactionBodiesJSON - transaction_witness_sets: TransactionWitnessSetsJSON -} -export interface HeaderJSON { - body_signature: string - header_body: HeaderBodyJSON -} -export interface HeaderBodyJSON { - block_body_hash: string - block_body_size: number - block_number: number - issuer_vkey: VkeyJSON - leader_cert: HeaderLeaderCertEnumJSON - operational_cert: OperationalCertJSON - prev_hash?: string | null - protocol_version: ProtocolVersionJSON - slot: string - vrf_vkey: string -} -export interface VRFCertJSON { - output: number[] - proof: number[] -} -export interface OperationalCertJSON { - hot_vkey: string - kes_period: number - sequence_number: number - sigma: string -} -export interface ProtocolVersionJSON { - major: number - minor: number -} -export interface TransactionBodyJSON { - auxiliary_data_hash?: string | null - certs?: CertificateJSON[] | null - collateral?: TransactionInputJSON[] | null - collateral_return?: TransactionOutputJSON | null - current_treasury_value?: string | null - donation?: string | null - fee: string - inputs: TransactionInputJSON[] - mint?: MintJSON | null - network_id?: NetworkIdJSON | null - outputs: TransactionOutputsJSON - reference_inputs?: TransactionInputJSON[] | null - required_signers?: string[] | null - script_data_hash?: string | null - total_collateral?: string | null - ttl?: string | null - update?: UpdateJSON | null - validity_start_interval?: string | null - voting_procedures?: VoterVotesJSON[] | null - voting_proposals?: VotingProposalJSON[] | null - withdrawals?: { - [k: string]: string - } | null -} -export interface StakeRegistrationJSON { - coin?: string | null - stake_credential: CredTypeJSON -} -export interface StakeDeregistrationJSON { - coin?: string | null - stake_credential: CredTypeJSON -} -export interface StakeDelegationJSON { - pool_keyhash: string - stake_credential: CredTypeJSON -} -export interface PoolRegistrationJSON { - pool_params: PoolParamsJSON -} -export interface PoolParamsJSON { - cost: string - margin: UnitIntervalJSON - operator: string - pledge: string - pool_metadata?: PoolMetadataJSON | null - pool_owners: string[] - relays: RelaysJSON - reward_account: string - vrf_keyhash: string -} -export interface UnitIntervalJSON { - denominator: string - numerator: string -} -export interface PoolMetadataJSON { - pool_metadata_hash: string - url: URLJSON -} -export interface SingleHostAddrJSON { - ipv4?: Ipv4JSON | null - ipv6?: Ipv6JSON | null - port?: number | null -} -export interface SingleHostNameJSON { - dns_name: DNSRecordAorAAAAJSON - port?: number | null -} -export interface MultiHostNameJSON { - dns_name: DNSRecordSRVJSON -} -export interface PoolRetirementJSON { - epoch: number - pool_keyhash: string -} -export interface GenesisKeyDelegationJSON { - genesis_delegate_hash: string - genesishash: string - vrf_keyhash: string -} -export interface MoveInstantaneousRewardsCertJSON { - move_instantaneous_reward: MoveInstantaneousRewardJSON -} -export interface MoveInstantaneousRewardJSON { - pot: MIRPotJSON - variant: MIREnumJSON -} -export interface StakeToCoinJSON { - amount: string - stake_cred: CredTypeJSON -} -export interface CommitteeHotAuthJSON { - committee_cold_credential: CredTypeJSON - committee_hot_credential: CredTypeJSON -} -export interface CommitteeColdResignJSON { - anchor?: AnchorJSON | null - committee_cold_credential: CredTypeJSON -} -export interface DRepDeregistrationJSON { - coin: string - voting_credential: CredTypeJSON -} -export interface DRepRegistrationJSON { - anchor?: AnchorJSON | null - coin: string - voting_credential: CredTypeJSON -} -export interface DRepUpdateJSON { - anchor?: AnchorJSON | null - voting_credential: CredTypeJSON -} -export interface StakeAndVoteDelegationJSON { - drep: DRepJSON - pool_keyhash: string - stake_credential: CredTypeJSON -} -export interface StakeRegistrationAndDelegationJSON { - coin: string - pool_keyhash: string - stake_credential: CredTypeJSON -} -export interface StakeVoteRegistrationAndDelegationJSON { - coin: string - drep: DRepJSON - pool_keyhash: string - stake_credential: CredTypeJSON -} -export interface VoteDelegationJSON { - drep: DRepJSON - stake_credential: CredTypeJSON -} -export interface VoteRegistrationAndDelegationJSON { - coin: string - drep: DRepJSON - stake_credential: CredTypeJSON -} -export interface TransactionInputJSON { - index: number - transaction_id: string -} -export interface TransactionOutputJSON { - address: string - amount: ValueJSON - plutus_data?: DataOptionJSON | null - script_ref?: ScriptRefJSON | null -} -export interface ValueJSON { - coin: string - multiasset?: MultiAssetJSON | null -} -export interface MultiAssetJSON { - [k: string]: AssetsJSON -} -export interface MintAssetsJSON { - [k: string]: string -} -export interface UpdateJSON { - epoch: number - proposed_protocol_parameter_updates: { - [k: string]: ProtocolParamUpdateJSON - } -} -export interface ProtocolParamUpdateJSON { - ada_per_utxo_byte?: string | null - collateral_percentage?: number | null - committee_term_limit?: number | null - cost_models?: CostmdlsJSON | null - d?: UnitIntervalJSON | null - drep_deposit?: string | null - drep_inactivity_period?: number | null - drep_voting_thresholds?: DRepVotingThresholdsJSON | null - execution_costs?: ExUnitPricesJSON | null - expansion_rate?: UnitIntervalJSON | null - extra_entropy?: NonceJSON | null - governance_action_deposit?: string | null - governance_action_validity_period?: number | null - key_deposit?: string | null - max_block_body_size?: number | null - max_block_ex_units?: ExUnitsJSON | null - max_block_header_size?: number | null - max_collateral_inputs?: number | null - max_epoch?: number | null - max_tx_ex_units?: ExUnitsJSON | null - max_tx_size?: number | null - max_value_size?: number | null - min_committee_size?: number | null - min_pool_cost?: string | null - minfee_a?: string | null - minfee_b?: string | null - n_opt?: number | null - pool_deposit?: string | null - pool_pledge_influence?: UnitIntervalJSON | null - pool_voting_thresholds?: PoolVotingThresholdsJSON | null - protocol_version?: ProtocolVersionJSON | null - ref_script_coins_per_byte?: UnitIntervalJSON | null - treasury_growth_rate?: UnitIntervalJSON | null -} -export interface CostmdlsJSON { - [k: string]: CostModelJSON -} -export interface DRepVotingThresholdsJSON { - committee_no_confidence: UnitIntervalJSON - committee_normal: UnitIntervalJSON - hard_fork_initiation: UnitIntervalJSON - motion_no_confidence: UnitIntervalJSON - pp_economic_group: UnitIntervalJSON - pp_governance_group: UnitIntervalJSON - pp_network_group: UnitIntervalJSON - pp_technical_group: UnitIntervalJSON - treasury_withdrawal: UnitIntervalJSON - update_constitution: UnitIntervalJSON -} -export interface ExUnitPricesJSON { - mem_price: UnitIntervalJSON - step_price: UnitIntervalJSON -} -export interface NonceJSON { - /** - * @minItems 32 - * @maxItems 32 - */ - hash?: - | [ - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - number, - ] - | null -} -export interface ExUnitsJSON { - mem: string - steps: string -} -export interface PoolVotingThresholdsJSON { - committee_no_confidence: UnitIntervalJSON - committee_normal: UnitIntervalJSON - hard_fork_initiation: UnitIntervalJSON - motion_no_confidence: UnitIntervalJSON - security_relevant_threshold: UnitIntervalJSON -} -export interface VoterVotesJSON { - voter: VoterJSON - votes: VoteJSON[] -} -export interface VoteJSON { - action_id: GovernanceActionIdJSON - voting_procedure: VotingProcedureJSON -} -export interface GovernanceActionIdJSON { - index: number - transaction_id: string -} -export interface VotingProcedureJSON { - anchor?: AnchorJSON | null - vote: VoteKindJSON -} -export interface VotingProposalJSON { - anchor: AnchorJSON - deposit: string - governance_action: GovernanceActionJSON - reward_account: string -} -export interface ParameterChangeActionJSON { - gov_action_id?: GovernanceActionIdJSON | null - policy_hash?: string | null - protocol_param_updates: ProtocolParamUpdateJSON -} -export interface HardForkInitiationActionJSON { - gov_action_id?: GovernanceActionIdJSON | null - protocol_version: ProtocolVersionJSON -} -export interface TreasuryWithdrawalsActionJSON { - policy_hash?: string | null - withdrawals: TreasuryWithdrawalsJSON -} -export interface TreasuryWithdrawalsJSON { - [k: string]: string -} -export interface NoConfidenceActionJSON { - gov_action_id?: GovernanceActionIdJSON | null -} -export interface UpdateCommitteeActionJSON { - committee: CommitteeJSON - gov_action_id?: GovernanceActionIdJSON | null - members_to_remove: CredTypeJSON[] -} -export interface CommitteeJSON { - members: CommitteeMemberJSON[] - quorum_threshold: UnitIntervalJSON -} -export interface CommitteeMemberJSON { - stake_credential: CredTypeJSON - term_limit: number -} -export interface NewConstitutionActionJSON { - constitution: ConstitutionJSON - gov_action_id?: GovernanceActionIdJSON | null -} -export interface ConstitutionJSON { - anchor: AnchorJSON - script_hash?: string | null -} -export interface TransactionWitnessSetJSON { - bootstraps?: BootstrapWitnessJSON[] | null - native_scripts?: NativeScriptsJSON | null - plutus_data?: PlutusListJSON | null - plutus_scripts?: PlutusScriptsJSON | null - redeemers?: RedeemerJSON[] | null - vkeys?: VkeywitnessJSON[] | null -} -export interface BootstrapWitnessJSON { - attributes: number[] - chain_code: number[] - signature: string - vkey: VkeyJSON -} -export interface PlutusListJSON { - definite_encoding?: boolean | null - elems: string[] -} -export interface RedeemerJSON { - data: string - ex_units: ExUnitsJSON - index: string - tag: RedeemerTagJSON -} -export interface VkeywitnessJSON { - signature: string - vkey: VkeyJSON -} -export type BlockHashJSON = string -export type BootstrapWitnessesJSON = BootstrapWitnessJSON[] - -export type CertificateEnumJSON = - | { - StakeRegistration: StakeRegistrationJSON - } - | { - StakeDeregistration: StakeDeregistrationJSON - } - | { - StakeDelegation: StakeDelegationJSON - } - | { - PoolRegistration: PoolRegistrationJSON - } - | { - PoolRetirement: PoolRetirementJSON - } - | { - GenesisKeyDelegation: GenesisKeyDelegationJSON - } - | { - MoveInstantaneousRewardsCert: MoveInstantaneousRewardsCertJSON - } - | { - CommitteeHotAuth: CommitteeHotAuthJSON - } - | { - CommitteeColdResign: CommitteeColdResignJSON - } - | { - DRepDeregistration: DRepDeregistrationJSON - } - | { - DRepRegistration: DRepRegistrationJSON - } - | { - DRepUpdate: DRepUpdateJSON - } - | { - StakeAndVoteDelegation: StakeAndVoteDelegationJSON - } - | { - StakeRegistrationAndDelegation: StakeRegistrationAndDelegationJSON - } - | { - StakeVoteRegistrationAndDelegation: StakeVoteRegistrationAndDelegationJSON - } - | { - VoteDelegation: VoteDelegationJSON - } - | { - VoteRegistrationAndDelegation: VoteRegistrationAndDelegationJSON - } -export type CertificatesJSON = CertificateJSON[] - -export type CredentialJSON = CredTypeJSON -export type CredentialsJSON = CredTypeJSON[] -export type DRepEnumJSON = - | ('AlwaysAbstain' | 'AlwaysNoConfidence') - | { - KeyHash: string - } - | { - ScriptHash: string - } -export type DataHashJSON = string -export type Ed25519KeyHashJSON = string -export type Ed25519KeyHashesJSON = string[] -export type Ed25519SignatureJSON = string -export interface GeneralTransactionMetadataJSON { - [k: string]: string -} -export type GenesisDelegateHashJSON = string -export type GenesisHashJSON = string -export type GenesisHashesJSON = string[] -export type GovernanceActionEnumJSON = - | { - ParameterChangeAction: ParameterChangeActionJSON - } - | { - HardForkInitiationAction: HardForkInitiationActionJSON - } - | { - TreasuryWithdrawalsAction: TreasuryWithdrawalsActionJSON - } - | { - NoConfidenceAction: NoConfidenceActionJSON - } - | { - UpdateCommitteeAction: UpdateCommitteeActionJSON - } - | { - NewConstitutionAction: NewConstitutionActionJSON - } - | { - InfoAction: InfoActionJSON - } -export type GovernanceActionIdsJSON = GovernanceActionIdJSON[] - -export type IntJSON = string -/** - * @minItems 4 - * @maxItems 4 - */ -export type KESVKeyJSON = string -export type LanguageJSON = LanguageKindJSON -export type LanguageKindJSON = 'PlutusV1' | 'PlutusV2' | 'PlutusV3' -export type LanguagesJSON = LanguageJSON[] -export type MIRToStakeCredentialsJSON = StakeToCoinJSON[] - -export type MintsAssetsJSON = MintAssetsJSON[] - -export type NetworkIdKindJSON = 'Testnet' | 'Mainnet' -export type PlutusScriptJSON = string -export type PoolMetadataHashJSON = string -export interface ProposedProtocolParameterUpdatesJSON { - [k: string]: ProtocolParamUpdateJSON -} -export type PublicKeyJSON = string -export type RedeemerTagKindJSON = 'Spend' | 'Mint' | 'Cert' | 'Reward' | 'Vote' | 'VotingProposal' -export type RedeemersJSON = RedeemerJSON[] - -export type RelayEnumJSON = - | { - SingleHostAddr: SingleHostAddrJSON - } - | { - SingleHostName: SingleHostNameJSON - } - | { - MultiHostName: MultiHostNameJSON - } -/** - * @minItems 4 - * @maxItems 4 - */ -export type RewardAddressJSON = string -export type RewardAddressesJSON = string[] -export type ScriptDataHashJSON = string -export type ScriptHashJSON = string -export type ScriptHashesJSON = string[] -export type ScriptRefEnumJSON = - | { - NativeScript: NativeScriptJSON - } - | { - PlutusScript: string - } -export interface TransactionJSON { - auxiliary_data?: AuxiliaryDataJSON | null - body: TransactionBodyJSON - is_valid: boolean - witness_set: TransactionWitnessSetJSON -} -export type TransactionHashJSON = string -export type TransactionInputsJSON = TransactionInputJSON[] - -export type TransactionMetadatumJSON = string -export interface TransactionUnspentOutputJSON { - input: TransactionInputJSON - output: TransactionOutputJSON -} -export type TransactionUnspentOutputsJSON = TransactionUnspentOutputJSON[] - -export type VRFKeyHashJSON = string -export type VRFVKeyJSON = string -export interface VersionedBlockJSON { - block: BlockJSON - era_code: number -} -export type VkeywitnessesJSON = VkeywitnessJSON[] - -export type VoterEnumJSON = - | { - ConstitutionalCommitteeHotCred: CredTypeJSON - } - | { - DRep: CredTypeJSON - } - | { - StakingPool: string - } -export type VotersJSON = VoterJSON[] -export type VotingProceduresJSON = VoterVotesJSON[] - -export type VotingProposalsJSON = VotingProposalJSON[] - -export interface WithdrawalsJSON { - [k: string]: string -} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx deleted file mode 100644 index c2c50d55b4..0000000000 --- a/apps/wallet-mobile/src/features/ReviewTransaction/ReviewTransactionNavigator.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import {createStackNavigator} from '@react-navigation/stack' -import {useTheme} from '@yoroi/theme' -import * as React from 'react' -import {StyleSheet} from 'react-native' - -import {KeyboardAvoidingView} from '../../components' -import {defaultStackNavigationOptions, ReviewTransactionRoutes} from '../../kernel/navigation' -import {ReviewTransactionScreen} from './useCases/ReviewTransactionScreen/ReviewTransactionScreen' - -const Stack = createStackNavigator() - -export const ReviewTransactionNavigator = () => { - const {atoms, color} = useTheme() - const styles = useStyles() - - return ( - - - - - - ) -} - -const useStyles = () => { - const {color, atoms} = useTheme() - const styles = StyleSheet.create({ - root: { - ...atoms.flex_1, - backgroundColor: color.bg_color_max, - }, - }) - return styles -} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx index d93d4a573d..93de538741 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx @@ -1,10 +1,10 @@ import {isNonNullable} from '@yoroi/common' import {infoExtractName} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' import * as _ from 'lodash' -import {useTokenInfos} from '../../../yoroi-wallets/hooks' -import {asQuantity} from '../../../yoroi-wallets/utils' -import {formatAdaWithText, formatTokenWithText} from '../../../yoroi-wallets/utils/format' +import {asQuantity} from '../../../yoroi-wallets/utils/utils' +import {usePortfolioTokenInfos} from '../../Portfolio/common/hooks/usePortfolioTokenInfos' import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' import {TransactionBody} from './types' @@ -24,7 +24,7 @@ export const useFormattedTransaction = (data: TransactionBody) => { const inputTokenIds = inputs.flatMap((i) => { const receiveUTxO = getUtxoByTxIdAndIndex(i.transaction_id, i.index) - return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}`) ?? [] + return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? [] }) const outputTokenIds = outputs.flatMap((o) => { @@ -32,25 +32,25 @@ export const useFormattedTransaction = (data: TransactionBody) => { const policyIds = Object.keys(o.amount.multiasset) const tokenIds = policyIds.flatMap((policyId) => { const assetIds = Object.keys(o.amount.multiasset?.[policyId] ?? {}) - return assetIds.map((assetId) => `${policyId}.${assetId}`) + return assetIds.map((assetId) => `${policyId}.${assetId}` as Portfolio.Token.Id) }) return tokenIds }) - const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) - const tokenInfos = useTokenInfos({wallet, tokenIds}) + const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) + const {tokenInfos} = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true}) const formattedInputs = inputs.map((input) => { const receiveUTxO = getUtxoByTxIdAndIndex(input.transaction_id, input.index) const address = receiveUTxO?.receiver const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null - const coinText = coin != null ? formatAdaWithText(coin, wallet.primaryToken) : null const primaryAssets = - coinText != null + coin != null ? [ { - label: coinText, + name: wallet.portfolioPrimaryTokenInfo.name, + label: `${coin} ${wallet.portfolioPrimaryTokenInfo.name}`, quantity: coin, isPrimary: true, }, @@ -60,11 +60,12 @@ export const useFormattedTransaction = (data: TransactionBody) => { const multiAssets = receiveUTxO?.assets .map((a) => { - const tokenInfo = tokenInfos.find((t) => t.id === a.assetId) + const tokenInfo = tokenInfos?.get(a.assetId as Portfolio.Token.Id) if (!tokenInfo) return null const quantity = asQuantity(a.amount) return { - label: formatTokenWithText(quantity, tokenInfo), + name: infoExtractName(tokenInfo), + label: `${quantity} ${infoExtractName(tokenInfo)}`, quantity, isPrimary: false, } @@ -83,27 +84,25 @@ export const useFormattedTransaction = (data: TransactionBody) => { const formattedOutputs = outputs.map((output) => { const address = output.address const coin = asQuantity(output.amount.coin) - const coinText = formatAdaWithText(coin, wallet.primaryToken) - const primaryAssets = - coinText != null - ? [ - { - label: coinText, - quantity: coin, - isPrimary: true, - }, - ] - : [] + const primaryAssets = [ + { + name: wallet.portfolioPrimaryTokenInfo.name, + label: `${coin} ${wallet.portfolioPrimaryTokenInfo.name}`, + quantity: coin, + isPrimary: true, + }, + ] const multiAssets = output.amount.multiasset ? Object.entries(output.amount.multiasset).map(([policyId, assets]) => { return Object.entries(assets).map(([assetId, amount]) => { - const tokenInfo = tokenInfos.find((t) => t.id === `${policyId}.${assetId}`) + const tokenInfo = tokenInfos?.get(`${policyId}.${assetId}`) if (tokenInfo == null) return null const quantity = asQuantity(amount) return { name: infoExtractName(tokenInfo), + label: `${quantity} ${infoExtractName(tokenInfo)}`, quantity, isPrimary: false, } @@ -115,9 +114,16 @@ export const useFormattedTransaction = (data: TransactionBody) => { return {assets, address, ownAddress: address != null && isOwnedAddress(address)} }) - const formattedFee = formatAdaWithText(asQuantity(data?.fee ?? '0'), wallet.primaryToken) + const fee = asQuantity(data?.fee ?? '0') + + const formattedFee = { + name: wallet.portfolioPrimaryTokenInfo.name, + label: `${fee} ${wallet.portfolioPrimaryTokenInfo.name}`, + quantity: fee, + isPrimary: true, + } return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} } -export type formattedTx = ReturnType +export type FormattedTx = ReturnType diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts index 311b526bcf..899bb3aa5f 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts +++ b/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts @@ -73,7 +73,7 @@ export const multiAssetsOneReceiver: TransactionBody = { coin: '10000000', multiasset: { cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { - '43415354': '10', + '43415354': '5', }, f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a: { '000de1406a6176696275656e6f': '1', @@ -102,7 +102,7 @@ export const multiAssetsOneReceiver: TransactionBody = { '484f534b59': '115930085', }, cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { - '43415354': '4498', + '43415354': '4503', }, e0c4c2d7c4a0ed2cf786753fd845dee82c45512cee03e92adfd3fb8d: { '6a6176696275656e6f2e616461': '1', @@ -127,7 +127,7 @@ export const multiAssetsOneReceiver: TransactionBody = { }, ], fee: '189349', - ttl: '220396208', + ttl: '93045', certs: null, withdrawals: null, update: null, diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx index 11c34c223d..d7cb2d150d 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx +++ b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx @@ -4,17 +4,18 @@ import * as React from 'react' import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import Svg, {Defs, Image, Pattern, Rect, SvgProps, Use} from 'react-native-svg' -import {Icon} from '../../../../../components' +import {Icon} from '../../../../../components/Icon' import {Space} from '../../../../../components/Space/Space' -import {Warning} from '../../../../../components/Warning' +import {Warning} from '../../../../../components/Warning/Warning' import {useCopy} from '../../../../../hooks/useCopy' import {useRewardAddress} from '../../../../../yoroi-wallets/hooks' +import {Quantities} from '../../../../../yoroi-wallets/utils/utils' import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' import {Divider} from '../../../common/Divider' -import {formattedTx} from '../../../common/formattedTransaction' +import {FormattedTx} from '../../../common/formattedTransaction' -export const OverviewTab = ({tx, createdBy}: {tx: formattedTx; createdBy?: React.ReactNode}) => { +export const OverviewTab = ({tx, createdBy}: {tx: FormattedTx; createdBy?: React.ReactNode}) => { const {styles} = useStyles() return ( @@ -33,7 +34,7 @@ export const OverviewTab = ({tx, createdBy}: {tx: formattedTx; createdBy?: React )} - + @@ -59,13 +60,14 @@ const WalletInfoItem = () => { const {wallet, meta} = useSelectedWallet() const {walletManager} = useWalletManager() const {plate, seed} = walletManager.checksum(wallet.publicKeyHex) + const seedImage = new Blockies({seed}).asBase64() return ( Wallet - + @@ -108,7 +110,7 @@ const CreatedByInfoItem = () => { ) } -const SenderTokensSection = ({tx}: {tx: formattedTx}) => { +const SenderTokensSection = ({tx}: {tx: FormattedTx}) => { const {wallet} = useSelectedWallet() const rewardAddress = useRewardAddress(wallet) @@ -142,8 +144,31 @@ const Address = ({address}: {address: string}) => { ) } -const SenderTokensItems = ({tx}: {tx: formattedTx}) => { +const SenderTokensItems = ({tx}: {tx: FormattedTx}) => { const {styles} = useStyles() + const {wallet} = useSelectedWallet() + + const totalPrimaryTokenSent = React.useMemo( + () => + tx.outputs + .filter((output) => !output.ownAddress) + .flatMap((output) => output.assets.filter((asset) => asset.isPrimary)) + .reduce((previous, current) => Quantities.sum([previous, current.quantity]), Quantities.zero), + [tx.outputs], + ) + const totalPrimaryTokenSpent = React.useMemo( + () => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity]), + [totalPrimaryTokenSent, tx.fee.quantity], + ) + const totalPrimaryTokenSpentLabel = `${totalPrimaryTokenSpent} ${wallet.portfolioPrimaryTokenInfo.name}` + + const notPrimaryTokenSent = React.useMemo( + () => + tx.outputs + .filter((output) => !output.ownAddress) + .flatMap((output) => output.assets.filter((asset) => !asset.isPrimary)), + [tx.outputs], + ) return ( @@ -152,25 +177,11 @@ const SenderTokensItems = ({tx}: {tx: formattedTx}) => { - - - - - - - - - - - - - - - - - + - + {notPrimaryTokenSent.map((token) => ( + + ))} ) diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx index 38833abb88..36eeec40d0 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx +++ b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import {StyleSheet} from 'react-native' import {SafeArea} from '../../../../components/SafeArea' -import {Tab, Tabs} from '../../../../components/Tabs' +import {Tab, Tabs} from '../../../../components/Tabs/Tabs' import {Divider} from '../../common/Divider' import {useFormattedTransaction} from '../../common/formattedTransaction' import {multiAssetsOneReceiver} from '../../common/mocks' diff --git a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx index 7875a3457b..ed81857507 100644 --- a/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/features/Transactions/TxHistoryNavigator.tsx @@ -28,7 +28,7 @@ import {ReceiveProvider} from '../Receive/common/ReceiveProvider' import {DescribeSelectedAddressScreen} from '../Receive/useCases/DescribeSelectedAddressScreen' import {ListMultipleAddressesScreen} from '../Receive/useCases/ListMultipleAddressesScreen' import {RequestSpecificAmountScreen} from '../Receive/useCases/RequestSpecificAmountScreen' -import {ReviewTransactionNavigator} from '../ReviewTransaction/ReviewTransactionNavigator' +import {ReviewTransactionScreen} from '../ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen' import {CodeScannerButton} from '../Scan/common/CodeScannerButton' import {ScanCodeScreen} from '../Scan/useCases/ScanCodeScreen' import {ShowCameraPermissionDeniedScreen} from '../Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen' @@ -222,7 +222,7 @@ export const TxHistoryNavigator = () => { >>>>>> origin/develop +import {CardanoMobile} from '../wallets' const crashReportsStorageKey = 'sendCrashReports' diff --git a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json index c7bbf3a72a..676a07292c 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/features/Transactions/TxHistoryNavigator.json @@ -6,12 +6,12 @@ "start": { "line": 419, "column": 16, - "index": 15421 + "index": 15445 }, "end": { "line": 422, "column": 3, - "index": 15510 + "index": 15534 } }, { @@ -21,12 +21,12 @@ "start": { "line": 423, "column": 32, - "index": 15544 + "index": 15568 }, "end": { "line": 426, "column": 3, - "index": 15657 + "index": 15681 } }, { @@ -36,12 +36,12 @@ "start": { "line": 427, "column": 13, - "index": 15672 + "index": 15696 }, "end": { "line": 430, "column": 3, - "index": 15745 + "index": 15769 } }, { @@ -51,12 +51,12 @@ "start": { "line": 431, "column": 17, - "index": 15764 + "index": 15788 }, "end": { "line": 434, "column": 3, - "index": 15841 + "index": 15865 } }, { @@ -66,12 +66,12 @@ "start": { "line": 435, "column": 15, - "index": 15858 + "index": 15882 }, "end": { "line": 438, "column": 3, - "index": 15931 + "index": 15955 } }, { @@ -81,12 +81,12 @@ "start": { "line": 439, "column": 21, - "index": 15954 + "index": 15978 }, "end": { "line": 442, "column": 3, - "index": 16049 + "index": 16073 } }, { @@ -96,12 +96,12 @@ "start": { "line": 443, "column": 14, - "index": 16065 + "index": 16089 }, "end": { "line": 446, "column": 3, - "index": 16146 + "index": 16170 } }, { @@ -111,12 +111,12 @@ "start": { "line": 447, "column": 13, - "index": 16161 + "index": 16185 }, "end": { "line": 450, "column": 3, - "index": 16241 + "index": 16265 } }, { @@ -126,12 +126,12 @@ "start": { "line": 451, "column": 18, - "index": 16261 + "index": 16285 }, "end": { "line": 454, "column": 3, - "index": 16362 + "index": 16386 } }, { @@ -141,12 +141,12 @@ "start": { "line": 455, "column": 20, - "index": 16384 + "index": 16408 }, "end": { "line": 458, "column": 3, - "index": 16473 + "index": 16497 } }, { @@ -156,12 +156,12 @@ "start": { "line": 459, "column": 26, - "index": 16501 + "index": 16525 }, "end": { "line": 462, "column": 3, - "index": 16602 + "index": 16626 } }, { @@ -171,12 +171,12 @@ "start": { "line": 463, "column": 19, - "index": 16623 + "index": 16647 }, "end": { "line": 466, "column": 3, - "index": 16716 + "index": 16740 } }, { @@ -186,12 +186,12 @@ "start": { "line": 467, "column": 16, - "index": 16734 + "index": 16758 }, "end": { "line": 470, "column": 3, - "index": 16820 + "index": 16844 } }, { @@ -201,12 +201,12 @@ "start": { "line": 471, "column": 19, - "index": 16841 + "index": 16865 }, "end": { "line": 477, "column": 3, - "index": 17079 + "index": 17103 } }, { @@ -216,12 +216,12 @@ "start": { "line": 478, "column": 27, - "index": 17108 + "index": 17132 }, "end": { "line": 481, "column": 3, - "index": 17201 + "index": 17225 } }, { @@ -231,12 +231,12 @@ "start": { "line": 482, "column": 13, - "index": 17216 + "index": 17240 }, "end": { "line": 485, "column": 3, - "index": 17291 + "index": 17315 } }, { @@ -246,12 +246,12 @@ "start": { "line": 486, "column": 25, - "index": 17318 + "index": 17342 }, "end": { "line": 489, "column": 3, - "index": 17392 + "index": 17416 } }, { @@ -261,12 +261,12 @@ "start": { "line": 490, "column": 18, - "index": 17412 + "index": 17436 }, "end": { "line": 493, "column": 3, - "index": 17526 + "index": 17550 } }, { @@ -276,12 +276,12 @@ "start": { "line": 494, "column": 28, - "index": 17556 + "index": 17580 }, "end": { "line": 497, "column": 3, - "index": 17652 + "index": 17676 } }, { @@ -291,12 +291,12 @@ "start": { "line": 498, "column": 29, - "index": 17683 + "index": 17707 }, "end": { "line": 501, "column": 3, - "index": 17791 + "index": 17815 } }, { @@ -306,12 +306,12 @@ "start": { "line": 502, "column": 30, - "index": 17823 + "index": 17847 }, "end": { "line": 505, "column": 3, - "index": 17933 + "index": 17957 } }, { @@ -321,12 +321,12 @@ "start": { "line": 506, "column": 18, - "index": 17953 + "index": 17977 }, "end": { "line": 509, "column": 3, - "index": 18047 + "index": 18071 } } ] \ No newline at end of file From 7c43a3082d211023db2fcee8e260fbd389544afb Mon Sep 17 00:00:00 2001 From: Javier Bueno Date: Fri, 20 Sep 2024 11:03:11 +0200 Subject: [PATCH 03/23] feat(tx-review): add confirm tx to send funnel --- apps/wallet-mobile/src/WalletNavigator.tsx | 3 + .../ReviewTransaction/ReviewTransaction.tsx | 2 +- .../common/formattedTransaction.tsx | 129 ----- .../Overview/OverviewTab.tsx | 479 ------------------ .../ReviewTransactionScreen.tsx | 59 --- .../features/ReviewTx/ReviewTxNavigator.tsx | 34 ++ .../src/features/ReviewTx/common/Address.tsx | 46 ++ .../ReviewTx/common/CollapsibleSection.tsx | 44 ++ .../common/Divider.tsx | 16 +- .../features/ReviewTx/common/TokenItem.tsx | 76 +++ .../ReviewTx/common/hooks/useAddressType.tsx | 12 + .../ReviewTx/common/hooks/useFormattedTx.tsx | 228 +++++++++ .../ReviewTx/common/hooks/useOnConfirm.tsx | 71 +++ .../ReviewTx/common/hooks/useStrings.tsx | 11 + .../ReviewTx/common/hooks/useTxBody.tsx | 44 ++ .../common/mocks.ts | 2 +- .../common/types.ts | 39 ++ .../ReviewTxScreen/Overview/OverviewTab.tsx | 344 +++++++++++++ .../ReviewTxScreen/ReviewTxScreen.tsx | 95 ++++ .../ListAmountsToSendScreen.tsx | 28 +- .../Transactions/TxHistoryNavigator.tsx | 4 +- apps/wallet-mobile/src/kernel/navigation.tsx | 29 +- .../src/yoroi-wallets/cardano/utils.ts | 41 ++ .../src/yoroi-wallets/hooks/index.ts | 15 - .../messages/src/WalletNavigator.json | 96 ++-- .../ListAmountsToSendScreen.json | 8 +- .../Transactions/TxHistoryNavigator.json | 88 ++-- 27 files changed, 1245 insertions(+), 798 deletions(-) delete mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx delete mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx delete mode 100644 apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/Address.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/CollapsibleSection.tsx rename apps/wallet-mobile/src/features/{ReviewTransaction => ReviewTx}/common/Divider.tsx (51%) create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx rename apps/wallet-mobile/src/features/{ReviewTransaction => ReviewTx}/common/mocks.ts (98%) rename apps/wallet-mobile/src/features/{ReviewTransaction => ReviewTx}/common/types.ts (95%) create mode 100644 apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx diff --git a/apps/wallet-mobile/src/WalletNavigator.tsx b/apps/wallet-mobile/src/WalletNavigator.tsx index 8a1345c895..bb5620f4b1 100644 --- a/apps/wallet-mobile/src/WalletNavigator.tsx +++ b/apps/wallet-mobile/src/WalletNavigator.tsx @@ -25,6 +25,7 @@ import {useLinksShowActionResult} from './features/Links/common/useLinksShowActi import {MenuNavigator} from './features/Menu/Menu' import {PortfolioNavigator} from './features/Portfolio/PortfolioNavigator' import {CatalystNavigator} from './features/RegisterCatalyst/CatalystNavigator' +import {ReviewTxNavigator} from './features/ReviewTx/ReviewTxNavigator' import {SearchProvider} from './features/Search/SearchContext' import {SettingsScreenNavigator} from './features/Settings' import {NetworkTag} from './features/Settings/ChangeNetwork/NetworkTag' @@ -260,6 +261,8 @@ export const WalletNavigator = () => { + + { }, [promptRootKey]) } -const useSignTxWithHW = () => { +export const useSignTxWithHW = () => { const {confirmHWConnection, closeModal} = useConfirmHWConnectionModal() const {wallet, meta} = useSelectedWallet() diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx deleted file mode 100644 index 93de538741..0000000000 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/formattedTransaction.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import {isNonNullable} from '@yoroi/common' -import {infoExtractName} from '@yoroi/portfolio' -import {Portfolio} from '@yoroi/types' -import * as _ from 'lodash' - -import {asQuantity} from '../../../yoroi-wallets/utils/utils' -import {usePortfolioTokenInfos} from '../../Portfolio/common/hooks/usePortfolioTokenInfos' -import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' -import {TransactionBody} from './types' - -export const useFormattedTransaction = (data: TransactionBody) => { - const {wallet} = useSelectedWallet() - - const inputs = data?.inputs ?? [] - const outputs = data?.outputs ?? [] - - const getUtxoByTxIdAndIndex = (txId: string, index: number) => { - return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) - } - - const isOwnedAddress = (bech32Address: string) => { - return wallet.internalAddresses.includes(bech32Address) || wallet.externalAddresses.includes(bech32Address) - } - - const inputTokenIds = inputs.flatMap((i) => { - const receiveUTxO = getUtxoByTxIdAndIndex(i.transaction_id, i.index) - return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? [] - }) - - const outputTokenIds = outputs.flatMap((o) => { - if (!o.amount.multiasset) return [] - const policyIds = Object.keys(o.amount.multiasset) - const tokenIds = policyIds.flatMap((policyId) => { - const assetIds = Object.keys(o.amount.multiasset?.[policyId] ?? {}) - return assetIds.map((assetId) => `${policyId}.${assetId}` as Portfolio.Token.Id) - }) - return tokenIds - }) - - const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) - const {tokenInfos} = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true}) - - const formattedInputs = inputs.map((input) => { - const receiveUTxO = getUtxoByTxIdAndIndex(input.transaction_id, input.index) - const address = receiveUTxO?.receiver - const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null - - const primaryAssets = - coin != null - ? [ - { - name: wallet.portfolioPrimaryTokenInfo.name, - label: `${coin} ${wallet.portfolioPrimaryTokenInfo.name}`, - quantity: coin, - isPrimary: true, - }, - ] - : [] - - const multiAssets = - receiveUTxO?.assets - .map((a) => { - const tokenInfo = tokenInfos?.get(a.assetId as Portfolio.Token.Id) - if (!tokenInfo) return null - const quantity = asQuantity(a.amount) - return { - name: infoExtractName(tokenInfo), - label: `${quantity} ${infoExtractName(tokenInfo)}`, - quantity, - isPrimary: false, - } - }) - .filter(Boolean) ?? [] - - return { - assets: [...primaryAssets, ...multiAssets].filter(isNonNullable), - address, - ownAddress: address != null && isOwnedAddress(address), - txIndex: input.index, - txHash: input.transaction_id, - } - }) - - const formattedOutputs = outputs.map((output) => { - const address = output.address - const coin = asQuantity(output.amount.coin) - - const primaryAssets = [ - { - name: wallet.portfolioPrimaryTokenInfo.name, - label: `${coin} ${wallet.portfolioPrimaryTokenInfo.name}`, - quantity: coin, - isPrimary: true, - }, - ] - - const multiAssets = output.amount.multiasset - ? Object.entries(output.amount.multiasset).map(([policyId, assets]) => { - return Object.entries(assets).map(([assetId, amount]) => { - const tokenInfo = tokenInfos?.get(`${policyId}.${assetId}`) - if (tokenInfo == null) return null - const quantity = asQuantity(amount) - return { - name: infoExtractName(tokenInfo), - label: `${quantity} ${infoExtractName(tokenInfo)}`, - quantity, - isPrimary: false, - } - }) - }) - : [] - - const assets = [...primaryAssets, ...multiAssets.flat()].filter(isNonNullable) - return {assets, address, ownAddress: address != null && isOwnedAddress(address)} - }) - - const fee = asQuantity(data?.fee ?? '0') - - const formattedFee = { - name: wallet.portfolioPrimaryTokenInfo.name, - label: `${fee} ${wallet.portfolioPrimaryTokenInfo.name}`, - quantity: fee, - isPrimary: true, - } - - return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} -} - -export type FormattedTx = ReturnType diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx deleted file mode 100644 index d7cb2d150d..0000000000 --- a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/Overview/OverviewTab.tsx +++ /dev/null @@ -1,479 +0,0 @@ -import {Blockies} from '@yoroi/identicon' -import {useTheme} from '@yoroi/theme' -import * as React from 'react' -import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' -import Svg, {Defs, Image, Pattern, Rect, SvgProps, Use} from 'react-native-svg' - -import {Icon} from '../../../../../components/Icon' -import {Space} from '../../../../../components/Space/Space' -import {Warning} from '../../../../../components/Warning/Warning' -import {useCopy} from '../../../../../hooks/useCopy' -import {useRewardAddress} from '../../../../../yoroi-wallets/hooks' -import {Quantities} from '../../../../../yoroi-wallets/utils/utils' -import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' -import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' -import {Divider} from '../../../common/Divider' -import {FormattedTx} from '../../../common/formattedTransaction' - -export const OverviewTab = ({tx, createdBy}: {tx: FormattedTx; createdBy?: React.ReactNode}) => { - const {styles} = useStyles() - - return ( - - - - - - - - {createdBy !== undefined && ( - <> - - - - - )} - - - - - - - - - - - - - - - - - - - - ) -} - -const WalletInfoItem = () => { - const {styles} = useStyles() - const {wallet, meta} = useSelectedWallet() - const {walletManager} = useWalletManager() - const {plate, seed} = walletManager.checksum(wallet.publicKeyHex) - const seedImage = new Blockies({seed}).asBase64() - - return ( - - Wallet - - - - - - - {`${plate} | ${meta.name}`} - - - ) -} - -const FeeInfoItem = ({fee}: {fee: string}) => { - const {styles} = useStyles() - - return ( - - Fee - - {fee} - - ) -} - -// TODO (for dapps) -const CreatedByInfoItem = () => { - const {styles} = useStyles() - - return ( - - Created By - - - - - - - Linking.openURL('https://google.com')}> - dapp.org - - - - ) -} - -const SenderTokensSection = ({tx}: {tx: FormattedTx}) => { - const {wallet} = useSelectedWallet() - const rewardAddress = useRewardAddress(wallet) - - return ( - - - -
- - - - - - ) -} - -const Address = ({address}: {address: string}) => { - const {styles, colors} = useStyles() - const [, copy] = useCopy() - - return ( - - - {address} - - - copy(address)} activeOpacity={0.5}> - - - - ) -} - -const SenderTokensItems = ({tx}: {tx: FormattedTx}) => { - const {styles} = useStyles() - const {wallet} = useSelectedWallet() - - const totalPrimaryTokenSent = React.useMemo( - () => - tx.outputs - .filter((output) => !output.ownAddress) - .flatMap((output) => output.assets.filter((asset) => asset.isPrimary)) - .reduce((previous, current) => Quantities.sum([previous, current.quantity]), Quantities.zero), - [tx.outputs], - ) - const totalPrimaryTokenSpent = React.useMemo( - () => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity]), - [totalPrimaryTokenSent, tx.fee.quantity], - ) - const totalPrimaryTokenSpentLabel = `${totalPrimaryTokenSpent} ${wallet.portfolioPrimaryTokenInfo.name}` - - const notPrimaryTokenSent = React.useMemo( - () => - tx.outputs - .filter((output) => !output.ownAddress) - .flatMap((output) => output.assets.filter((asset) => !asset.isPrimary)), - [tx.outputs], - ) - - return ( - - - - - - - - - {notPrimaryTokenSent.map((token) => ( - - ))} - - - ) -} - -const SenderTokensSectionLabel = () => { - const {styles, colors} = useStyles() - - return ( - - - - - - Send - - ) -} - -const ReceiverTokensSectionLabel = () => { - const {styles, colors} = useStyles() - - return ( - - - - - - Receive - - ) -} - -const ReceiverTokensSection = () => { - const {styles, colors} = useStyles() - - const isRegularAdress = true - const isMultiReceiver = true - - if (isMultiReceiver) { - return ( - <> - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) - } - - return ( - <> - - - - {isRegularAdress ? `To` : 'To script'}: - - - stake1u948jr02falxxqphnv3g3rkd3mdzqmtqq3x0tjl39m7dqngqg0fxp - - - - - - ) -} - -const TokenItem = ({ - isPrimaryToken = true, - isSent = true, - value, -}: { - isPrimaryToken?: boolean - isSent?: boolean - value: string -}) => { - const {styles} = useStyles() - - if (!isSent) - return ( - - - {value} - - - ) - - return ( - - {value} - - ) -} - -const CollapsibleSection = ({label, children}: {label: string; children: React.ReactNode}) => { - const {styles, colors} = useStyles() - const [isOpen, setIsOpen] = React.useState(false) - - return ( - <> - - {label} - - setIsOpen((isOpen) => !isOpen)}> - - - - - {isOpen && children} - - ) -} - -const useStyles = () => { - const {atoms, color} = useTheme() - const styles = StyleSheet.create({ - root: { - ...atoms.px_lg, - }, - infoItem: { - ...atoms.flex_row, - ...atoms.justify_between, - }, - infoLabel: { - ...atoms.body_2_md_regular, - color: color.gray_600, - }, - walletInfoText: { - ...atoms.body_2_md_medium, - color: color.text_primary_medium, - }, - plate: { - ...atoms.flex_row, - ...atoms.align_center, - }, - fee: { - color: color.gray_900, - ...atoms.body_2_md_regular, - }, - link: { - color: color.text_primary_medium, - ...atoms.body_2_md_medium, - }, - sectionHeader: { - ...atoms.flex_row, - ...atoms.justify_between, - }, - myWalletAddress: { - ...atoms.flex_row, - ...atoms.align_center, - ...atoms.flex_row, - ...atoms.justify_between, - }, - myWalletAddressText: { - ...atoms.flex_1, - ...atoms.body_2_md_regular, - color: color.gray_900, - }, - sectionHeaderText: { - ...atoms.body_1_lg_medium, - color: color.gray_900, - }, - tokenSectionLabel: { - ...atoms.body_2_md_regular, - color: color.gray_900, - }, - sentTokenItem: { - ...atoms.flex, - ...atoms.flex_row, - ...atoms.align_center, - ...atoms.py_xs, - ...atoms.px_md, - borderRadius: 8, - backgroundColor: color.primary_500, - }, - receivedTokenItem: { - ...atoms.flex, - ...atoms.flex_row, - ...atoms.align_center, - ...atoms.py_xs, - ...atoms.px_md, - borderRadius: 8, - backgroundColor: color.secondary_300, - }, - senderTokenItems: { - ...atoms.flex_wrap, - ...atoms.flex_row, - ...atoms.justify_end, - ...atoms.flex_1, - gap: 8, - }, - tokenSentItemText: { - ...atoms.body_2_md_regular, - color: color.white_static, - }, - tokenReceivedItemText: { - ...atoms.body_2_md_regular, - color: color.text_gray_max, - }, - notPrimarySentTokenItem: { - backgroundColor: color.primary_100, - }, - notPrimaryReceivedTokenItem: { - backgroundColor: color.secondary_100, - }, - notPrimarySentTokenItemText: { - color: color.text_primary_medium, - }, - notPrimaryReceivedTokenItemText: { - color: color.secondary_700, - }, - tokensSection: { - ...atoms.flex_row, - ...atoms.justify_between, - }, - tokensSectionLabel: { - ...atoms.flex_row, - ...atoms.align_center, - }, - walletChecksum: { - width: 24, - height: 24, - }, - }) - - const colors = { - copy: color.gray_900, - chevron: color.gray_900, - send: color.primary_500, - received: color.green_static, - } - - return {styles, colors} as const -} - -function SvgComponent(props: SvgProps) { - return ( - - - - - - - - - - - - ) -} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx b/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx deleted file mode 100644 index 36eeec40d0..0000000000 --- a/apps/wallet-mobile/src/features/ReviewTransaction/useCases/ReviewTransactionScreen/ReviewTransactionScreen.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import {useTheme} from '@yoroi/theme' -import * as React from 'react' -import {StyleSheet} from 'react-native' - -import {SafeArea} from '../../../../components/SafeArea' -import {Tab, Tabs} from '../../../../components/Tabs/Tabs' -import {Divider} from '../../common/Divider' -import {useFormattedTransaction} from '../../common/formattedTransaction' -import {multiAssetsOneReceiver} from '../../common/mocks' -import {OverviewTab} from './Overview/OverviewTab' - -export const ReviewTransactionScreen = () => { - const {styles} = useStyles() - const [activeTab, setActiveTab] = React.useState('overview') - const formatedTx = useFormattedTransaction(multiAssetsOneReceiver) - - console.log('tx', JSON.stringify(formatedTx, null, 2)) - - const renderTabs = React.useMemo(() => { - return ( - - setActiveTab('overview')} - label="Overview" - /> - - setActiveTab('utxos')} label="UTxOs" /> - - ) - }, [activeTab, setActiveTab, styles.tab, styles.tabs]) - - return ( - - {renderTabs} - - - - {activeTab === 'overview' && } - - ) -} - -const useStyles = () => { - const {atoms, color} = useTheme() - const styles = StyleSheet.create({ - tabs: { - ...atoms.px_lg, - ...atoms.gap_lg, - backgroundColor: color.bg_color_max, - }, - tab: { - flex: 0, - }, - }) - - return {styles} as const -} diff --git a/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx new file mode 100644 index 0000000000..9a0e390ca5 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx @@ -0,0 +1,34 @@ +import {createStackNavigator} from '@react-navigation/stack' +import {Atoms, ThemedPalette, useTheme} from '@yoroi/theme' +import React from 'react' + +import {Boundary} from '../../components/Boundary/Boundary' +import {defaultStackNavigationOptions, ReviewTxRoutes} from '../../kernel/navigation' +import {ReviewTxScreen} from './useCases/ReviewTxScreen/ReviewTxScreen' + +export const Stack = createStackNavigator() + +export const ReviewTxNavigator = () => { + const {atoms, color} = useTheme() + + return ( + + + {() => ( + + + + )} + + + ) +} + +const screenOptions = (atoms: Atoms, color: ThemedPalette) => ({ + ...defaultStackNavigationOptions(atoms, color), + gestureEnabled: true, +}) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/Address.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/Address.tsx new file mode 100644 index 0000000000..aad8711413 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/Address.tsx @@ -0,0 +1,46 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, TextStyle, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../components/Icon' +import {useCopy} from '../../../hooks/useCopy' + +export const Address = ({address, textStyle}: {address: string; textStyle?: TextStyle}) => { + const {styles, colors} = useStyles() + const [, copy] = useCopy() + + return ( + + + {address} + + + copy(address)} activeOpacity={0.5}> + + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + address: { + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.flex_row, + ...atoms.justify_between, + }, + addressText: { + ...atoms.flex_1, + ...atoms.body_2_md_regular, + color: color.gray_900, + }, + }) + + const colors = { + copy: color.gray_900, + } + + return {styles, colors} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/CollapsibleSection.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/CollapsibleSection.tsx new file mode 100644 index 0000000000..0bb8691140 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/CollapsibleSection.tsx @@ -0,0 +1,44 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../components/Icon' + +export const CollapsibleSection = ({label, children}: {label: string; children: React.ReactNode}) => { + const {styles, colors} = useStyles() + const [isOpen, setIsOpen] = React.useState(false) + + return ( + <> + + {label} + + setIsOpen((isOpen) => !isOpen)}> + + + + + {isOpen && children} + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + sectionHeader: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + sectionHeaderText: { + ...atoms.body_1_lg_medium, + color: color.gray_900, + }, + }) + + const colors = { + chevron: color.gray_900, + } + + return {styles, colors} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx similarity index 51% rename from apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx rename to apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx index c53f95c9cd..1733930fda 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/Divider.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx @@ -1,10 +1,20 @@ -import {useTheme} from '@yoroi/theme' +import {SpacingSize, useTheme} from '@yoroi/theme' import * as React from 'react' import {StyleSheet, View} from 'react-native' -export const Divider = () => { +import {Space} from '../../../components/Space/Space' + +export const Divider = ({verticalSpace = 'none'}: {verticalSpace?: SpacingSize}) => { const {styles} = useStyles() - return + return ( + <> + + + + + + + ) } const useStyles = () => { diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx new file mode 100644 index 0000000000..751a3bbd56 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx @@ -0,0 +1,76 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +export const TokenItem = ({ + isPrimaryToken = true, + isSent = true, + label, +}: { + isPrimaryToken?: boolean + isSent?: boolean + label: string +}) => { + const {styles} = useStyles() + + if (!isSent) + return ( + + + {label} + + + ) + + return ( + + {label} + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + sentTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.primary_500, + }, + receivedTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.secondary_300, + }, + tokenSentItemText: { + ...atoms.body_2_md_regular, + color: color.white_static, + }, + tokenReceivedItemText: { + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + notPrimarySentTokenItem: { + backgroundColor: color.primary_100, + }, + notPrimaryReceivedTokenItem: { + backgroundColor: color.secondary_100, + }, + notPrimarySentTokenItemText: { + color: color.text_primary_medium, + }, + notPrimaryReceivedTokenItemText: { + color: color.secondary_700, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx new file mode 100644 index 0000000000..debc3efe06 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx @@ -0,0 +1,12 @@ +import {useQuery} from 'react-query' + +import {getAddressType} from '../../../../yoroi-wallets/cardano/utils' + +export const useAddressType = (address: string) => { + const query = useQuery(['useAddressType', address], () => getAddressType(address), { + suspense: true, + }) + + if (query.data === undefined) throw new Error('invalid formatted outputs') + return query.data +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx new file mode 100644 index 0000000000..9770e71a55 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx @@ -0,0 +1,228 @@ +import {invalid, isNonNullable} from '@yoroi/common' +import {infoExtractName} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' +import * as _ from 'lodash' +import {useQuery} from 'react-query' + +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' +import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl' +import {formatTokenWithText} from '../../../../yoroi-wallets/utils/format' +import {asQuantity} from '../../../../yoroi-wallets/utils/utils' +import {usePortfolioTokenInfos} from '../../../Portfolio/common/hooks/usePortfolioTokenInfos' +import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' +import { + FormattedFee, + FormattedInputs, + FormattedOutputs, + TransactionBody, + TransactionInputs, + TransactionOutputs, +} from '../types' + +export const useFormattedTx = (data: TransactionBody) => { + const {wallet} = useSelectedWallet() + + const inputs = data?.inputs ?? [] + const outputs = data?.outputs ?? [] + + const inputTokenIds = inputs.flatMap((i) => { + const receiveUTxO = getUtxoByTxIdAndIndex(wallet, i.transaction_id, i.index) + return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? [] + }) + + const outputTokenIds = outputs.flatMap((o) => { + if (!o.amount.multiasset) return [] + const policyIds = Object.keys(o.amount.multiasset) + const tokenIds = policyIds.flatMap((policyId) => { + const assetIds = Object.keys(o.amount.multiasset?.[policyId] ?? {}) + return assetIds.map((assetId) => `${policyId}.${assetId}` as Portfolio.Token.Id) + }) + return tokenIds + }) + + const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) + const portfolioTokenInfos = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true}) + + const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos) + const formattedOutputs = useFormattedOutputs(wallet, outputs, portfolioTokenInfos) + const formattedFee = formatFee(wallet, data) + + return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} +} + +export const useFormattedInputs = ( + wallet: YoroiWallet, + inputs: TransactionInputs, + tokenInfosResult: ReturnType, +) => { + const query = useQuery( + ['useFormattedInputs', inputs], + async () => { + const inputss = await formatInputs(wallet, inputs, tokenInfosResult) + console.log('inputs', inputs) + console.log('inputss', inputss) + return inputss + }, + { + suspense: true, + }, + ) + + if (!query.data) throw new Error('invalid formatted inputs') + return query.data +} + +export const useFormattedOutputs = ( + wallet: YoroiWallet, + outputs: TransactionOutputs, + portfolioTokenInfos: ReturnType, +) => { + const query = useQuery( + ['useFormattedOutputs', outputs], + () => formatOutputs(wallet, outputs, portfolioTokenInfos), + { + suspense: true, + }, + ) + + if (!query.data) throw new Error('invalid formatted outputs') + return query.data +} + +const formatInputs = async ( + wallet: YoroiWallet, + inputs: TransactionInputs, + portfolioTokenInfos: ReturnType, +): Promise => { + return Promise.all( + inputs.map(async (input) => { + const receiveUTxO = getUtxoByTxIdAndIndex(wallet, input.transaction_id, input.index) + const address = receiveUTxO?.receiver + const rewardAddress = + address !== undefined ? await deriveRewardAddressFromAddress(address, wallet.networkManager.chainId) : null + const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null + + const primaryAssets = + coin != null + ? [ + { + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(coin, wallet.portfolioPrimaryTokenInfo), + quantity: coin, + isPrimary: true, + }, + ] + : [] + + const multiAssets = + receiveUTxO?.assets + .map((a) => { + const tokenInfo = portfolioTokenInfos.tokenInfos?.get(a.assetId as Portfolio.Token.Id) + if (!tokenInfo) return null + const quantity = asQuantity(a.amount) + return { + name: infoExtractName(tokenInfo), + label: formatTokenWithText(quantity, tokenInfo), + quantity, + isPrimary: false, + } + }) + .filter(Boolean) ?? [] + + return { + assets: [...primaryAssets, ...multiAssets].filter(isNonNullable), + address, + rewardAddress, + ownAddress: address != null && isOwnedAddress(wallet, address), + txIndex: input.index, + txHash: input.transaction_id, + } + }), + ) +} + +const formatOutputs = async ( + wallet: YoroiWallet, + outputs: TransactionOutputs, + portfolioTokenInfos: ReturnType, +): Promise => { + return Promise.all( + outputs.map(async (output) => { + const address = output.address + const rewardAddress = await deriveRewardAddressFromAddress(address, wallet.networkManager.chainId) + const coin = asQuantity(output.amount.coin) + + const primaryAssets = [ + { + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(coin, wallet.portfolioPrimaryTokenInfo), + quantity: coin, + isPrimary: true, + }, + ] + + const multiAssets = output.amount.multiasset + ? Object.entries(output.amount.multiasset).flatMap(([policyId, assets]) => { + return Object.entries(assets).map(([assetId, amount]) => { + const tokenInfo = portfolioTokenInfos.tokenInfos?.get(`${policyId}.${assetId}`) + if (tokenInfo == null) return null + const quantity = asQuantity(amount) + return { + name: infoExtractName(tokenInfo), + label: formatTokenWithText(quantity, tokenInfo), + quantity, + isPrimary: false, + } + }) + }) + : [] + + const assets = [...primaryAssets, ...multiAssets].filter(isNonNullable) + + return { + assets, + address, + rewardAddress, + ownAddress: isOwnedAddress(wallet, address), + } + }), + ) +} + +export const formatFee = (wallet: YoroiWallet, data: TransactionBody): FormattedFee => { + const fee = asQuantity(data?.fee ?? '0') + + return { + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(fee, wallet.portfolioPrimaryTokenInfo), + quantity: fee, + isPrimary: true, + } +} + +export const deriveRewardAddressFromAddress = async (address: string, chainId: number): Promise => { + const {csl, release} = wrappedCsl() + + try { + const result = await csl.Address.fromBech32(address) + .then((address) => csl.BaseAddress.fromAddress(address)) + .then((baseAddress) => baseAddress?.stakeCred() ?? invalid('invalid base address')) + .then((stakeCredential) => csl.RewardAddress.new(chainId, stakeCredential)) + .then((rewardAddress) => rewardAddress.toAddress()) + .then((rewardAddrAsAddress) => rewardAddrAsAddress.toBech32(undefined)) + .catch((error) => error) + + if (typeof result !== 'string') throw new Error('Its not possible to derive reward address') + return result + } finally { + release() + } +} + +const getUtxoByTxIdAndIndex = (wallet: YoroiWallet, txId: string, index: number) => { + return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) +} + +const isOwnedAddress = (wallet: YoroiWallet, bech32Address: string) => { + return wallet.internalAddresses.includes(bech32Address) || wallet.externalAddresses.includes(bech32Address) +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx new file mode 100644 index 0000000000..dfb076eab5 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' + +import {ConfirmTxWithHwModal} from '../../../../components/ConfirmTxWithHwModal/ConfirmTxWithHwModal' +import {ConfirmTxWithOsModal} from '../../../../components/ConfirmTxWithOsModal/ConfirmTxWithOsModal' +import {ConfirmTxWithSpendingPasswordModal} from '../../../../components/ConfirmTxWithSpendingPasswordModal/ConfirmTxWithSpendingPasswordModal' +import {useModal} from '../../../../components/Modal/ModalContext' +import {YoroiSignedTx, YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' +import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' +import {useStrings} from './useStrings' + +// TODO: make it compatible with CBOR signing +export const useOnConfirm = ({ + unsignedTx, + onSuccess, + onError, + onNotSupportedCIP1694, +}: { + onSuccess: (txId: YoroiSignedTx) => void + onError: () => void + cbor?: string + unsignedTx?: YoroiUnsignedTx + onNotSupportedCIP1694?: () => void +}) => { + if (unsignedTx === undefined) throw new Error('useOnConfirm: unsignedTx missing') + + const {meta} = useSelectedWallet() + const {openModal, closeModal} = useModal() + const strings = useStrings() + + const onConfirm = () => { + if (meta.isHW) { + openModal( + strings.signTransaction, + onSuccess(signedTx)} + onNotSupportedCIP1694={onNotSupportedCIP1694} + />, + 400, + ) + return + } + + if (!meta.isHW && !meta.isEasyConfirmationEnabled) { + openModal( + strings.signTransaction, + onSuccess(signedTx)} + onError={onError} + />, + ) + return + } + + if (!meta.isHW && meta.isEasyConfirmationEnabled) { + openModal( + strings.signTransaction, + onSuccess(signedTx)} + onError={onError} + />, + ) + return + } + } + + return {onConfirm} +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx new file mode 100644 index 0000000000..0513fdd25f --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx @@ -0,0 +1,11 @@ +import {useIntl} from 'react-intl' + +import {txLabels} from '../../../../kernel/i18n/global-messages' + +export const useStrings = () => { + const intl = useIntl() + + return { + signTransaction: intl.formatMessage(txLabels.signingTx), + } +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx new file mode 100644 index 0000000000..9de33bb075 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx @@ -0,0 +1,44 @@ +import {useQuery} from 'react-query' + +import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl' +import {YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' + +export const useTxBody = ({cbor, unsignedTx}: {cbor?: string; unsignedTx?: YoroiUnsignedTx}) => { + const query = useQuery( + ['useTxBody', cbor, unsignedTx], + async () => { + if (cbor !== undefined) { + return getCborTxBody(cbor) + } else if (unsignedTx !== undefined) { + return getUnsignedTxTxBody(unsignedTx) + } else { + throw new Error('useTxBody: missing cbor and unsignedTx') + } + }, + { + useErrorBoundary: true, + suspense: true, + }, + ) + + if (query.data === undefined) throw new Error('useTxBody: cannot extract txBody') + return query.data +} +const getCborTxBody = async (cbor: string) => { + const {csl, release} = wrappedCsl() + try { + const tx = await csl.Transaction.fromHex(cbor) + const jsonString = await tx.toJson() + return JSON.parse(jsonString).body + } finally { + release() + } +} + +const getUnsignedTxTxBody = async (unsignedTx: YoroiUnsignedTx) => { + const { + unsignedTx: {txBody}, + } = unsignedTx + const txBodyjson = await txBody.toJson() + return JSON.parse(txBodyjson) +} diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts similarity index 98% rename from apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts rename to apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts index 899bb3aa5f..12fbe9274a 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/mocks.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts @@ -54,7 +54,7 @@ export const adaTransactionSingleReceiver: TransactionBody = { current_treasury_value: null, } -export const multiAssetsOneReceiver: TransactionBody = { +export const multiAssetsTransactionOneReceiver: TransactionBody = { inputs: [ { transaction_id: '46fe71d85a733d970fe7bb8e6586624823803936d18c7e14601713d05b5b287a', diff --git a/apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts similarity index 95% rename from apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts rename to apps/wallet-mobile/src/features/ReviewTx/common/types.ts index 0c6847d747..52785e8c48 100644 --- a/apps/wallet-mobile/src/features/ReviewTransaction/common/types.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts @@ -1,3 +1,7 @@ +import {Balance} from '@yoroi/types' + +import {useFormattedTx} from './hooks/useFormattedTx' + export type TransactionDetails = { id: string walletPlate: React.ReactNode @@ -853,3 +857,38 @@ export type VotingProposals = VotingProposal[] export interface Withdrawals { [k: string]: string } + +export type FormattedInput = { + assets: Array<{ + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean + }> + address: string | undefined + rewardAddress: string | null + ownAddress: boolean + txIndex: number + txHash: string +} + +export type FormattedInputs = Array +export type FormattedTx = ReturnType +export type FormattedOutput = { + assets: Array<{ + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean + }> + address: string + rewardAddress: string | null + ownAddress: boolean +} +export type FormattedOutputs = Array +export type FormattedFee = { + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx new file mode 100644 index 0000000000..42b46ba0d5 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx @@ -0,0 +1,344 @@ +import {Blockies} from '@yoroi/identicon' +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../../../components/Icon' +import {Space} from '../../../../../components/Space/Space' +import {Warning} from '../../../../../components/Warning/Warning' +import {formatTokenWithText} from '../../../../../yoroi-wallets/utils/format' +import {Quantities} from '../../../../../yoroi-wallets/utils/utils' +import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' +import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' +import {Address} from '../../../common/Address' +import {CollapsibleSection} from '../../../common/CollapsibleSection' +import {Divider} from '../../../common/Divider' +import {useAddressType} from '../../../common/hooks/useAddressType' +import {TokenItem} from '../../../common/TokenItem' +import {FormattedOutputs, FormattedTx} from '../../../common/types' + +export const OverviewTab = ({tx}: {tx: FormattedTx}) => { + const {styles} = useStyles() + const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs]) + const ownedOutputs = React.useMemo(() => tx.outputs.filter((output) => output.ownAddress), [tx.outputs]) + + return ( + + + + + + + + + + ) +} + +const WalletInfoSection = ({tx}: {tx: FormattedTx}) => { + const {styles} = useStyles() + const {wallet, meta} = useSelectedWallet() + const {walletManager} = useWalletManager() + const {plate, seed} = walletManager.checksum(wallet.publicKeyHex) + const seedImage = new Blockies({seed}).asBase64() + + return ( + <> + + Wallet + + + + + + + {`${plate} | ${meta.name}`} + + + + + + + + ) +} + +const FeeInfoItem = ({fee}: {fee: string}) => { + const {styles} = useStyles() + + return ( + + Fee + + {fee} + + ) +} + +const SenderSection = ({ + tx, + notOwnedOutputs, + ownedOutputs, +}: { + tx: FormattedTx + notOwnedOutputs: FormattedOutputs + ownedOutputs: FormattedOutputs +}) => { + const address = ownedOutputs[0]?.rewardAddress ?? ownedOutputs[0]?.address + + return ( + + + +
+ + + + + + {notOwnedOutputs.length === 1 && } + + ) +} + +const SenderTokens = ({tx, notOwnedOutputs}: {tx: FormattedTx; notOwnedOutputs: FormattedOutputs}) => { + const {styles} = useStyles() + const {wallet} = useSelectedWallet() + + const totalPrimaryTokenSent = React.useMemo( + () => + notOwnedOutputs + .flatMap((output) => output.assets.filter((asset) => asset.isPrimary)) + .reduce((previous, current) => Quantities.sum([previous, current.quantity]), Quantities.zero), + [notOwnedOutputs], + ) + const totalPrimaryTokenSpent = React.useMemo( + () => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity]), + [totalPrimaryTokenSent, tx.fee.quantity], + ) + const totalPrimaryTokenSpentLabel = formatTokenWithText(totalPrimaryTokenSpent, wallet.portfolioPrimaryTokenInfo) + + const notPrimaryTokenSent = React.useMemo( + () => notOwnedOutputs.flatMap((output) => output.assets.filter((asset) => !asset.isPrimary)), + [notOwnedOutputs], + ) + + return ( + + + + + + + + + {notPrimaryTokenSent.map((token) => ( + + ))} + + + ) +} + +const SenderSectionLabel = () => { + const {styles, colors} = useStyles() + + return ( + + + + + + Send + + ) +} + +const ReceiverSection = ({notOwnedOutputs}: {notOwnedOutputs: FormattedOutputs}) => { + const address = notOwnedOutputs[0]?.rewardAddress ?? notOwnedOutputs[0]?.address + const {styles} = useStyles() + const addressType = useAddressType(address) + const isScriptAddress = addressType === 'script' + + return ( + <> + + + + {isScriptAddress ? 'To script' : `To`}: + +
+ + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.px_lg, + }, + infoItem: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + infoLabel: { + ...atoms.body_2_md_regular, + color: color.gray_600, + }, + walletInfoText: { + ...atoms.body_2_md_medium, + color: color.text_primary_medium, + }, + plate: { + ...atoms.flex_row, + ...atoms.align_center, + }, + fee: { + color: color.gray_900, + ...atoms.body_2_md_regular, + }, + link: { + color: color.text_primary_medium, + ...atoms.body_2_md_medium, + }, + receiverAddress: { + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.flex_row, + ...atoms.justify_between, + }, + tokenSectionLabel: { + ...atoms.body_2_md_regular, + color: color.gray_900, + }, + senderTokenItems: { + ...atoms.flex_wrap, + ...atoms.flex_row, + ...atoms.justify_end, + ...atoms.flex_1, + gap: 8, + }, + tokensSection: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + tokensSectionLabel: { + ...atoms.flex_row, + ...atoms.align_center, + }, + walletChecksum: { + width: 24, + height: 24, + }, + receiverSectionAddress: { + maxWidth: 260, + }, + }) + + const colors = { + send: color.primary_500, + received: color.green_static, + } + + return {styles, colors} as const +} + +// WORK IN PROGRESS BELOW + +// TODO: WIP +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CreatedByInfoItem = () => { + const {styles} = useStyles() + + return ( + + Created By + + + {/* */} + + + + Linking.openURL('https://google.com')}> + dapp.org + + + + ) +} + +// TODO: WIP +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ReceiverTokensSectionMultiReceiver = () => { + const {styles} = useStyles() + + return ( + <> + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +// TODO: WIP +const ReceiverSectionLabel = () => { + const {styles, colors} = useStyles() + + return ( + + + + + + Receive + + ) +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx new file mode 100644 index 0000000000..2d831995ee --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx @@ -0,0 +1,95 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, View} from 'react-native' + +import {Button} from '../../../../components/Button/Button' +import {KeyboardAvoidingView} from '../../../../components/KeyboardAvoidingView/KeyboardAvoidingView' +import {SafeArea} from '../../../../components/SafeArea' +import {ScrollView} from '../../../../components/ScrollView/ScrollView' +import {Tab, Tabs} from '../../../../components/Tabs/Tabs' +import {ReviewTxRoutes, useUnsafeParams} from '../../../../kernel/navigation' +import {Divider} from '../../common/Divider' +import {useFormattedTx} from '../../common/hooks/useFormattedTx' +import {useOnConfirm} from '../../common/hooks/useOnConfirm' +import {useTxBody} from '../../common/hooks/useTxBody' +import {OverviewTab} from './Overview/OverviewTab' + +type Tabs = 'overview' | 'utxos' + +export const ReviewTxScreen = () => { + const {styles} = useStyles() + const [activeTab, setActiveTab] = React.useState('overview') + + const params = useUnsafeParams() + + // TODO: add cbor arguments + const txBody = useTxBody({unsignedTx: params.unsignedTx}) + const formatedTx = useFormattedTx(txBody) + + const {onConfirm} = useOnConfirm({ + unsignedTx: params.unsignedTx, + onSuccess: params.onSuccess, + onError: params.onError, + }) + + const renderTabs = React.useMemo(() => { + return ( + + setActiveTab('overview')} + label="Overview" + /> + + setActiveTab('utxos')} label="UTxOs" /> + + ) + }, [activeTab, setActiveTab, styles.tab, styles.tabs]) + + return ( + + + + {renderTabs} + + + + {activeTab === 'overview' && } + + + +