Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: Fee Payment with Sufficient Assets #11336

Merged
merged 3 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;