Skip to content

Commit

Permalink
Enhancement: Fee Payment with Sufficient Assets (#11336)
Browse files Browse the repository at this point in the history
* refactor: remove choosing fee asset from transfer and it's types

* feat: choose fee asset in authorize tx modal

* fix: asset id formatting
  • Loading branch information
ap211unitech authored Feb 26, 2025
1 parent 9b95890 commit a36d3ab
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 39 deletions.
2 changes: 2 additions & 0 deletions packages/apps/public/locales/en/react-signer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions packages/react-components/src/Status/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 2 additions & 6 deletions packages/react-components/src/TxButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ 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';
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);

Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion packages/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 0 additions & 4 deletions packages/react-components/src/modals/Transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
? (
Expand Down
11 changes: 7 additions & 4 deletions packages/react-hooks/src/ctx/PayWithAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]
);
Expand All @@ -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]
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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],
Expand Down Expand Up @@ -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>
);
};

Expand Down
6 changes: 4 additions & 2 deletions packages/react-signer/src/PaymentInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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);

Expand Down
4 changes: 3 additions & 1 deletion packages/react-signer/src/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand Down
15 changes: 10 additions & 5 deletions packages/react-signer/src/TxSigned.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -339,15 +340,15 @@ 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);

await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options, api, isMockSign);
}
},
[api, getLedger, tip]
[api, getLedger, signedOptions, tip]
);

const _onSign = useCallback(
Expand Down Expand Up @@ -446,6 +447,7 @@ function TxSigned ({ className, currentItem, isQueueSubmit, queueSize, requestAd
accountId={senderInfo.signAddress}
currentItem={currentItem}
onError={toggleRenderError}
signerOptions={signedOptions}
/>
<Address
currentItem={currentItem}
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/react-signer/src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -39,3 +41,5 @@ export interface Signed {
message: Uint8Array;
signature: Uint8Array;
}

export type ExtendedSignerOptions = (Partial<SignerOptions & { feeAsset: AssetInfoComplete | null }>) | undefined;

0 comments on commit a36d3ab

Please sign in to comment.