diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts new file mode 100644 index 0000000000..d1ceb15303 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -0,0 +1,81 @@ +import { + CallBuilder, + TransactionBuilder, + TransactionVisitor +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet, Wallet} from "@shared/core/types/wallet"; +import {Account, Chain} from "@shared/core"; +import {ApiPromise} from "@polkadot/api"; +import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; +import {SubmittableExtrinsic} from "@polkadot/api/types"; +import {LeafTransactionBuilder} from "@entities/transactionBuilder/lib/leaf"; + +export class CompoundWalletTransactionBuilder implements TransactionBuilder { + + readonly wallet: Wallet + readonly allChildrenAccounts: Account[] + + readonly api: ApiPromise + + readonly chain: Chain + + private innerBuilder: TransactionBuilder + private selectedSChildrenAccounts: Account[] + + constructor( + api: ApiPromise, + chain: Chain, + wallet: Wallet, + childrenAccounts: Account[], + ) { + this.wallet = wallet + this.allChildrenAccounts = childrenAccounts + this.api = api + + if (childrenAccounts.length == 0) throw new Error("Empty children accounts list") + + const firstChild = childrenAccounts[0] + const firstChildInWallet: AccountInWallet = { + wallet: wallet, + account: firstChild + } + this.selectedSChildrenAccounts = [firstChild] + // We cannot have complex structure for wallets with multiple accounts per chain + this.innerBuilder = new LeafTransactionBuilder(api, firstChildInWallet, chain) + this.chain = chain + } + + effectiveCallBuilder(): CallBuilder { + return this.innerBuilder.effectiveCallBuilder() + } + + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + + this.innerBuilder.visitAll(visitor) + } + + visitSelf(visitor: TransactionVisitor) { + if (visitor.visitCompoundWallet == undefined) return + + visitor.visitCompoundWallet({ + wallet: this.wallet, + allChildrenAccounts: this.allChildrenAccounts, + selectedChildrenAccounts: this.selectedSChildrenAccounts, + updateSelectedChildren: this.#updateSelectedSChildren, + }) + } + + unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { + return this.innerBuilder.unsignedTransaction(options, info) + } + + submittableExtrinsic(): Promise | null> { + return this.innerBuilder.submittableExtrinsic() + } + + #updateSelectedSChildren(selectedChildren: Account[]) { + // No need to re-create `innerBuilder` since it is the leaf and won't change anyway + this.selectedSChildrenAccounts = selectedChildren + } +} diff --git a/src/renderer/entities/transactionBuilder/lib/factory.ts b/src/renderer/entities/transactionBuilder/lib/factory.ts new file mode 100644 index 0000000000..d131bcab44 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -0,0 +1,77 @@ +import {Account, AccountId, Chain, MultisigAccount, Wallet, WalletType} from "@shared/core"; +import {TransactionBuilder} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet} from "@shared/core/types/wallet"; +import keyBy from 'lodash/keyBy'; +import {walletUtils} from "@entities/wallet"; +import {LeafTransactionBuilder} from "@entities/transactionBuilder/lib/leaf"; +import {MultisigTransactionBuilder} from "@entities/transactionBuilder/lib/multisig"; +import {groupBy} from "lodash"; +import {CompoundWalletTransactionBuilder} from "@entities/transactionBuilder/lib/compound-wallet"; +import {ApiPromise} from "@polkadot/api"; + +export type NestedTransactionBuilderFactory = (shard: AccountInWallet) => TransactionBuilder + +export function createTransactionBuilder( + selectedWallet: Wallet, + selectedAccounts: Account[], + allWallets: Wallet[], + allAccounts: Account[], + chain: Chain, + api: ApiPromise +): TransactionBuilder { + + const walletsById = keyBy(allWallets, 'id') + const accountsByAccountId = groupBy(allAccounts, "accountId") + + const createInner: NestedTransactionBuilderFactory = (accountInWallet: AccountInWallet) => { + const wallet = accountInWallet.wallet + + switch (walletUtils.getWalletFamily(wallet)) { + case WalletType.MULTISIG: + const multisigAccount = accountInWallet.account as MultisigAccount + const matchingAccounts = multisigAccount.signatories.flatMap((signatory) => { + return findMatchingAccounts(signatory.accountId, walletsById, accountsByAccountId) + }) + + return new MultisigTransactionBuilder( + api, + chain, + multisigAccount.threshold, + multisigAccount.signatories.map((signatory) => signatory.accountId), + matchingAccounts, + createInner + ) + + case WalletType.WATCH_ONLY: + throw new Error("Signing with Watch only is not allowed") + + case WalletType.WALLET_CONNECT: + case WalletType.NOVA_WALLET: + case WalletType.POLKADOT_VAULT: + return new LeafTransactionBuilder(api, accountInWallet, chain) + } + } + + if (selectedAccounts.length > 1) { + return new CompoundWalletTransactionBuilder(api, chain, selectedWallet, selectedAccounts) + } else { + return createInner({ wallet: selectedWallet, account: selectedAccounts[0] }) + } +} + +function findMatchingAccounts( + accountId: AccountId, + allWalletsById: Record, + allAccountsById: Record +): AccountInWallet[] { + const idMatchingAccounts = allAccountsById[accountId] + + return idMatchingAccounts.map((account) => { + const wallet = allWalletsById[account.walletId] + const accountInWallet: AccountInWallet = {wallet, account} + return accountInWallet + } + ).filter((accountInWallet) => { + !walletUtils.isWatchOnly(accountInWallet.wallet) + }) +} diff --git a/src/renderer/entities/transactionBuilder/lib/helpers/amount-reduction.ts b/src/renderer/entities/transactionBuilder/lib/helpers/amount-reduction.ts new file mode 100644 index 0000000000..85cbece9bc --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/helpers/amount-reduction.ts @@ -0,0 +1,48 @@ +import {AccountId} from "@shared/core"; + + +export interface AmountReduction { + + reductionFor(accountId: AccountId): bigint + + totalReduction(): bigint +} + +export interface AmountReductionBuilder { + + addReductionAmount(payer: AccountId, amount: bigint): void + + build(): AmountReduction +} + +export function buildAmountReduction(): AmountReductionBuilder { + const amountByAccount: Record = {} + let total: bigint = BigInt(0) + + const addReductionAmount = (payer: AccountId, amount: bigint) => { + const currentAmount = amountByAccount[payer] || BigInt(0) + amountByAccount[payer] = currentAmount + amount + total += amount + } + + const build = () => createAmountReduction(amountByAccount, total) + + return { + addReductionAmount, + build + } +} + +function createAmountReduction( + amountByAccount: Record, + total: bigint +): AmountReduction { + return { + reductionFor(accountId: AccountId): bigint { + return amountByAccount[accountId] || BigInt(0) + }, + totalReduction(): bigint { + return total + } + } +} diff --git a/src/renderer/entities/transactionBuilder/lib/helpers/helpers.ts b/src/renderer/entities/transactionBuilder/lib/helpers/helpers.ts new file mode 100644 index 0000000000..2037c79e92 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/helpers/helpers.ts @@ -0,0 +1,88 @@ +import {AccountsInWallet, asMany} from "@shared/core/types/wallet"; +import { + CompoundWalletVisit, + LeafVisit, + MultisigVisit, + TransactionBuilder +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AmountReduction, buildAmountReduction} from "@entities/transactionBuilder/lib/helpers/amount-reduction"; +import {ApiPromise} from "@polkadot/api"; +import {UnsignedTransaction} from "@substrate/txwrapper-polkadot"; +import {createTxMetadatas, toAddress} from "@shared/lib/utils"; + +export function getSigningAccounts(transactionBuilder: TransactionBuilder): AccountsInWallet | undefined { + let signingAccounts: AccountsInWallet | undefined = undefined + + transactionBuilder.visitSelf({ + visitLeaf(visit: LeafVisit) { + signingAccounts = asMany(visit.account) + }, + + visitMultisig(visit: MultisigVisit) { + signingAccounts = asMany(visit.selectedSignatory) + }, + + visitCompoundWallet(visit: CompoundWalletVisit) { + signingAccounts = { + wallet: visit.wallet, + accounts: visit.selectedChildrenAccounts + } + } + }) + + return signingAccounts +} + +export async function getTransactionFee(transactionBuilder: TransactionBuilder): Promise { + const signingAccounts = getSigningAccounts(transactionBuilder) + if (signingAccounts == undefined) return undefined + + const singleTx = await transactionBuilder.submittableExtrinsic() + const singleTxFee = await singleTx?.paymentInfo(signingAccounts.accounts[0].accountId) + if (singleTxFee == undefined) return undefined + + const reductionBuilder = buildAmountReduction() + + signingAccounts.accounts.forEach((account) => { + reductionBuilder.addReductionAmount(account.accountId, singleTxFee.partialFee.toBigInt()) + }) + + return reductionBuilder.build() +} + +export async function getDeposits(transactionBuilder: TransactionBuilder): Promise { + const reductionBuilder = buildAmountReduction() + + const multisigDeposit = await getMultisigDeposit(transactionBuilder.api) + + transactionBuilder.visitAll({ + visitMultisig(visit: MultisigVisit) { + // TODO check who pays deposit in case multisig is wrapped in some other structure, like proxy + reductionBuilder.addReductionAmount(visit.selectedSignatory.account.accountId, multisigDeposit) + }, + }) + + return reductionBuilder.build() +} + +export async function getUnsignedTransactions(transactionBuilder: TransactionBuilder): Promise { + const signers = getSigningAccounts(transactionBuilder) + if (signers == undefined) throw new Error("No signing accounts found") + + const addresses = signers.accounts.map((signer) => { + return toAddress(signer.accountId, {prefix: transactionBuilder.chain.addressPrefix}) + }) + + const txMetadatas = await createTxMetadatas(addresses, transactionBuilder.api) + + return Promise.all( + txMetadatas.map(({options, info}) => { + return transactionBuilder.unsignedTransaction(options, info) + }) + ) +} + +async function getMultisigDeposit(api: ApiPromise): Promise { + const {depositFactor, depositBase} = api.consts.multisig; + return depositFactor.toBigInt() * depositBase.toBigInt() +} diff --git a/src/renderer/entities/transactionBuilder/lib/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts new file mode 100644 index 0000000000..593cc8ccf6 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -0,0 +1,95 @@ +import { + CallBuilder, CallBuilding, + TransactionBuilder, + TransactionVisitor +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet} from "@shared/core/types/wallet"; +import {ApiPromise} from "@polkadot/api"; +import {BaseTxInfo, methods, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; +import {SubmittableExtrinsic} from "@polkadot/api/types"; +import {Chain} from "@shared/core"; + +export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { + + currentCalls: CallBuilding[] + + readonly api: ApiPromise + readonly chain: Chain + + readonly accountInWallet: AccountInWallet + + constructor( + api: ApiPromise, + accountInWallet: AccountInWallet, + chain: Chain, + ) { + this.currentCalls = [] + + this.api = api + this.chain = chain + + this.accountInWallet = accountInWallet + } + + effectiveCallBuilder(): CallBuilder { + return this + } + + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + } + + visitSelf(visitor: TransactionVisitor) { + if (visitor.visitLeaf == undefined) return + + visitor.visitLeaf({account: this.accountInWallet}) + } + + addCall(call: CallBuilding): void { + this.currentCalls.push(call) + } + + resetCalls(): void { + this.currentCalls = [] + } + + setCall(call: CallBuilding): void { + this.currentCalls = [call] + } + + initFrom(callBuilder: CallBuilder): void { + this.currentCalls = callBuilder.currentCalls + } + + async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { + const nestedUnsignedTxs = this.currentCalls.map(call => call.viaTxWrapper(info, options)) + + if (nestedUnsignedTxs.length == 0) throw new Error("Cannot sign empty transaction") + + let maybeWrappedInBatch: UnsignedTransaction + if (nestedUnsignedTxs.length > 1) { + const innerMethods = nestedUnsignedTxs.map((nestedUnsignedTx) => nestedUnsignedTx.method) + maybeWrappedInBatch = methods.utility.batchAll({calls: innerMethods,}, info, options) + } else { + maybeWrappedInBatch = nestedUnsignedTxs[0] + } + + return maybeWrappedInBatch + } + + async submittableExtrinsic(): Promise | null> { + const viaApiCalls = this.currentCalls.map(call => call.viaApi) + + if (viaApiCalls.length == 0) return null + + let maybeWrappedInBatch: SubmittableExtrinsic<"promise"> + if (viaApiCalls.length > 1) { + maybeWrappedInBatch = this.api.tx.utility.batchAll(viaApiCalls) + } else { + maybeWrappedInBatch = viaApiCalls[0] + } + + return maybeWrappedInBatch + } + +} diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts new file mode 100644 index 0000000000..ba439be768 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -0,0 +1,137 @@ +import { + CallBuilder, + TransactionBuilder, + TransactionVisitor +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet} from "@shared/core/types/wallet"; +import {ApiPromise} from "@polkadot/api"; +import {BaseTxInfo, methods, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; +import {SubmittableExtrinsic} from "@polkadot/api/types"; +import {isOldMultisigPallet} from "@entities/transaction"; +import {AccountId, Chain} from "@shared/core"; +import {NestedTransactionBuilderFactory} from "@entities/transactionBuilder/lib/factory"; + +export class MultisigTransactionBuilder implements TransactionBuilder { + + readonly knownSignatoryAccounts: AccountInWallet[] + + readonly signatories: AccountId[] + readonly threshold: number + + readonly api: ApiPromise + readonly chain: Chain + + private selectedSignatory: AccountInWallet + private innerBuilder: TransactionBuilder + private readonly innerFactory: NestedTransactionBuilderFactory + + constructor( + api: ApiPromise, + chain: Chain, + threshold: number, + signatories: AccountId[], + knownSignatoryAccounts: AccountInWallet[], + innerFactory: NestedTransactionBuilderFactory, + ) { + this.knownSignatoryAccounts = knownSignatoryAccounts + this.innerFactory = innerFactory + + this.threshold = threshold + this.signatories = signatories + + this.api = api + this.chain = chain + + if (knownSignatoryAccounts.length == 0) { + // TODO maybe handle it gracefully? + throw new Error("No known signatories found") + } + this.selectedSignatory = knownSignatoryAccounts[0] + this.innerBuilder = innerFactory(this.selectedSignatory) + } + + effectiveCallBuilder(): CallBuilder { + return this.innerBuilder.effectiveCallBuilder() + } + + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + + this.innerBuilder.visitAll(visitor) + } + + visitSelf(visitor: TransactionVisitor) { + if (visitor.visitMultisig == undefined) return + + visitor.visitMultisig({ + knownSignatories: this.knownSignatoryAccounts, + threshold: this.threshold, + selectedSignatory: this.selectedSignatory, + updateSelectedSignatory: this.updateSelectedSignatory, + }) + } + + async submittableExtrinsic(): Promise | null> { + const innerInfo = await this.#innerInfo() + if (innerInfo == null) return null + + const {innerCall, innerWeight} = innerInfo + + const otherSignatories = this.#otherSignatories() + const maybeTimepoint = null + + return isOldMultisigPallet(this.api) ? + // @ts-ignore + this.api.tx.multisig.asMulti(this.threshold, otherSignatories, maybeTimepoint, innerCall, false, innerWeight) + : this.api.tx.multisig.asMulti(this.threshold, otherSignatories, maybeTimepoint, innerCall, innerWeight) + } + + async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { + const innerInfo = await this.#innerInfo() + if (innerInfo == null) throw new Error("Multisig cannot sign empty nested tx") + + const {innerWeight} = innerInfo + const maybeTimepoint = null + + const innerUnsignedTx = await this.innerBuilder.unsignedTransaction(options, info) + + return methods.multisig.asMulti( + { + threshold: this.threshold, + otherSignatories: this.#otherSignatories(), + maybeTimepoint: maybeTimepoint, + maxWeight: innerWeight, + storeCall: false, + call: innerUnsignedTx.method, + }, + info, + options + ) + } + + updateSelectedSignatory(signatory: AccountInWallet) { + if (signatory === this.selectedSignatory) return + + const currentCallBuilder = this.effectiveCallBuilder() + + this.selectedSignatory = signatory + this.innerBuilder = this.innerFactory(signatory) + this.innerBuilder.effectiveCallBuilder().initFrom(currentCallBuilder) + } + + async #innerInfo(): Promise<{ innerCall: SubmittableExtrinsic<"promise">, innerWeight: any } | null> { + const innerCall = await this.innerBuilder.submittableExtrinsic() + if (innerCall == null) return null + + const paymentInfo = await innerCall.paymentInfo(this.selectedSignatory.account.accountId) + const innerWeight = paymentInfo.weight + + return {innerCall, innerWeight} + } + + #otherSignatories(): AccountId[] { + return this.signatories + .filter((signatory) => signatory != this.selectedSignatory.account.accountId) + .sort() + } +} diff --git a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts new file mode 100644 index 0000000000..20588e22c8 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -0,0 +1,79 @@ +import {Account, Chain, Wallet} from "@shared/core"; +import {AccountInWallet} from "@shared/core/types/wallet"; +import {ApiPromise} from "@polkadot/api"; +import {SubmittableExtrinsic} from "@polkadot/api/types"; +import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; + +export interface TransactionBuilder { + + readonly chain: Chain + + readonly api: ApiPromise + + effectiveCallBuilder(): CallBuilder + + visitAll(visitor: TransactionVisitor): void + + visitSelf(visitor: TransactionVisitor): void + + submittableExtrinsic(): Promise | null> + + unsignedTransaction( + options: OptionsWithMeta, + info: BaseTxInfo + ): Promise +} + +export interface CallBuilder { + + readonly currentCalls: CallBuilding[] + + addCall(call: CallBuilding): void + + setCall(call: CallBuilding): void + + initFrom(callBuilder: CallBuilder): void + + resetCalls(): void +} + +export type CallBuilding = { + viaApi: SubmittableExtrinsic + viaTxWrapper: (info: BaseTxInfo, options: OptionsWithMeta) => UnsignedTransaction +} + +export interface TransactionVisitor { + + visitMultisig?(visit: MultisigVisit): void + + visitCompoundWallet?(visit: CompoundWalletVisit): void + + visitLeaf?(visit: LeafVisit): void +} + +export interface MultisigVisit { + + knownSignatories: AccountInWallet[] + + threshold: number + + selectedSignatory: AccountInWallet + + updateSelectedSignatory(newSignatory: AccountInWallet): void +} + +export interface CompoundWalletVisit { + + allChildrenAccounts: Account[] + + selectedChildrenAccounts: Account[] + + wallet: Wallet + + updateSelectedChildren(selectedChildren: Account[]): void +} + +export interface LeafVisit { + + account: AccountInWallet +} diff --git a/src/renderer/entities/wallet/lib/wallet-utils.ts b/src/renderer/entities/wallet/lib/wallet-utils.ts index d5882644b5..8404824c79 100644 --- a/src/renderer/entities/wallet/lib/wallet-utils.ts +++ b/src/renderer/entities/wallet/lib/wallet-utils.ts @@ -1,4 +1,4 @@ -import { WalletType } from '@shared/core'; +import {WalletFamily, WalletType} from '@shared/core'; import type { Wallet, PolkadotVaultWallet, @@ -20,6 +20,7 @@ export const walletUtils = { isWalletConnect, isWalletConnectFamily, isValidSignatory, + getWalletFamily }; function isPolkadotVault(wallet?: Pick): wallet is PolkadotVaultWallet { @@ -57,6 +58,16 @@ function isWalletConnect(wallet?: Pick): wallet is WalletConnect return wallet?.type === WalletType.WALLET_CONNECT; } +function getWalletFamily(wallet: Pick): WalletFamily { + if (isPolkadotVault(wallet)) return WalletType.POLKADOT_VAULT; + if (isMultisig(wallet)) return WalletType.MULTISIG; + if (isWatchOnly(wallet)) return WalletType.WATCH_ONLY; + if (isWalletConnect(wallet)) return WalletType.WALLET_CONNECT; + if (isNovaWallet(wallet)) return WalletType.NOVA_WALLET; + + throw new Error("Cannot determine wallet family for" + wallet.type) +} + function isWalletConnectFamily(wallet?: Pick): wallet is WalletConnectWallet { return isNovaWallet(wallet) || isWalletConnect(wallet); } diff --git a/src/renderer/pages/Staking/Operations/Restake/Restake.tsx b/src/renderer/pages/Staking/Operations/Restake/Restake.tsx index 01db6940dc..c832f1a327 100644 --- a/src/renderer/pages/Staking/Operations/Restake/Restake.tsx +++ b/src/renderer/pages/Staking/Operations/Restake/Restake.tsx @@ -34,69 +34,69 @@ export const Restake = () => { const navigate = useNavigate(); const { connections } = useNetworkContext(); const { setTxs, txs, setWrappers, wrapTx, buildTransaction } = useTransaction(); - const [searchParams] = useSearchParams(); - const params = useParams<{ chainId: ChainId }>(); + const [searchParams] = useSearchParams(); + const params = useParams<{ chainId: ChainId }>(); - const [isRestakeModalOpen, toggleRestakeModal] = useToggle(true); + const [isRestakeModalOpen, toggleRestakeModal] = useToggle(true); - const [activeStep, setActiveStep] = useState(Step.INIT); + const [activeStep, setActiveStep] = useState(Step.INIT); - const [restakeAmount, setRestakeAmount] = useState(''); - const [description, setDescription] = useState(''); + const [restakeAmount, setRestakeAmount] = useState(''); + const [description, setDescription] = useState(''); - const [unsignedTransactions, setUnsignedTransactions] = useState([]); + const [unsignedTransactions, setUnsignedTransactions] = useState([]); - const [accounts, setAccounts] = useState([]); - const [txAccounts, setTxAccounts] = useState([]); - const [signer, setSigner] = useState(); - const [signatures, setSignatures] = useState([]); + const [accounts, setAccounts] = useState([]); + const [txAccounts, setTxAccounts] = useState([]); + const [signer, setSigner] = useState(); + const [signatures, setSignatures] = useState([]); - const isMultisigWallet = walletUtils.isMultisig(activeWallet); + const isMultisigWallet = walletUtils.isMultisig(activeWallet); - const accountIds = searchParams.get('id')?.split(',') || []; - const chainId = params.chainId || ('' as ChainId); + const accountIds = searchParams.get('id')?.split(',') || []; + const chainId = params.chainId || ('' as ChainId); - useEffect(() => { - priceProviderModel.events.assetsPricesRequested({ includeRates: true }); - }, []); + useEffect(() => { + priceProviderModel.events.assetsPricesRequested({ includeRates: true }); + }, []); - useEffect(() => { - if (!activeAccounts.length || !accountIds.length) return; + useEffect(() => { + if (!activeAccounts.length || !accountIds.length) return; - const accounts = activeAccounts.filter((a) => a.id && accountIds.includes(a.id.toString())); - setAccounts(accounts); - }, [activeAccounts.length]); + const accounts = activeAccounts.filter((a) => a.id && accountIds.includes(a.id.toString())); + setAccounts(accounts); + }, [activeAccounts.length]); - const connection = connections[chainId]; + const connection = connections[chainId]; - if (!connection || accountIds.length === 0) { - return ; - } + if (!connection || accountIds.length === 0) { + return ; + } + + const { api, explorers, addressPrefix, assets, name } = connections[chainId]; + const asset = getRelaychainAsset(assets); + + const goToPrevStep = () => { + if (activeStep === Step.INIT) { + navigate(Paths.STAKING); + } else { + setActiveStep((prev) => prev - 1); + } + }; + + const closeRestakeModal = () => { + toggleRestakeModal(); + setTimeout(() => navigate(Paths.STAKING), DEFAULT_TRANSITION); + }; - const { api, explorers, addressPrefix, assets, name } = connections[chainId]; - const asset = getRelaychainAsset(assets); - - const goToPrevStep = () => { - if (activeStep === Step.INIT) { - navigate(Paths.STAKING); - } else { - setActiveStep((prev) => prev - 1); - } - }; - - const closeRestakeModal = () => { - toggleRestakeModal(); - setTimeout(() => navigate(Paths.STAKING), DEFAULT_TRANSITION); - }; - - if (!api?.isConnected) { - return ( - } onClose={closeRestakeModal} > @@ -107,97 +107,97 @@ export const Restake = () => { - ); - } - - if (!asset) { - return ( - } - onClose={closeRestakeModal} + ); + } + + if (!asset) { + return ( + } + onClose={closeRestakeModal} > -
- -
-
- ); +
+ +
+
+ ); +} + +const onInitResult = ({ accounts, amount, signer, description }: RestakeResult) => { + const transactions = getRestakeTxs(accounts, amount); + + if (signer && isMultisigWallet) { + setWrappers([ + { + signatoryId: signer.accountId, + account: accounts[0], + }, + ]); + setSigner(signer); + setDescription(description || ''); } - const onInitResult = ({ accounts, amount, signer, description }: RestakeResult) => { - const transactions = getRestakeTxs(accounts, amount); - - if (signer && isMultisigWallet) { - setWrappers([ - { - signatoryId: signer.accountId, - account: accounts[0], - }, - ]); - setSigner(signer); - setDescription(description || ''); - } - - setTxs(transactions); - setTxAccounts(accounts); - setRestakeAmount(amount); - setActiveStep(Step.CONFIRMATION); - }; - - const getRestakeTxs = (accounts: Account[], amount: string): Transaction[] => { - return accounts.map(({ accountId }) => - buildTransaction(TransactionType.RESTAKE, toAddress(accountId, { prefix: addressPrefix }), chainId, { - value: amount, - }), - ); - }; - - const onSignResult = (signatures: HexString[], unsigned: UnsignedTransaction[]) => { - setUnsignedTransactions(unsigned); - setSignatures(signatures); - setActiveStep(Step.SUBMIT); - }; - - const explorersProps = { explorers, addressPrefix, asset }; - const restakeValues = new Array(accounts.length).fill(restakeAmount); - const multisigTx = isMultisigWallet ? wrapTx(txs[0], api, addressPrefix) : undefined; - - return ( - <> - } - onClose={closeRestakeModal} - > - {activeStep === Step.INIT && ( - - )} - {activeStep === Step.CONFIRMATION && ( - setActiveStep(Step.SIGNING)} - onGoBack={goToPrevStep} - {...explorersProps} - > - - {t('staking.confirmation.hintRestake')} - - - )} - {activeStep === Step.SIGNING && ( - { + return accounts.map(({ accountId }) => + buildTransaction(TransactionType.RESTAKE, toAddress(accountId, { prefix: addressPrefix }), chainId, { + value: amount, + }), + ); +}; + +const onSignResult = (signatures: HexString[], unsigned: UnsignedTransaction[]) => { + setUnsignedTransactions(unsigned); + setSignatures(signatures); + setActiveStep(Step.SUBMIT); +}; + +const explorersProps = { explorers, addressPrefix, asset }; +const restakeValues = new Array(accounts.length).fill(restakeAmount); +const multisigTx = isMultisigWallet ? wrapTx(txs[0], api, addressPrefix) : undefined; + +return ( + <> + } + onClose={closeRestakeModal} + > + {activeStep === Step.INIT && ( + + )} + {activeStep === Step.CONFIRMATION && ( + setActiveStep(Step.SIGNING)} + onGoBack={goToPrevStep} + {...explorersProps} + > + + {t('staking.confirmation.hintRestake')} + + + )} + {activeStep === Step.SIGNING && ( + => { - const [{ block }, blockHash, genesisHash, metadataRpc, nonce, { specVersion, transactionVersion, specName }] = + const [{block}, blockHash, genesisHash, metadataRpc, nonce, {specVersion, transactionVersion, specName}] = await Promise.all([ api.rpc.chain.getBlock(), api.rpc.chain.getBlockHash(), @@ -54,7 +54,55 @@ export const createTxMetadata = async ( signedExtensions: registry.signedExtensions, }; - return { options, info, registry }; + return {options, info, registry}; +}; + +export type TxMetadata = { registry: TypeRegistry; options: OptionsWithMeta; info: BaseTxInfo } + +export const createTxMetadatas = async ( + addresses: Address[], + api: ApiPromise, +): Promise => { + const [{block}, blockHash, genesisHash, metadataRpc, {specVersion, transactionVersion, specName}] = + await Promise.all([ + api.rpc.chain.getBlock(), + api.rpc.chain.getBlockHash(), + api.rpc.chain.getBlockHash(0), + api.rpc.state.getMetadata(), + api.rpc.state.getRuntimeVersion(), + ]); + + const nonces = await Promise.all(addresses.map((address) => api.rpc.system.accountNextIndex(address))) + + const registry = getRegistry({ + chainName: specName.toString() as GetRegistryOpts['specName'], + specName: specName.toString() as GetRegistryOpts['specName'], + specVersion: specVersion.toNumber(), + metadataRpc: metadataRpc.toHex(), + }); + + const options: OptionsWithMeta = { + metadataRpc: metadataRpc.toHex(), + registry, + signedExtensions: registry.signedExtensions, + }; + + return addresses.map((address, index) => { + const baseTxInfo: BaseTxInfo = { + address: address, + blockHash: blockHash.toString(), + blockNumber: block.header.number.toNumber(), + genesisHash: genesisHash.toString(), + metadataRpc: metadataRpc.toHex(), + nonce: nonces[index].toNumber(), + specVersion: specVersion.toNumber(), + transactionVersion: transactionVersion.toNumber(), + eraPeriod: 64, + tip: 0, + } + + return { registry, options, info: baseTxInfo } + }) }; /** @@ -71,7 +119,7 @@ export const validateCallData = => { - const { block } = await api.rpc.chain.getBlock(); + const {block} = await api.rpc.chain.getBlock(); return block.header.number.toNumber(); };