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:
parent
9b95890158
commit
a36d3ab0e9
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
? (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user