Enhancement: Fee Payment with Sufficient Assets (#11336)

* refactor: remove choosing fee asset from transfer and it's types

* feat: choose fee asset in authorize tx modal

* fix: asset id formatting
This commit is contained in:
Arjun Porwal 2025-02-26 18:36:14 +05:30 committed by GitHub
parent 9b95890158
commit a36d3ab0e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 66 additions and 39 deletions

View File

@ -1,6 +1,7 @@
{
"Adding an optional tip to the transaction could allow for higher priority, especially when the chain is busy.": "Adding an optional tip to the transaction could allow for higher priority, especially when the chain is busy.",
"Authorize transaction": "Authorize transaction",
"By selecting this option, the transaction fee will be automatically deducted from the specified asset, ensuring a seamless and efficient payment process.": "By selecting this option, the transaction fee will be automatically deducted from the specified asset, ensuring a seamless and efficient payment process.",
"Current account nonce: {{accountNonce}}": "Current account nonce: {{accountNonce}}",
"Do not include a tip for the block author": "Do not include a tip for the block author",
"Don't use a proxy for this call": "Don't use a proxy for this call",
@ -35,6 +36,7 @@
"Unable to connect to the Ledger, ensure support is enabled in settings and no other app is using it. {{errorMessage}}": "Unable to connect to the Ledger, ensure support is enabled in settings and no other app is using it. {{errorMessage}}",
"Unlock the sending account to allow signing of this transaction.": "Unlock the sending account to allow signing of this transaction.",
"Use a proxy for this call": "Use a proxy for this call",
"asset to pay the fee": "asset to pay the fee",
"call hash": "call hash",
"multisig call data": "multisig call data",
"multisig signatory": "multisig signatory",

View File

@ -3,8 +3,7 @@
import type { SubmittableResult } from '@polkadot/api';
import type { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import type { SignerOptions, SignerResult } from '@polkadot/api/types';
import type { AssetInfoComplete } from '@polkadot/react-hooks/types';
import type { SignerResult } from '@polkadot/api/types';
import type { AccountId, Address } from '@polkadot/types/interfaces';
import type { DefinitionRpcExt, Registry, SignerPayloadJSON } from '@polkadot/types/types';
@ -52,7 +51,6 @@ export interface QueueTx extends AccountInfo {
txUpdateCb?: TxCallback;
values?: unknown[];
status: QueueTxStatus;
signerOptions?: Partial<SignerOptions & { feeAsset: AssetInfoComplete | null }>;
}
export interface QueueStatus extends ActionStatus {
@ -89,7 +87,6 @@ export interface PartialQueueTxExtrinsic extends PartialAccountInfo {
txStartCb?: () => void;
txUpdateCb?: TxCallback;
isUnsigned?: boolean;
signerOptions?: Partial<SignerOptions & { feeAsset: AssetInfoComplete | null }>;
}
export interface PartialQueueTxRpc extends PartialAccountInfo {

View File

@ -7,8 +7,7 @@ import type { TxButtonProps as Props } from './types.js';
import React, { useCallback, useEffect, useState } from 'react';
import { useApi, useIsMountedRef, usePayWithAsset, useQueue } from '@polkadot/react-hooks';
import { getFeeAssetLocation } from '@polkadot/react-hooks/utils/getFeeAssetLocation';
import { useIsMountedRef, useQueue } from '@polkadot/react-hooks';
import { assert, isFunction } from '@polkadot/util';
import Button from './Button/index.js';
@ -16,10 +15,8 @@ import { useTranslation } from './translate.js';
function TxButton ({ accountId, className = '', extrinsic: propsExtrinsic, icon, isBasic, isBusy, isDisabled, isIcon, isToplevel, isUnsigned, label, onClick, onFailed, onSendRef, onStart, onSuccess, onUpdate, params, tooltip, tx, withSpinner, withoutLink }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const mountedRef = useIsMountedRef();
const { queueExtrinsic } = useQueue();
const { selectedFeeAsset } = usePayWithAsset();
const [isSending, setIsSending] = useState(false);
const [isStarted, setIsStarted] = useState(false);
@ -79,7 +76,6 @@ function TxButton ({ accountId, className = '', extrinsic: propsExtrinsic, icon,
accountId: accountId?.toString(),
extrinsic,
isUnsigned,
signerOptions: { assetId: getFeeAssetLocation(api, selectedFeeAsset), feeAsset: selectedFeeAsset },
txFailedCb: withSpinner ? _onFailed : onFailed,
txStartCb: _onStart,
txSuccessCb: withSpinner ? _onSuccess : onSuccess,
@ -89,7 +85,7 @@ function TxButton ({ accountId, className = '', extrinsic: propsExtrinsic, icon,
onClick && onClick();
},
[_onFailed, _onStart, _onSuccess, accountId, api, isUnsigned, mountedRef, onClick, onFailed, onSuccess, onUpdate, params, propsExtrinsic, queueExtrinsic, selectedFeeAsset, tx, withSpinner]
[_onFailed, _onStart, _onSuccess, accountId, isUnsigned, mountedRef, onClick, onFailed, onSuccess, onUpdate, params, propsExtrinsic, queueExtrinsic, tx, withSpinner]
);
if (onSendRef) {

View File

@ -74,7 +74,6 @@ export { default as Output } from './Output.js';
export { default as ParaLink } from './ParaLink.js';
export { default as Password } from './Password.js';
export { default as PasswordStrength } from './PasswordStrength.js';
export { default as PayWithAsset } from './PayWithAsset.js';
export { default as Popup } from './Popup/index.js';
export { default as Progress } from './Progress.js';
export { default as ProgressBar } from './ProgressBar.js';

View File

@ -19,7 +19,6 @@ import InputBalance from '../InputBalance.js';
import MarkError from '../MarkError.js';
import MarkWarning from '../MarkWarning.js';
import Modal from '../Modal/index.js';
import PayWithAsset from '../PayWithAsset.js';
import { styled } from '../styled.js';
import Toggle from '../Toggle.js';
import { useTranslation } from '../translate.js';
@ -148,9 +147,6 @@ function Transfer ({ className = '', onClose, recipientId: propRecipientId, send
<MarkError content={t('The recipient is associated with a known phishing site on {{url}}', { replace: { url: recipientPhish } })} />
)}
</Modal.Columns>
<Modal.Columns hint={t('By selecting this option, the transaction fee will be automatically deducted from the specified asset, ensuring a seamless and efficient payment process.')}>
<PayWithAsset />
</Modal.Columns>
<Modal.Columns hint={t('If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.')}>
{canToggleAll && isAll
? (

View File

@ -5,10 +5,9 @@ import type { BN } from '@polkadot/util';
import type { AssetInfoComplete } from '../types.js';
import type { PayWithAsset } from './types.js';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useApi, useAssetIds, useAssetInfos } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';
interface Props {
children?: React.ReactNode;
@ -44,7 +43,7 @@ function PayWithAssetProvider ({ children }: Props): React.ReactElement<Props> {
const completeAssetInfos = useMemo(
() => (assetInfos
?.filter((i): i is AssetInfoComplete =>
!!(i.details && i.metadata) && !i.details.supply.toHuman() && !!i.details?.toJSON().isSufficient)
!!(i.details && i.metadata) && !i.details.supply.isZero() && !!i.details?.toJSON().isSufficient)
) || [],
[assetInfos]
);
@ -53,7 +52,7 @@ function PayWithAssetProvider ({ children }: Props): React.ReactElement<Props> {
() => [
{ text: `${nativeAsset} (Native)`, value: nativeAsset },
...completeAssetInfos.map(({ id, metadata }) => ({
text: `${metadata.name.toUtf8()} (${formatNumber(id)})`,
text: `${metadata.name.toUtf8()} (${id.toString()})`,
value: id.toString()
}))],
[completeAssetInfos, nativeAsset]
@ -74,6 +73,10 @@ function PayWithAssetProvider ({ children }: Props): React.ReactElement<Props> {
[api.registry.metadata.extrinsic.signedExtensions, api.tx.assetConversion, completeAssetInfos.length]
);
useEffect(() => {
return () => setSelectedFeeAsset(null);
});
const values: PayWithAsset = useMemo(() => {
return {
assetOptions,

View File

@ -1,22 +1,28 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// Copyright 2017-2025 @polkadot/react-signer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DropdownItemProps } from 'semantic-ui-react';
import type { ExtendedSignerOptions } from './types.js';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Dropdown } from '@polkadot/react-components';
import { Dropdown, Modal } from '@polkadot/react-components';
import { useApi, usePayWithAsset } from '@polkadot/react-hooks';
import { getFeeAssetLocation } from '@polkadot/react-hooks/utils/getFeeAssetLocation';
import { BN } from '@polkadot/util';
import { useTranslation } from './translate.js';
const PayWithAsset = () => {
interface Props {
onChangeFeeAsset: React.Dispatch<React.SetStateAction<ExtendedSignerOptions>>
}
const PayWithAsset = ({ onChangeFeeAsset }: Props) => {
const { t } = useTranslation();
const { api } = useApi();
const [selectedAssetValue, setSelectedAssetValue] = useState('0');
const { assetOptions, isDisabled, onChange } = usePayWithAsset();
const { assetOptions, isDisabled, onChange, selectedFeeAsset } = usePayWithAsset();
const nativeAsset = useMemo(
() => api.registry.chainTokens[0],
@ -46,15 +52,30 @@ const PayWithAsset = () => {
}
}, [assetOptions, selectedAssetValue, nativeAsset]);
useEffect(() => {
if (selectedFeeAsset) {
onChangeFeeAsset((e) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
({
...e,
assetId: getFeeAssetLocation(api, selectedFeeAsset),
feeAsset: selectedFeeAsset
})
);
}
}, [api, onChangeFeeAsset, selectedFeeAsset]);
return (
<Dropdown
isDisabled={isDisabled}
label={t('asset to pay the fee')}
onChange={onSelect}
onSearch={onSearch}
options={assetOptions}
value={selectedAssetValue}
/>
<Modal.Columns hint={t('By selecting this option, the transaction fee will be automatically deducted from the specified asset, ensuring a seamless and efficient payment process.')}>
<Dropdown
isDisabled={isDisabled}
label={t('asset to pay the fee')}
onChange={onSelect}
onSearch={onSearch}
options={assetOptions}
value={selectedAssetValue}
/>
</Modal.Columns>
);
};

View File

@ -3,8 +3,8 @@
import type { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import type { DeriveBalancesAll } from '@polkadot/api-derive/types';
import type { QueueTx } from '@polkadot/react-components/Status/types';
import type { RuntimeDispatchInfo } from '@polkadot/types/interfaces';
import type { ExtendedSignerOptions } from './types.js';
import React, { useEffect, useState } from 'react';
import { Trans } from 'react-i18next';
@ -22,7 +22,7 @@ interface Props {
isHeader?: boolean;
onChange?: (hasAvailable: boolean) => void;
tip?: BN;
signerOptions?: QueueTx['signerOptions'];
signerOptions: ExtendedSignerOptions;
}
function PaymentInfo ({ accountId, className = '', extrinsic, isHeader, signerOptions }: Props): React.ReactElement<Props> | null {
@ -35,6 +35,8 @@ function PaymentInfo ({ accountId, className = '', extrinsic, isHeader, signerOp
useEffect((): void => {
accountId && extrinsic && extrinsic.hasPaymentInfo &&
nextTick(async (): Promise<void> => {
setDispatchInfo(null);
try {
const info = await extrinsic.paymentInfo(accountId, signerOptions);

View File

@ -3,6 +3,7 @@
import type { QueueTx } from '@polkadot/react-components/Status/types';
import type { BN } from '@polkadot/util';
import type { ExtendedSignerOptions } from './types.js';
import React from 'react';
@ -18,9 +19,10 @@ interface Props {
currentItem: QueueTx;
onError: () => void;
tip?: BN;
signerOptions?: ExtendedSignerOptions;
}
function Transaction ({ accountId, className, currentItem: { extrinsic, isUnsigned, payload, signerOptions }, onError, tip }: Props): React.ReactElement<Props> | null {
function Transaction ({ accountId, className, currentItem: { extrinsic, isUnsigned, payload }, onError, signerOptions, tip }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
if (!extrinsic) {

View File

@ -15,7 +15,7 @@ import type { Option } from '@polkadot/types';
import type { Multisig, Timepoint } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { HexString } from '@polkadot/util/types';
import type { AddressFlags, AddressProxy, QrState } from './types.js';
import type { AddressFlags, AddressProxy, ExtendedSignerOptions, QrState } from './types.js';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@ -29,6 +29,7 @@ import { addressEq } from '@polkadot/util-crypto';
import { AccountSigner, LedgerSigner, QrSigner } from './signers/index.js';
import Address from './Address.js';
import PayWithAsset from './PayWithAsset.js';
import Qr from './Qr.js';
import SignFields from './SignFields.js';
import Tip from './Tip.js';
@ -243,7 +244,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
const [isSubmit, setIsSubmit] = useState(true);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [senderInfo, setSenderInfo] = useState<AddressProxy>(() => ({ isMultiCall: false, isUnlockCached: false, multiRoot: null, proxyRoot: null, signAddress: requestAddress, signPassword: '' }));
const [signedOptions, setSignedOptions] = useState<Partial<SignerOptions>>({});
const [signedOptions, setSignedOptions] = useState<ExtendedSignerOptions>({});
const [signedTx, setSignedTx] = useState<string | null>(null);
const [{ innerHash, innerTx }, setCallInfo] = useState<InnerTx>(EMPTY_INNER);
const [tip, setTip] = useState<BN | undefined>();
@ -339,7 +340,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
if (senderInfo.signAddress) {
const [tx, [status, pairOrAddress, options, isMockSign]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(api, senderInfo.signAddress, { nonce: -1, tip, withSignedTransaction: true, ...currentItem.signerOptions }, getLedger, setQrState)
extractParams(api, senderInfo.signAddress, { nonce: -1, tip, withSignedTransaction: true, ...signedOptions }, getLedger, setQrState)
]);
queueSetTxStatus(currentItem.id, status);
@ -347,7 +348,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options, api, isMockSign);
}
},
[api, getLedger, tip]
[api, getLedger, signedOptions, tip]
);
const _onSign = useCallback(
@ -446,6 +447,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
accountId={senderInfo.signAddress}
currentItem={currentItem}
onError={toggleRenderError}
signerOptions={signedOptions}
/>
<Address
currentItem={currentItem}
@ -455,7 +457,10 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
requestAddress={requestAddress}
/>
{!currentItem.payload && (
<Tip onChange={setTip} />
<>
<PayWithAsset onChangeFeeAsset={setSignedOptions} />
<Tip onChange={setTip} />
</>
)}
{!isSubmit && (
<SignFields

View File

@ -1,7 +1,9 @@
// Copyright 2017-2025 @polkadot/react-signer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SignerOptions } from '@polkadot/api/submittable/types';
import type { SignerResult } from '@polkadot/api/types';
import type { AssetInfoComplete } from '@polkadot/react-hooks/types';
export interface AddressFlags {
accountOffset: number;
@ -39,3 +41,5 @@ export interface Signed {
message: Uint8Array;
signature: Uint8Array;
}
export type ExtendedSignerOptions = (Partial<SignerOptions & { feeAsset: AssetInfoComplete | null }>) | undefined;