Skip to content

Commit

Permalink
feat: add batch transaction for flexible multisig (#2672)
Browse files Browse the repository at this point in the history
  • Loading branch information
sokolova-an authored Nov 20, 2024
1 parent 8da82dd commit 16aed7e
Show file tree
Hide file tree
Showing 21 changed files with 466 additions and 402 deletions.
82 changes: 81 additions & 1 deletion src/renderer/entities/transaction/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { type ApiPromise } from '@polkadot/api';
import { type ClaimAction } from '@/shared/api/governance';
import { type MultisigTransactionDS } from '@/shared/api/storage';
import {
type Account,
type AccountId,
type Address,
type Asset,
type Chain,
type ChainId,
type Conviction,
type MultisigAccount,
type ReferendumId,
type TrackId,
type Transaction,
TransactionType,
WrapperKind,
} from '@/shared/core';
import { TEST_ACCOUNTS, formatAmount, getAssetId, toAccountId, toAddress } from '@/shared/lib/utils';
import { type RevoteTransaction, type TransactionVote, type VoteTransaction } from '@/entities/governance';
Expand All @@ -38,6 +41,8 @@ export const transactionBuilder = {
buildRemoveVote,
buildRemoveVotes,
buildRejectMultisigTx,
buildCreatePureProxy,
buildCreateFlexibleMultisig,

buildBatchAll,
splitBatchAll,
Expand Down Expand Up @@ -480,7 +485,6 @@ function buildRemoveVotes({ chain, accountId, votes }: RemoveVotesParams): Trans

return buildBatchAll({ chain, accountId, transactions });
}

type RejectTxParams = {
chain: Chain;
signerAddress: Address;
Expand All @@ -504,3 +508,79 @@ function buildRejectMultisigTx({ chain, signerAddress, threshold, otherSignatori
},
};
}

type CreateProxyPureParams = {
chain: Chain;
accountId: AccountId;
};

function buildCreatePureProxy({ chain, accountId }: CreateProxyPureParams): Transaction {
return {
chainId: chain.chainId,
address: toAddress(accountId, { prefix: chain.addressPrefix }),
type: TransactionType.CREATE_PURE_PROXY,
args: { proxyType: 'Any', delay: 0, index: 0 },
};
}

type CreateFlexibleMultisigParams = {
chain: Chain;
signer: Account;
api: ApiPromise;
multisigAccountId: AccountId;
threshold: number;
proxyDeposit: string;
signatories: {
accountId: AccountId;
address: Address;
}[];
};

function buildCreateFlexibleMultisig({
api,
chain,
multisigAccountId,
threshold,
signatories,
signer,
proxyDeposit,
}: CreateFlexibleMultisigParams): Transaction {
const proxyTransaction = transactionBuilder.buildCreatePureProxy({
chain: chain,
accountId: signer.accountId,
});

const wrappedTransaction = transactionService.getWrappedTransaction({
api: api,
addressPrefix: chain.addressPrefix,
transaction: proxyTransaction,
txWrappers: [
{
kind: WrapperKind.MULTISIG,
multisigAccount: {
accountId: multisigAccountId,
signatories,
threshold,
} as MultisigAccount,
signatories: signatories.map((s) => ({
accountId: toAccountId(s.address),
})) as Account[],
signer,
},
],
});

const transferTransaction = {
chainId: chain.chainId,
address: toAddress(signer.accountId, { prefix: chain.addressPrefix }),
type: TransactionType.TRANSFER,
args: {
dest: toAddress(multisigAccountId, { prefix: chain.addressPrefix }),
value: proxyDeposit,
},
};

const transactions = [wrappedTransaction.wrappedTx, transferTransaction];

return buildBatchAll({ chain, accountId: signer.accountId, transactions });
}
2 changes: 1 addition & 1 deletion src/renderer/entities/transaction/ui/Fee/Fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Props = {
api?: ApiPromise | null;
multiply?: number;
asset: Asset;
transaction?: Transaction;
transaction?: Transaction | null;
className?: string;
onFeeChange?: (fee: string) => void;
onFeeLoading?: (loading: boolean) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export const ConfirmationStep = () => {
const api = useUnit(flexibleMultisigModel.$api);
const transaction = useUnit(flexibleMultisigModel.$transaction);
const isEnoughBalance = useUnit(flexibleMultisigModel.$isEnoughBalance);

const chain = useUnit(formModel.$chain);
const signatories = useUnit(signatoryModel.$signatories);
const ownedSignatories = useUnit(signatoryModel.$ownedSignatoriesWallets);

const {
fields: { name, threshold, chain },
fields: { name, threshold },
} = useForm(formModel.$createMultisigForm);

const asset = chain?.assets?.[0];
const walletName = signer?.name || (signerWallet?.type === WalletType.POLKADOT_VAULT && signerWallet?.name) || '';

return (
Expand All @@ -40,7 +42,7 @@ export const ConfirmationStep = () => {
</div>
<DetailRow label={t('createMultisigAccount.walletName')}>{name.value}</DetailRow>
<DetailRow wrapperClassName="my-4" label={t('createMultisigAccount.signatoriesLabel')}>
<SelectedSignatoriesModal signatories={signatories} addressPrefix={chain.value.addressPrefix}>
<SelectedSignatoriesModal signatories={signatories} addressPrefix={chain?.addressPrefix}>
<div className="flex items-center">
<Counter className="mr-2" variant="neutral">
{signatories.length}
Expand Down Expand Up @@ -68,27 +70,18 @@ export const ConfirmationStep = () => {
</div>
</DetailRow>
<Separator className="my-4 border-filter-border" />
<div className="mb-4 flex flex-1 flex-col gap-y-4">
<ProxyDepositWithLabel
asset={chain.value.assets[0]}
proxyNumber={1}
deposit="0"
api={api}
className="text-footnote"
/>
<MultisigDepositWithLabel
className="text-footnote"
asset={chain.value.assets[0]}
threshold={threshold.value}
api={api}
/>
<FeeWithLabel api={api} asset={chain.value.assets[0]} transaction={transaction?.wrappedTx} />
<Alert variant="error" title={t('createMultisigAccount.notEnoughTokensTitle')} active={!isEnoughBalance}>
<Alert.Item withDot={false}>
{t('createMultisigAccount.flexibleMultisig.notEnoughMultisigTokens')}
</Alert.Item>
</Alert>
</div>
{chain && asset ? (
<div className="mb-4 flex flex-1 flex-col gap-y-4">
<ProxyDepositWithLabel asset={asset} proxyNumber={1} deposit="0" api={api} className="text-footnote" />
<MultisigDepositWithLabel className="text-footnote" asset={asset} threshold={threshold.value} api={api} />
<FeeWithLabel api={api} asset={asset} transaction={transaction} />
<Alert variant="error" title={t('createMultisigAccount.notEnoughTokensTitle')} active={!isEnoughBalance}>
<Alert.Item withDot={false}>
{t('createMultisigAccount.flexibleMultisig.notEnoughMultisigTokens')}
</Alert.Item>
</Alert>
</div>
) : null}

<div className="mt-3 flex items-center justify-between">
<Button
Expand All @@ -102,6 +95,7 @@ export const ConfirmationStep = () => {
{t('createMultisigAccount.backButton')}
</Button>
<SignButton
disabled={!isEnoughBalance}
type={signerWallet?.type || WalletType.POLKADOT_VAULT}
onClick={confirmModel.output.formSubmitted}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const FlexibleMultisigWallet = ({ isOpen, onClose, onGoBack }: Props) =>

const activeStep = useUnit(flexibleMultisigModel.$step);
const {
fields: { chain },
fields: { chainId },
} = useForm(formModel.$createMultisigForm);

if (isStep(activeStep, Step.SUBMIT)) {
Expand All @@ -43,15 +43,14 @@ export const FlexibleMultisigWallet = ({ isOpen, onClose, onGoBack }: Props) =>
<>
<span className="mx-1">{t('createMultisigAccount.titleOn')}</span>
<ChainTitle
chainId={chain.value.chainId}
chainId={chainId.value}
className="gap-x-1.5"
fontClass="font-manrope text-header-title text-text-primary truncate"
/>
</>
)}
</div>
);
console.log(activeStep);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { useForm } from 'effector-forms';
import { useUnit } from 'effector-react';

import { type Chain } from '@/shared/core';
import { type ChainId } from '@/shared/core';
import { useI18n } from '@/shared/i18n';
import { Step } from '@/shared/lib/utils';
import { Button, FootnoteText, InputHint, Select, SmallTitleText } from '@/shared/ui';
import { type DropdownOption } from '@/shared/ui/types';
import { Box, Input } from '@/shared/ui-kit';
import { Button, FootnoteText, InputHint, SmallTitleText } from '@/shared/ui';
import { Box, Field, Input, Select } from '@/shared/ui-kit';
import { ChainTitle } from '@/entities/chain';
import { networkModel, networkUtils } from '@/entities/network';
import { flexibleMultisigModel } from '../model/flexible-multisig-create';
import { formModel } from '../model/form-model';

import { MultisigFees } from './MultisigFees';

const getChainOptions = (chains: Chain[]): DropdownOption<Chain>[] => {
return chains
.filter((c) => networkUtils.isMultisigSupported(c.options))
.map((chain) => ({
id: chain.chainId.toString(),
value: chain,
element: <ChainTitle chain={chain} fontClass="text-text-primary" />,
}));
};

interface Props {
onGoBack: () => void;
}
Expand All @@ -32,13 +21,14 @@ export const NameNetworkSelection = ({ onGoBack }: Props) => {
const { t } = useI18n();

const chains = useUnit(networkModel.$chains);
const chain = useUnit(formModel.$chain);

const {
fields: { name, chain },
fields: { name, chainId },
} = useForm(formModel.$createMultisigForm);

const chainOptions = getChainOptions(Object.values(chains));
const isNameError = name.isTouched && !name.value;
const asset = chain?.assets.at(0);

return (
<section className="flex h-full max-h-[594px] w-modal-lg flex-1 flex-col">
Expand All @@ -62,14 +52,23 @@ export const NameNetworkSelection = ({ onGoBack }: Props) => {
</InputHint>
</div>
<div className="flex items-end gap-x-4">
<Select
placeholder={t('createMultisigAccount.chainPlaceholder')}
label={t('createMultisigAccount.chainName')}
className="w-[386px]"
selectedId={chain.value.chainId.toString()}
options={chainOptions}
onChange={({ value }) => chain.onChange(value)}
/>
<Box width="386px">
<Field text={t('createMultisigAccount.chainName')}>
<Select
placeholder={t('createMultisigAccount.chainPlaceholder')}
value={chainId.value}
onChange={(value) => chainId.onChange(value as ChainId)}
>
{Object.values(chains)
.filter((c) => networkUtils.isMultisigSupported(c.options))
.map((chain) => (
<Select.Item key={chain.chainId} value={chain.chainId}>
<ChainTitle className="overflow-hidden" chain={chain} fontClass="text-text-primary truncate" />
</Select.Item>
))}
</Select>
</Field>
</Box>
<FootnoteText className="mt-2 text-text-tertiary">
{t('createMultisigAccount.networkDescription')}
</FootnoteText>
Expand All @@ -79,7 +78,7 @@ export const NameNetworkSelection = ({ onGoBack }: Props) => {
{t('createMultisigAccount.backButton')}
</Button>
<div className="mt-auto flex items-center justify-end">
<MultisigFees asset={chain.value.assets[0]} />
{asset ? <MultisigFees asset={asset} /> : null}
<Button
key="create"
disabled={isNameError || !name.isTouched}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ export const SelectSignatories = () => {
const signatories = useUnit(signatoryModel.$signatories);

const onAddSignatoryClick = () => {
signatoryModel.events.addSignatory({ name: '', address: '' });
signatoryModel.events.addSignatory({ name: '', address: '', walletId: '' });
};

return (
<div className="flex flex-1 flex-col">
<div className="flex flex-col gap-2">
{signatories.map((value, index) => (
<Signatory
key={index}
key={value.address}
signatoryIndex={index}
isOwnAccount={index === 0}
signatoryName={value.name}
signatoryAddress={value.address}
selectedWalletId={value.walletId}
onDelete={signatoryModel.events.deleteSignatory}
/>
))}
Expand Down
Loading

0 comments on commit 16aed7e

Please sign in to comment.