diff --git a/packages/yoroi-extension/app/actions/common/tx-builder-actions.js b/packages/yoroi-extension/app/actions/common/tx-builder-actions.js index 547e2f1c7f..e6c01c564a 100644 --- a/packages/yoroi-extension/app/actions/common/tx-builder-actions.js +++ b/packages/yoroi-extension/app/actions/common/tx-builder-actions.js @@ -9,7 +9,10 @@ import type { TokenRow, } from '../../api/ada/lib/storage/database/primitives/ta import BigNumber from 'bignumber.js'; export default class TxBuilderActions { - updateReceiver: Action = new Action(); + updateReceiver: Action<{| + address: void | string, + handle?: void | {| handle: string, nameServer: string |}, + |}> = new Action(); updateAmount: Action = new Action(); updateMemo: Action = new Action(); addToken: Action<{| diff --git a/packages/yoroi-extension/app/api/ada/index.js b/packages/yoroi-extension/app/api/ada/index.js index 4d45651c83..7658f85fef 100644 --- a/packages/yoroi-extension/app/api/ada/index.js +++ b/packages/yoroi-extension/app/api/ada/index.js @@ -270,6 +270,7 @@ export type CreateUnsignedTxRequest = {| }, absSlotNumber: BigNumber, receiver: string, + receiverHandle?: {| handle: string, nameServer: string |}, filter: ElementOf => boolean, tokens: SendTokenList, metadata: Array | void, @@ -329,6 +330,7 @@ export type CreateUnsignedTxForUtxosRequest = {| absSlotNumber: BigNumber, receivers: Array<{| ...Address, + +addressHandle?: {| handle: string, nameServer: string |}, ...InexactSubset, |}>, network: $ReadOnly, @@ -863,11 +865,13 @@ export default class AdaApi { const { protocolParameters } = request; + let signRequestReceiver; if (hasSendAllDefault(request.tokens)) { if (request.receivers.length !== 1) { throw new Error(`${nameof(this.createUnsignedTxForUtxos)} wrong output size for sendAll`); } const receiver = request.receivers[0]; + signRequestReceiver = { address: receiver.address, handle: receiver.addressHandle }; unsignedTxResponse = shelleySendAllUnsignedTx( receiver, request.utxos, @@ -903,23 +907,25 @@ export default class AdaApi { throw new Error(`${nameof(this.createUnsignedTxForUtxos)} needs exactly one change address`); } const changeAddr = changeAddresses[0]; - const otherAddresses: Array<{| ...Address, |}> = request.receivers.reduce( + const otherAddresses: Array<{| ...Address, +addressHandle?: {| handle: string, nameServer: string |}, |}> = request.receivers.reduce( (arr, next) => { if (next.addressing == null) { - arr.push({ address: next.address }); + arr.push({ address: next.address, addressHandle: next.addressHandle }); return arr; } return arr; }, - ([]: Array<{| ...Address, |}>) + ([]: Array<{| ...Address, +addressHandle?: {| handle: string, nameServer: string |}, |}>) ); if (otherAddresses.length > 1) { throw new Error(`${nameof(this.createUnsignedTxForUtxos)} can't send to more than one address`); } + const receiver = otherAddresses[0]; + signRequestReceiver = { address: receiver.address, handle: receiver.addressHandle }; unsignedTxResponse = await shelleyNewAdaUnsignedTx( otherAddresses.length === 1 ? [{ - address: otherAddresses[0].address, + address: receiver.address, amount: builtSendTokenList( request.defaultToken, request.tokens, @@ -966,6 +972,7 @@ export default class AdaApi { neededHashes: new Set(), wits: new Set(), }, + receiver: signRequestReceiver, }); } catch (error) { Logger.error( @@ -984,7 +991,8 @@ export default class AdaApi { const addressedUtxo = asAddressedUtxo(filteredUtxos); const receivers = [{ - address: request.receiver + address: request.receiver, + addressHandle: request.receiverHandle, }]; // note: we need to create a change address IFF we're not sending all of the default asset diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/HaskellShelleyTxSignRequest.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/HaskellShelleyTxSignRequest.js index 266731d05f..29a77b90a8 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/HaskellShelleyTxSignRequest.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/HaskellShelleyTxSignRequest.js @@ -46,6 +46,7 @@ export type TrezorTCatalystRegistrationTxSignData = export class HaskellShelleyTxSignRequest implements ISignRequest { + receiver: ?{| address: string, handle: {| handle: string, nameServer: string |} | void |}; senderUtxos: Array; unsignedTx: RustModule.WalletV4.TransactionBuilder; changeAddr: Array<{| ...Address, ...Value, ...Addressing |}>; @@ -75,6 +76,7 @@ implements ISignRequest { void | TrezorTCatalystRegistrationTxSignData; ledgerNanoCatalystRegistrationTxSignData?: void | LedgerNanoCatalystRegistrationTxSignData; + receiver?: ?{| address: string, handle: {| handle: string, nameServer: string |} | void |}, |}) { this.senderUtxos = data.senderUtxos; this.unsignedTx = data.unsignedTx; @@ -86,6 +88,7 @@ implements ISignRequest { data.trezorTCatalystRegistrationTxSignData; this.ledgerNanoCatalystRegistrationTxSignData = data.ledgerNanoCatalystRegistrationTxSignData; + this.receiver = data.receiver; } txId(): string { @@ -241,6 +244,10 @@ implements ISignRequest { })).toArray(); } + receiverWithHandle(): null | {| address: string, handle: void | {| handle: string, nameServer: string |} |} { + return this.receiver ?? null; + } + receivers(includeChange: boolean): Array { const outputStrings = iterateLenGet(this.unsignedTx.build().outputs()) .map(o => toHexOrBase58(o.address())).toArray(); diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js index a4a3760364..f458969dee 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js @@ -77,7 +77,7 @@ type TxMetadata = { }; export function sendAllUnsignedTx( - receiver: {| ...Address, ...InexactSubset |}, + receiver: { ...Address, ...InexactSubset, ... }, allUtxos: Array, absSlotNumber: BigNumber, protocolParams: {| @@ -288,7 +288,7 @@ function addUtxoInput( } export function sendAllUnsignedTxFromUtxo( - receiver: {| ...Address, ...InexactSubset |}, + receiver: { ...Address, ...InexactSubset, ... }, allUtxos: Array, absSlotNumber: BigNumber, protocolParams: {| diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js index f8b2595fe2..b943645608 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js @@ -170,7 +170,7 @@ type Props = {| +onSubmit: void => void, +totalInput: ?MultiToken, +isClassicTheme: boolean, - +updateReceiver: (void | string) => void, + +updateReceiver: (void | string, void | {| handle: string, nameServer: string |}) => void, +updateAmount: (?BigNumber) => void, +updateMemo: (void | string) => void, +shouldSendAll: boolean, @@ -245,6 +245,7 @@ type State = {| |}, domainResolverMessage: ?string, domainResolverIsLoading: boolean, + lastValidatedValue: ?string, |}; @observer @@ -260,6 +261,7 @@ export default class WalletSendFormRevamp extends Component { domainResolverResult: null, domainResolverMessage: null, domainResolverIsLoading: false, + lastValidatedValue: null, }; maxStep: number = SEND_FORM_STEP.RECEIVER; @@ -332,10 +334,12 @@ export default class WalletSendFormRevamp extends Component { isDomainResolvable: boolean, domainResolverMessage: ?string, resolvedAddress: ?string, + resolvedNameServer: ?string, |}> { let isDomainResolvable = false; let domainResolverMessage = null; let resolvedAddress = null; + let resolvedNameServer = null; const { resolveDomainAddress } = this.props; if (resolveDomainAddress != null) { isDomainResolvable = isResolvableDomain(handle); @@ -347,6 +351,7 @@ export default class WalletSendFormRevamp extends Component { domainResolverMessage = this.context.intl.formatMessage(messages.receiverFieldLabelUnresolvedAddress); } else if (res.address != null) { resolvedAddress = res.address; + resolvedNameServer = res.nameServer; domainResolverResult = { handle, address: res.address, @@ -372,6 +377,7 @@ export default class WalletSendFormRevamp extends Component { isDomainResolvable, domainResolverMessage, resolvedAddress, + resolvedNameServer, }; } @@ -385,44 +391,57 @@ export default class WalletSendFormRevamp extends Component { value: this.props.uriParams ? this.props.uriParams.address : '', validators: [ async ({ field }) => { - let receiverValue = field.value; - if (receiverValue === '') { - this.props.updateReceiver(); - this.setState({ - domainResolverResult: null, - domainResolverMessage: null, - domainResolverIsLoading: false, - }); - return [false, this.context.intl.formatMessage(globalMessages.fieldIsRequired)]; - } - const updateReceiver = (isValid: boolean) => { - if (isValid) { - this.props.updateReceiver(getAddressPayload(receiverValue, this.props.selectedNetwork)); - } else { + const inputFieldValue = field.value; + let handle = undefined; + let receiverValue = inputFieldValue; + try { + if (receiverValue === '') { this.props.updateReceiver(); + this.setState({ + domainResolverResult: null, + domainResolverMessage: null, + domainResolverIsLoading: false, + }); + return [false, this.context.intl.formatMessage(globalMessages.fieldIsRequired)]; } - }; + const updateReceiver = (isValid: boolean) => { + if (isValid) { + this.props.updateReceiver( + getAddressPayload(receiverValue, this.props.selectedNetwork), + handle, + ); + } else { + this.props.updateReceiver(); + } + }; - // DOMAIN RESOLVER - const { isDomainResolvable, domainResolverMessage, resolvedAddress } = await this.resolveDomainAddress( - receiverValue - ); - if (resolvedAddress != null) { - receiverValue = resolvedAddress; - } - //////////////////// + // DOMAIN RESOLVER + const { isDomainResolvable, domainResolverMessage, resolvedAddress, resolvedNameServer } = + // $FlowIgnore[incompatible-call] + await this.resolveDomainAddress(receiverValue); - const isValid = isValidReceiveAddress(receiverValue, this.props.selectedNetwork); - if (isValid === true) { - updateReceiver(true); - return [isValid]; + if (resolvedAddress != null) { + handle = { handle: receiverValue, nameServer: resolvedNameServer }; + receiverValue = resolvedAddress; + } + //////////////////// + + const isValid = isValidReceiveAddress(receiverValue, this.props.selectedNetwork); + if (isValid === true) { + updateReceiver(true); + return [isValid]; + } + const [result, errorMessage, errorType] = isValid; + updateReceiver(result); + const fieldError = isDomainResolvable + ? domainResolverMessage + : this.context.intl.formatMessage(errorType === 1 ? messages.receiverFieldLabelInvalidAddress : errorMessage); + return [isValid[0], fieldError]; + } finally { + this.setState({ + lastValidatedValue: inputFieldValue, + }); } - const [result, errorMessage, errorType] = isValid; - updateReceiver(result); - const fieldError = isDomainResolvable - ? domainResolverMessage - : this.context.intl.formatMessage(errorType === 1 ? messages.receiverFieldLabelInvalidAddress : errorMessage); - return [isValid[0], fieldError]; }, ], }, @@ -940,14 +959,6 @@ export default class WalletSendFormRevamp extends Component { trezorSend={this.props.trezorSend} selectedExplorer={this.props.selectedExplorer} selectedWallet={this.props.selectedWallet} - receiverHandle={ - domainResolverResult - ? { - nameServer: domainResolverResult.nameServer, - handle: domainResolverResult.handle, - } - : null - } /> ); default: @@ -962,6 +973,8 @@ export default class WalletSendFormRevamp extends Component { const { maxSendableAmount } = this.props; const receiverField = form.$('receiver'); + const isValidatedValue = receiverField.value === this.state.lastValidatedValue; + const isValidValue = isValidatedValue && receiverField.isValid; switch (step) { case SEND_FORM_STEP.RECEIVER: @@ -971,7 +984,7 @@ export default class WalletSendFormRevamp extends Component { variant="primary" size="medium" onClick={() => this.onUpdateStep(SEND_FORM_STEP.AMOUNT)} - disabled={invalidMemo || !receiverField.isValid} + disabled={invalidMemo || !isValidValue} id="wallet:send:enterAddressStep-nextToAddAssets-button" > {intl.formatMessage(globalMessages.nextButtonLabel)} diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js index b80b102dd6..b244de3131 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js @@ -43,11 +43,7 @@ type Props = {| +staleTx: boolean, +selectedExplorer: SelectedExplorer, +amount: MultiToken, - +receivers: Array, - +receiverHandle: ?{| - nameServer: string, - handle: string, - |}, + +receiver: {| address: string, handle: {| handle: string, nameServer: string |} | void |}, +totalAmount: MultiToken, +transactionFee: MultiToken, +transactionSize: ?string, @@ -439,7 +435,7 @@ export default class WalletSendPreviewStep extends Component { const { form } = this; const { intl } = this.context; const walletPasswordField = form.$('walletPassword'); - const { amount, receivers, isSubmitting, walletType } = this.props; + const { amount, receiver, isSubmitting, walletType } = this.props; const { passwordError } = this.state; const staleTxWarning = ( @@ -452,8 +448,6 @@ export default class WalletSendPreviewStep extends Component { ); - const { receiverHandle } = this.props; - return (
{ {this.renderError()} {this.props.staleTx ?
{staleTxWarning}
: null} - {receiverHandle ? ( + {receiver.handle != null ? (
@@ -482,7 +476,7 @@ export default class WalletSendPreviewStep extends Component { }} id="wallet:send:confrimTransactionStep-receiverHandleInfo-text" > - {receiverHandle.nameServer}: {receiverHandle.handle} + {receiver.handle?.nameServer}: {receiver.handle?.handle}
@@ -503,7 +497,7 @@ export default class WalletSendPreviewStep extends Component { }} id="wallet:send:confrimTransactionStep-receiverAddress-text" > - {this.props.addressToDisplayString(receivers[0])} + {this.props.addressToDisplayString(receiver.address)}
diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js index ea39bb169a..fe55e7b310 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js @@ -16,6 +16,7 @@ import LedgerSendActions from '../../../../actions/ada/ledger-send-actions'; import type { SendMoneyRequest } from '../../../../stores/toplevel/WalletStore'; import { getNetworkById } from '../../../../api/ada/lib/storage/database/prepackaged/networks'; import type { WalletState } from '../../../../../chrome/extension/background/types'; +import { HaskellShelleyTxSignRequest } from '../../../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; // TODO: unmagic the constants const MAX_VALUE_BYTES = 5000; @@ -54,10 +55,6 @@ type Props = {| +trezorSend: TrezorSendActions, selectedExplorer: Map, +selectedWallet: WalletState, - receiverHandle: ?{| - nameServer: string, - handle: string, - |}, |}; @observer @@ -112,7 +109,6 @@ export default class WalletSendPreviewStepContainer extends Component { isClassicTheme, getTokenInfo, getCurrentPrice, - receiverHandle, } = this.props; if (selectedWallet == null) @@ -126,12 +122,18 @@ export default class WalletSendPreviewStepContainer extends Component { const maxOutput = size ? Math.max(...size.outputs) : 0; const showSize = size != null && (size.full > MAX_TX_BYTES - 1000 || maxOutput > MAX_VALUE_BYTES - 1000); - const receivers = signRequest.receivers(false); const network = getNetworkById(selectedWallet.networkId); + const receiverWithHandle = signRequest instanceof HaskellShelleyTxSignRequest + ? signRequest.receiverWithHandle() + : null; + const receiver = { + address: receiverWithHandle?.address ?? signRequest.receivers(false)[0], + handle: receiverWithHandle?.handle, + } + return ( { getTokenInfo={getTokenInfo} getCurrentPrice={getCurrentPrice} amount={totalInput.joinSubtractCopy(fee)} - receivers={receivers} + receiver={receiver} totalAmount={totalInput} transactionFee={fee} transactionSize={ diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js index d8ac0eed73..b7c285f699 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js +++ b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js @@ -32,6 +32,7 @@ import NoOpenOrders from './NoOpenOrders'; import { LoadingCompletedOrders, LoadingOpenOrders } from './OrdersPlaceholders'; import { ampli } from '../../../../ampli/index'; import { tokenInfoToAnalyticsFromAndToAssets } from '../swapAnalytics'; +import { isHex } from '@emurgo/yoroi-lib/dist/internals/utils/index'; type ColumnContext = {| completedOrders: boolean, @@ -201,6 +202,12 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { collateral: utxoHex, }, }); + if (cancelTxCbor == null || !isHex(cancelTxCbor)) { + console.error('Failed to receive swap cancel tx from API. Expected cbor hex, got: ', cancelTxCbor); + // eslint-disable-next-line no-alert + alert('Unfortunately 3rd party API failed to produce cancellation transaction. Please retry later or report the issue and provide logs.'); + return; + } const totalCancelOutput = getTransactionTotalOutputFromCbor( cancelTxCbor, wallet.balance.getDefaults() diff --git a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js index cec7aa9bd0..337dbe5f96 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js @@ -212,7 +212,8 @@ class WalletSendPage extends Component { hasAnyPending={hasAnyPending} isClassicTheme={profile.isClassicTheme} shouldSendAll={transactionBuilderStore.shouldSendAll} - updateReceiver={(addr: void | string) => txBuilderActions.updateReceiver.trigger(addr)} + updateReceiver={(address: void | string, handle: void | {| handle: string, nameServer: string |}) => + txBuilderActions.updateReceiver.trigger({ address, handle })} updateAmount={(value: ?BigNumber) => txBuilderActions.updateAmount.trigger(value)} updateSendAllStatus={txBuilderActions.updateSendAllStatus.trigger} fee={transactionBuilderStore.fee} @@ -276,7 +277,7 @@ class WalletSendPage extends Component { totalInput={transactionBuilderStore.totalInput} hasAnyPending={hasAnyPending} classicTheme={profile.isClassicTheme} - updateReceiver={(addr: void | string) => txBuilderActions.updateReceiver.trigger(addr)} + updateReceiver={(address: void | string) => txBuilderActions.updateReceiver.trigger({ address })} updateAmount={(value: ?BigNumber) => txBuilderActions.updateAmount.trigger(value)} updateMemo={(content: void | string) => txBuilderActions.updateMemo.trigger(content)} shouldSendAll={transactionBuilderStore.shouldSendAll} diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.js index 36c596f408..14b175a8af 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.js @@ -47,6 +47,9 @@ export default class StakingDashboardPage extends Component ({ error })); const stakePools = errorIfPresent ?? this.getStakePools( publicDeriver.publicDeriverId, @@ -84,7 +87,7 @@ export default class StakingDashboardPage extends Component; /** Stores the tx that will be sent if the user confirms sending */ @@ -310,6 +311,7 @@ export default class TransactionBuilderStore extends Store this.api.ada.createUnsignedTx({ publicDeriver, receiver, + receiverHandle, tokens: this._genTokenList(), filter: this.filter, absSlotNumber, @@ -373,8 +376,12 @@ export default class TransactionBuilderStore extends Store void = (receiver) => { - this.receiver = receiver ?? null; + _updateReceiver: ({| + address: void | string, + handle?: void | {| handle: string, nameServer: string |}, + |}) => void = ({ address, handle }) => { + this.receiver = address ?? null; + this.receiverHandle = handle ?? null; } @action @@ -543,7 +550,9 @@ export default class TransactionBuilderStore extends Store => { this._setFilter(request.filter); const nextUnusedInternal = request.publicDeriver.receiveAddress; - this._updateReceiver(nextUnusedInternal.addr.Hash); + this._updateReceiver({ + address: nextUnusedInternal.addr.Hash, + }); // Todo: update shouldSendAll if (this.shouldSendAll === false) { this._updateSendAllStatus(true); diff --git a/packages/yoroi-extension/package-lock.json b/packages/yoroi-extension/package-lock.json index bea1d36631..485fd7def6 100644 --- a/packages/yoroi-extension/package-lock.json +++ b/packages/yoroi-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "yoroi", - "version": "5.4.500", + "version": "5.4.510", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yoroi", - "version": "5.4.500", + "version": "5.4.510", "license": "MIT", "dependencies": { "@amplitude/analytics-browser": "^2.1.3", diff --git a/packages/yoroi-extension/package.json b/packages/yoroi-extension/package.json index 5bfeb0e87c..99e7f46e91 100644 --- a/packages/yoroi-extension/package.json +++ b/packages/yoroi-extension/package.json @@ -1,6 +1,6 @@ { "name": "yoroi", - "version": "5.4.500", + "version": "5.4.510", "description": "Cardano ADA wallet", "scripts": { "dev-mv2": "rimraf dev/ && NODE_OPTIONS=--openssl-legacy-provider babel-node scripts-mv2/build --type=debug --env 'mainnet'",