From a3f5cf173d740f9a6661903514a340c40961b18f Mon Sep 17 00:00:00 2001 From: valentun Date: Tue, 28 Nov 2023 14:18:00 +0300 Subject: [PATCH 1/6] Transaction builder draft --- .../transactionBuilder/lib/compound-wallet.ts | 60 +++++++++ .../transactionBuilder/lib/factory.ts | 74 ++++++++++++ .../entities/transactionBuilder/lib/leaf.ts | 75 ++++++++++++ .../transactionBuilder/lib/multisig.ts | 114 ++++++++++++++++++ .../model/transaction-builder.ts | 56 +++++++++ .../entities/wallet/lib/wallet-utils.ts | 14 ++- src/renderer/shared/core/types/wallet.ts | 7 ++ 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 src/renderer/entities/transactionBuilder/lib/compound-wallet.ts create mode 100644 src/renderer/entities/transactionBuilder/lib/factory.ts create mode 100644 src/renderer/entities/transactionBuilder/lib/leaf.ts create mode 100644 src/renderer/entities/transactionBuilder/lib/multisig.ts create mode 100644 src/renderer/entities/transactionBuilder/model/transaction-builder.ts 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..8edf90e276 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -0,0 +1,60 @@ +import { + CallBuilder, + TransactionBuilder, + TransactionVisitor +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet, Wallet} from "@shared/core/types/wallet"; +import {Account} from "@shared/core"; +import {Exception} from "@zxing/library"; +import {ApiPromise} from "@polkadot/api"; +import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; +import {SubmittableExtrinsic} from "@polkadot/api/types"; + +export class CompoundWalletTransactionBuilder implements TransactionBuilder { + + effectiveCallBuilder: CallBuilder; + + readonly wallet: Wallet + readonly shards: Account[] + + readonly api: ApiPromise + + #inner: TransactionBuilder + + constructor( + api: ApiPromise, + wallet: Wallet, + shards: Account[], + innerFactory: (shard: AccountInWallet) => TransactionBuilder + ) { + this.wallet = wallet + this.shards = shards + this.api = api + + if (shards.length == 0) throw new Exception("Empty shard list") + + const firstAccountInWallet: AccountInWallet = { + wallet: wallet, + account: shards[0] + } + this.#inner = innerFactory(firstAccountInWallet) + this.effectiveCallBuilder = this.#inner.effectiveCallBuilder + } + + visit(visitor: TransactionVisitor): void { + visitor.visitCompoundWallet({ + wallet: this.wallet, + childrenAccounts: this.shards + }) + + this.#inner.visit(visitor) + } + + unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { + return this.#inner.unsignedTransaction(options, info) + } + + submittableExtrinsic(): Promise | null> { + return this.#inner.submittableExtrinsic() + } +} diff --git a/src/renderer/entities/transactionBuilder/lib/factory.ts b/src/renderer/entities/transactionBuilder/lib/factory.ts new file mode 100644 index 0000000000..f11679bb1f --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -0,0 +1,74 @@ +import {Account, AccountId, 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 {Exception} from "@zxing/library"; +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 function createTransactionBuilder( + activeWallet: Wallet, + activeAccounts: Account[], + allWallets: Wallet[], + allAccounts: Account[], + api: ApiPromise +): TransactionBuilder { + + const walletsById = keyBy(allWallets, 'id') + const accountsByAccountId = groupBy(allAccounts, "accountId") + + const createInner = (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, + multisigAccount.threshold, + multisigAccount.signatories.map((signatory) => signatory.accountId), + matchingAccounts, + createInner + ) + + case WalletType.WATCH_ONLY: + throw new Exception("Signing with Watch only is not allowed") + + case WalletType.WALLET_CONNECT: + case WalletType.NOVA_WALLET: + case WalletType.POLKADOT_VAULT: + return new LeafTransactionBuilder(api, accountInWallet) + } + } + + if (activeAccounts.length > 1) { + return new CompoundWalletTransactionBuilder(api, activeWallet, activeAccounts, createInner) + } else { + return createInner({ wallet: activeWallet, account: activeAccounts[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/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts new file mode 100644 index 0000000000..f036367f5f --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -0,0 +1,75 @@ +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 {Exception} from "@zxing/library"; + +export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { + + effectiveCallBuilder: CallBuilder; + #calls: CallBuilding[] + + api: ApiPromise + + readonly accountInWallet: AccountInWallet + + constructor(api: ApiPromise, accountInWallet: AccountInWallet) { + this.effectiveCallBuilder = this + this.#calls = [] + this.api = api + + this.accountInWallet = accountInWallet + } + + visit(_: TransactionVisitor): void { + // nothing interesting to visit in the leaf + } + + addCall(call: CallBuilding): void { + this.#calls.push(call) + } + + resetCalls(): void { + this.#calls = [] + } + + setCall(call: CallBuilding): void { + this.#calls = [call] + } + + async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { + const nestedUnsignedTxs = this.#calls.map(call => call.signing(info, options)) + + if (nestedUnsignedTxs.length == 0) throw new Exception("Cannot sign empty transaction") + + let maybeWrappedInBatch: UnsignedTransaction + if (nestedUnsignedTxs.length > 1) { + const innerMethods = nestedUnsignedTxs.map((nestedUnsignedTx) => nestedUnsignedTx.method) + maybeWrappedInBatch = methods.utility.batch({calls: innerMethods,}, info, options) + } else { + maybeWrappedInBatch = nestedUnsignedTxs[0] + } + + return maybeWrappedInBatch + } + + async submittableExtrinsic(): Promise | null> { + const feeCalls = this.#calls.map(call => call.fee) + + if (feeCalls.length == 0) return null + + let maybeWrappedInBatch: SubmittableExtrinsic<"promise"> + if (feeCalls.length > 1) { + maybeWrappedInBatch = this.api.tx.utility.batch(feeCalls) + } else { + maybeWrappedInBatch = feeCalls[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..d185cc4d78 --- /dev/null +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -0,0 +1,114 @@ +import { + CallBuilder, + TransactionBuilder, + TransactionVisitor +} from "@entities/transactionBuilder/model/transaction-builder"; +import {AccountInWallet} from "@shared/core/types/wallet"; +import {Exception} from "@zxing/library"; +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} from "@shared/core"; + +export class MultisigTransactionBuilder implements TransactionBuilder { + + #inner: TransactionBuilder + + readonly effectiveCallBuilder: CallBuilder + + readonly knownSignatoryAccounts: AccountInWallet[] + #selectedSignatory: AccountInWallet + + readonly signatories: AccountId[] + readonly threshold: number + + readonly api: ApiPromise + + constructor( + api: ApiPromise, + threshold: number, + signatories: AccountId[], + knownSignatoryAccounts: AccountInWallet[], + innerFactory: (signatory: AccountInWallet) => TransactionBuilder, + ) { + this.knownSignatoryAccounts = knownSignatoryAccounts + + this.threshold = threshold + this.signatories = signatories + + this.api = api + + if (knownSignatoryAccounts.length == 0) { + // TODO maybe handle it gracefully? + throw new Exception("No known signatories found") + } + this.#selectedSignatory = knownSignatoryAccounts[0] + this.#inner = innerFactory(this.#selectedSignatory) + + this.effectiveCallBuilder = this.#inner.effectiveCallBuilder + } + + visit(visitor: TransactionVisitor): void { + visitor.visitMultisig({ + knownSignatories: this.knownSignatoryAccounts, + threshold: this.threshold + }) + + this.#inner.visit(visitor) + } + + 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 Exception("Multisig cannot sign empty nested tx") + + const {innerWeight} = innerInfo + const maybeTimepoint = null + + const innerUnsignedTx = await this.#inner.unsignedTransaction(options, info) + + return methods.multisig.asMulti( + { + threshold: this.threshold, + otherSignatories: this.otherSignatories(), + maybeTimepoint: maybeTimepoint, + maxWeight: innerWeight, + storeCall: false, + call: innerUnsignedTx.method, + }, + info, + options + ) + } + + async innerInfo(): Promise<{ innerCall: SubmittableExtrinsic<"promise">, innerWeight: any } | null> { + const innerCall = await this.#inner.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..d2c4de179c --- /dev/null +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -0,0 +1,56 @@ +import {Account, 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 { + + api: ApiPromise + + effectiveCallBuilder: CallBuilder + + visit(visitor: TransactionVisitor): void + + submittableExtrinsic(): Promise | null> + + unsignedTransaction( + options: OptionsWithMeta, + info: BaseTxInfo + ): Promise +} + +export interface CallBuilder { + + addCall(call: CallBuilding): void + + setCall(call: CallBuilding): void + + resetCalls(): void +} + +export type CallBuilding = { + fee: SubmittableExtrinsic + signing: (info: BaseTxInfo, options: OptionsWithMeta) => UnsignedTransaction +} + +export interface TransactionVisitor { + + visitMultisig(visit: MultisigVisit): void + + visitCompoundWallet(visit: CompoundWalletVisit): void +} + +export interface MultisigVisit { + + knownSignatories: AccountInWallet[] + + threshold: number +} + +export interface CompoundWalletVisit { + + childrenAccounts: Account[] + + wallet: Wallet +} diff --git a/src/renderer/entities/wallet/lib/wallet-utils.ts b/src/renderer/entities/wallet/lib/wallet-utils.ts index d5882644b5..7fd3dff25a 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, @@ -9,6 +9,7 @@ import type { WatchOnlyWallet, NovaWalletWallet, } from '@shared/core'; +import {Exception} from "@zxing/library"; export const walletUtils = { isPolkadotVault, @@ -20,6 +21,7 @@ export const walletUtils = { isWalletConnect, isWalletConnectFamily, isValidSignatory, + getWalletFamily }; function isPolkadotVault(wallet?: Pick): wallet is PolkadotVaultWallet { @@ -57,6 +59,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 Exception("Cannot determine wallet family for" + wallet.type) +} + function isWalletConnectFamily(wallet?: Pick): wallet is WalletConnectWallet { return isNovaWallet(wallet) || isWalletConnect(wallet); } diff --git a/src/renderer/shared/core/types/wallet.ts b/src/renderer/shared/core/types/wallet.ts index c5fdac60f6..6e2ff57156 100644 --- a/src/renderer/shared/core/types/wallet.ts +++ b/src/renderer/shared/core/types/wallet.ts @@ -1,4 +1,5 @@ import type { ID } from './general'; +import {Account} from "@shared/core"; type AbstractWallet = { id: ID; @@ -56,3 +57,9 @@ export const enum SigningType { WALLET_CONNECT = 'signing_wc', // NOVA_WALLET = 'signing_nw', } + + +export type AccountInWallet = { + wallet: Wallet + account: Account +} From 17b85e670cf69159adc749cbf0b846b21f8626c6 Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 29 Nov 2023 11:03:01 +0300 Subject: [PATCH 2/6] Switch to Error instead of Exception --- .../entities/transactionBuilder/lib/compound-wallet.ts | 3 +-- src/renderer/entities/transactionBuilder/lib/factory.ts | 3 +-- src/renderer/entities/transactionBuilder/lib/leaf.ts | 3 +-- src/renderer/entities/transactionBuilder/lib/multisig.ts | 5 ++--- src/renderer/entities/wallet/lib/wallet-utils.ts | 3 +-- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts index 8edf90e276..fd8792138d 100644 --- a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -5,7 +5,6 @@ import { } from "@entities/transactionBuilder/model/transaction-builder"; import {AccountInWallet, Wallet} from "@shared/core/types/wallet"; import {Account} from "@shared/core"; -import {Exception} from "@zxing/library"; import {ApiPromise} from "@polkadot/api"; import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; import {SubmittableExtrinsic} from "@polkadot/api/types"; @@ -31,7 +30,7 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { this.shards = shards this.api = api - if (shards.length == 0) throw new Exception("Empty shard list") + if (shards.length == 0) throw new Error("Empty shard list") const firstAccountInWallet: AccountInWallet = { wallet: wallet, diff --git a/src/renderer/entities/transactionBuilder/lib/factory.ts b/src/renderer/entities/transactionBuilder/lib/factory.ts index f11679bb1f..fc4907f026 100644 --- a/src/renderer/entities/transactionBuilder/lib/factory.ts +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -3,7 +3,6 @@ import {TransactionBuilder} from "@entities/transactionBuilder/model/transaction import {AccountInWallet} from "@shared/core/types/wallet"; import keyBy from 'lodash/keyBy'; import {walletUtils} from "@entities/wallet"; -import {Exception} from "@zxing/library"; import {LeafTransactionBuilder} from "@entities/transactionBuilder/lib/leaf"; import {MultisigTransactionBuilder} from "@entities/transactionBuilder/lib/multisig"; import {groupBy} from "lodash"; @@ -40,7 +39,7 @@ export function createTransactionBuilder( ) case WalletType.WATCH_ONLY: - throw new Exception("Signing with Watch only is not allowed") + throw new Error("Signing with Watch only is not allowed") case WalletType.WALLET_CONNECT: case WalletType.NOVA_WALLET: diff --git a/src/renderer/entities/transactionBuilder/lib/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts index f036367f5f..7c4d894ab5 100644 --- a/src/renderer/entities/transactionBuilder/lib/leaf.ts +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -7,7 +7,6 @@ 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 {Exception} from "@zxing/library"; export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { @@ -45,7 +44,7 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { const nestedUnsignedTxs = this.#calls.map(call => call.signing(info, options)) - if (nestedUnsignedTxs.length == 0) throw new Exception("Cannot sign empty transaction") + if (nestedUnsignedTxs.length == 0) throw new Error("Cannot sign empty transaction") let maybeWrappedInBatch: UnsignedTransaction if (nestedUnsignedTxs.length > 1) { diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts index d185cc4d78..7101c114e4 100644 --- a/src/renderer/entities/transactionBuilder/lib/multisig.ts +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -4,7 +4,6 @@ import { TransactionVisitor } from "@entities/transactionBuilder/model/transaction-builder"; import {AccountInWallet} from "@shared/core/types/wallet"; -import {Exception} from "@zxing/library"; import {ApiPromise} from "@polkadot/api"; import {BaseTxInfo, methods, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; import {SubmittableExtrinsic} from "@polkadot/api/types"; @@ -41,7 +40,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { if (knownSignatoryAccounts.length == 0) { // TODO maybe handle it gracefully? - throw new Exception("No known signatories found") + throw new Error("No known signatories found") } this.#selectedSignatory = knownSignatoryAccounts[0] this.#inner = innerFactory(this.#selectedSignatory) @@ -75,7 +74,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { const innerInfo = await this.innerInfo() - if (innerInfo == null) throw new Exception("Multisig cannot sign empty nested tx") + if (innerInfo == null) throw new Error("Multisig cannot sign empty nested tx") const {innerWeight} = innerInfo const maybeTimepoint = null diff --git a/src/renderer/entities/wallet/lib/wallet-utils.ts b/src/renderer/entities/wallet/lib/wallet-utils.ts index 7fd3dff25a..8404824c79 100644 --- a/src/renderer/entities/wallet/lib/wallet-utils.ts +++ b/src/renderer/entities/wallet/lib/wallet-utils.ts @@ -9,7 +9,6 @@ import type { WatchOnlyWallet, NovaWalletWallet, } from '@shared/core'; -import {Exception} from "@zxing/library"; export const walletUtils = { isPolkadotVault, @@ -66,7 +65,7 @@ function getWalletFamily(wallet: Pick): WalletFamily { if (isWalletConnect(wallet)) return WalletType.WALLET_CONNECT; if (isNovaWallet(wallet)) return WalletType.NOVA_WALLET; - throw new Exception("Cannot determine wallet family for" + wallet.type) + throw new Error("Cannot determine wallet family for" + wallet.type) } function isWalletConnectFamily(wallet?: Pick): wallet is WalletConnectWallet { From 678d9c4d34feea96d86214a1caee2394e7068027 Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 29 Nov 2023 12:03:32 +0300 Subject: [PATCH 3/6] Allow to update selected multisig and shard --- .../transactionBuilder/lib/compound-wallet.ts | 37 +++++++++++++++---- .../transactionBuilder/lib/factory.ts | 4 +- .../entities/transactionBuilder/lib/leaf.ts | 25 ++++++++----- .../transactionBuilder/lib/multisig.ts | 36 ++++++++++++------ .../model/transaction-builder.ts | 12 +++++- 5 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts index fd8792138d..c5f86ff8a9 100644 --- a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -8,42 +8,51 @@ import {Account} from "@shared/core"; import {ApiPromise} from "@polkadot/api"; import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; import {SubmittableExtrinsic} from "@polkadot/api/types"; +import {NestedTransactionBuilderFactory} from "@entities/transactionBuilder/lib/factory"; export class CompoundWalletTransactionBuilder implements TransactionBuilder { - effectiveCallBuilder: CallBuilder; - readonly wallet: Wallet readonly shards: Account[] + #selectedShard: Account + readonly api: ApiPromise #inner: TransactionBuilder + readonly #innerFactory: NestedTransactionBuilderFactory constructor( api: ApiPromise, wallet: Wallet, shards: Account[], - innerFactory: (shard: AccountInWallet) => TransactionBuilder + innerFactory: NestedTransactionBuilderFactory ) { this.wallet = wallet this.shards = shards this.api = api + this.#innerFactory = innerFactory if (shards.length == 0) throw new Error("Empty shard list") + const firstShard = shards[0] const firstAccountInWallet: AccountInWallet = { wallet: wallet, - account: shards[0] + account: firstShard } - this.#inner = innerFactory(firstAccountInWallet) - this.effectiveCallBuilder = this.#inner.effectiveCallBuilder + this.#selectedShard = firstShard + this.#inner = this.#innerFactory(firstAccountInWallet) + } + + effectiveCallBuilder(): CallBuilder { + return this.#inner.effectiveCallBuilder() } visit(visitor: TransactionVisitor): void { visitor.visitCompoundWallet({ wallet: this.wallet, - childrenAccounts: this.shards + childrenAccounts: this.shards, + updateSelectedShard: this.updateSelectedShard, }) this.#inner.visit(visitor) @@ -56,4 +65,18 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { submittableExtrinsic(): Promise | null> { return this.#inner.submittableExtrinsic() } + + updateSelectedShard(shard: Account) { + if (shard === this.#selectedShard) return + + const currentCallBuilder = this.effectiveCallBuilder() + const newAccountInWallet: AccountInWallet = { + wallet: this.wallet, + account: shard + } + + this.#selectedShard = shard + this.#inner = this.#innerFactory(newAccountInWallet) + this.#inner.effectiveCallBuilder().initFrom(currentCallBuilder) + } } diff --git a/src/renderer/entities/transactionBuilder/lib/factory.ts b/src/renderer/entities/transactionBuilder/lib/factory.ts index fc4907f026..6e2edcc4fa 100644 --- a/src/renderer/entities/transactionBuilder/lib/factory.ts +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -9,6 +9,8 @@ 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( activeWallet: Wallet, activeAccounts: Account[], @@ -20,7 +22,7 @@ export function createTransactionBuilder( const walletsById = keyBy(allWallets, 'id') const accountsByAccountId = groupBy(allAccounts, "accountId") - const createInner = (accountInWallet: AccountInWallet) => { + const createInner: NestedTransactionBuilderFactory = (accountInWallet: AccountInWallet) => { const wallet = accountInWallet.wallet switch (walletUtils.getWalletFamily(wallet)) { diff --git a/src/renderer/entities/transactionBuilder/lib/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts index 7c4d894ab5..bafaf9e253 100644 --- a/src/renderer/entities/transactionBuilder/lib/leaf.ts +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -10,39 +10,45 @@ import {SubmittableExtrinsic} from "@polkadot/api/types"; export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { - effectiveCallBuilder: CallBuilder; - #calls: CallBuilding[] + currentCalls: CallBuilding[] api: ApiPromise readonly accountInWallet: AccountInWallet constructor(api: ApiPromise, accountInWallet: AccountInWallet) { - this.effectiveCallBuilder = this - this.#calls = [] + this.currentCalls = [] this.api = api this.accountInWallet = accountInWallet } + effectiveCallBuilder(): CallBuilder { + return this + } + visit(_: TransactionVisitor): void { // nothing interesting to visit in the leaf } addCall(call: CallBuilding): void { - this.#calls.push(call) + this.currentCalls.push(call) } resetCalls(): void { - this.#calls = [] + this.currentCalls = [] } setCall(call: CallBuilding): void { - this.#calls = [call] + this.currentCalls = [call] + } + + initFrom(callBuilder: CallBuilder): void { + this.currentCalls = callBuilder.currentCalls } async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { - const nestedUnsignedTxs = this.#calls.map(call => call.signing(info, options)) + const nestedUnsignedTxs = this.currentCalls.map(call => call.signing(info, options)) if (nestedUnsignedTxs.length == 0) throw new Error("Cannot sign empty transaction") @@ -58,7 +64,7 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { } async submittableExtrinsic(): Promise | null> { - const feeCalls = this.#calls.map(call => call.fee) + const feeCalls = this.currentCalls.map(call => call.fee) if (feeCalls.length == 0) return null @@ -71,4 +77,5 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { return maybeWrappedInBatch } + } diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts index 7101c114e4..52558b9446 100644 --- a/src/renderer/entities/transactionBuilder/lib/multisig.ts +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -9,12 +9,12 @@ import {BaseTxInfo, methods, OptionsWithMeta, UnsignedTransaction} from "@substr import {SubmittableExtrinsic} from "@polkadot/api/types"; import {isOldMultisigPallet} from "@entities/transaction"; import {AccountId} from "@shared/core"; +import {NestedTransactionBuilderFactory} from "@entities/transactionBuilder/lib/factory"; export class MultisigTransactionBuilder implements TransactionBuilder { #inner: TransactionBuilder - - readonly effectiveCallBuilder: CallBuilder + readonly #innerFactory: NestedTransactionBuilderFactory readonly knownSignatoryAccounts: AccountInWallet[] #selectedSignatory: AccountInWallet @@ -29,9 +29,10 @@ export class MultisigTransactionBuilder implements TransactionBuilder { threshold: number, signatories: AccountId[], knownSignatoryAccounts: AccountInWallet[], - innerFactory: (signatory: AccountInWallet) => TransactionBuilder, + innerFactory: NestedTransactionBuilderFactory, ) { this.knownSignatoryAccounts = knownSignatoryAccounts + this.#innerFactory = innerFactory this.threshold = threshold this.signatories = signatories @@ -44,26 +45,29 @@ export class MultisigTransactionBuilder implements TransactionBuilder { } this.#selectedSignatory = knownSignatoryAccounts[0] this.#inner = innerFactory(this.#selectedSignatory) + } - this.effectiveCallBuilder = this.#inner.effectiveCallBuilder + effectiveCallBuilder(): CallBuilder { + return this.#inner.effectiveCallBuilder() } visit(visitor: TransactionVisitor): void { visitor.visitMultisig({ knownSignatories: this.knownSignatoryAccounts, - threshold: this.threshold + threshold: this.threshold, + updateSelectedSignatory: this.updateSelectedSignatory, }) this.#inner.visit(visitor) } async submittableExtrinsic(): Promise | null> { - const innerInfo = await this.innerInfo() + const innerInfo = await this.#innerInfo() if (innerInfo == null) return null const {innerCall, innerWeight} = innerInfo - const otherSignatories = this.otherSignatories() + const otherSignatories = this.#otherSignatories() const maybeTimepoint = null return isOldMultisigPallet(this.api) ? @@ -73,7 +77,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { } async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { - const innerInfo = await this.innerInfo() + const innerInfo = await this.#innerInfo() if (innerInfo == null) throw new Error("Multisig cannot sign empty nested tx") const {innerWeight} = innerInfo @@ -84,7 +88,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { return methods.multisig.asMulti( { threshold: this.threshold, - otherSignatories: this.otherSignatories(), + otherSignatories: this.#otherSignatories(), maybeTimepoint: maybeTimepoint, maxWeight: innerWeight, storeCall: false, @@ -95,7 +99,17 @@ export class MultisigTransactionBuilder implements TransactionBuilder { ) } - async innerInfo(): Promise<{ innerCall: SubmittableExtrinsic<"promise">, innerWeight: any } | null> { + updateSelectedSignatory(signatory: AccountInWallet) { + if (signatory === this.#selectedSignatory) return + + const currentCallBuilder = this.effectiveCallBuilder() + + this.#selectedSignatory = signatory + this.#inner = this.#innerFactory(signatory) + this.#inner.effectiveCallBuilder().initFrom(currentCallBuilder) + } + + async #innerInfo(): Promise<{ innerCall: SubmittableExtrinsic<"promise">, innerWeight: any } | null> { const innerCall = await this.#inner.submittableExtrinsic() if (innerCall == null) return null @@ -105,7 +119,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { return {innerCall, innerWeight} } - otherSignatories(): AccountId[] { + #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 index d2c4de179c..0730f54677 100644 --- a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -6,9 +6,9 @@ import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwra export interface TransactionBuilder { - api: ApiPromise + readonly api: ApiPromise - effectiveCallBuilder: CallBuilder + effectiveCallBuilder(): CallBuilder visit(visitor: TransactionVisitor): void @@ -22,10 +22,14 @@ export interface TransactionBuilder { export interface CallBuilder { + readonly currentCalls: CallBuilding[] + addCall(call: CallBuilding): void setCall(call: CallBuilding): void + initFrom(callBuilder: CallBuilder): void + resetCalls(): void } @@ -46,6 +50,8 @@ export interface MultisigVisit { knownSignatories: AccountInWallet[] threshold: number + + updateSelectedSignatory(newSignatory: AccountInWallet): void } export interface CompoundWalletVisit { @@ -53,4 +59,6 @@ export interface CompoundWalletVisit { childrenAccounts: Account[] wallet: Wallet + + updateSelectedShard(shard: Account): void } From 2cc9ced9bf67524277ba437596ec91430872ea45 Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 29 Nov 2023 13:01:11 +0300 Subject: [PATCH 4/6] Improve builder interface to make multi-signer tx easier --- .../transactionBuilder/lib/compound-wallet.ts | 56 +++++++++---------- .../transactionBuilder/lib/factory.ts | 10 ++-- .../entities/transactionBuilder/lib/leaf.ts | 8 ++- .../transactionBuilder/lib/multisig.ts | 10 +++- .../model/transaction-builder.ts | 17 +++++- 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts index c5f86ff8a9..dafc57dc09 100644 --- a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -8,54 +8,57 @@ import {Account} from "@shared/core"; import {ApiPromise} from "@polkadot/api"; import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwrapper-polkadot"; import {SubmittableExtrinsic} from "@polkadot/api/types"; -import {NestedTransactionBuilderFactory} from "@entities/transactionBuilder/lib/factory"; +import {LeafTransactionBuilder} from "@entities/transactionBuilder/lib/leaf"; export class CompoundWalletTransactionBuilder implements TransactionBuilder { readonly wallet: Wallet - readonly shards: Account[] + readonly allChildrenAccounts: Account[] - #selectedShard: Account + #selectedSChildrenAccounts: Account[] readonly api: ApiPromise #inner: TransactionBuilder - readonly #innerFactory: NestedTransactionBuilderFactory constructor( api: ApiPromise, wallet: Wallet, - shards: Account[], - innerFactory: NestedTransactionBuilderFactory + childrenAccounts: Account[], ) { this.wallet = wallet - this.shards = shards + this.allChildrenAccounts = childrenAccounts this.api = api - this.#innerFactory = innerFactory - if (shards.length == 0) throw new Error("Empty shard list") + if (childrenAccounts.length == 0) throw new Error("Empty children accounts list") - const firstShard = shards[0] - const firstAccountInWallet: AccountInWallet = { + const firstChild = childrenAccounts[0] + const firstChildInWallet: AccountInWallet = { wallet: wallet, - account: firstShard + account: firstChild } - this.#selectedShard = firstShard - this.#inner = this.#innerFactory(firstAccountInWallet) + this.#selectedSChildrenAccounts = [firstChild] + // We cannot have complex structure for wallets with multiple accounts per chain + this.#inner = new LeafTransactionBuilder(api, firstChildInWallet) } effectiveCallBuilder(): CallBuilder { return this.#inner.effectiveCallBuilder() } - visit(visitor: TransactionVisitor): void { + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + + this.#inner.visitAll(visitor) + } + + visitSelf(visitor: TransactionVisitor) { visitor.visitCompoundWallet({ wallet: this.wallet, - childrenAccounts: this.shards, - updateSelectedShard: this.updateSelectedShard, + allChildrenAccounts: this.allChildrenAccounts, + selectedChildrenAccounts: this.#selectedSChildrenAccounts, + updateSelectedChildren: this.#updateSelectedSChildren, }) - - this.#inner.visit(visitor) } unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { @@ -66,17 +69,8 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { return this.#inner.submittableExtrinsic() } - updateSelectedShard(shard: Account) { - if (shard === this.#selectedShard) return - - const currentCallBuilder = this.effectiveCallBuilder() - const newAccountInWallet: AccountInWallet = { - wallet: this.wallet, - account: shard - } - - this.#selectedShard = shard - this.#inner = this.#innerFactory(newAccountInWallet) - this.#inner.effectiveCallBuilder().initFrom(currentCallBuilder) + #updateSelectedSChildren(selectedChildren: Account[]) { + // No need to re-create `inner` 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 index 6e2edcc4fa..75f343417d 100644 --- a/src/renderer/entities/transactionBuilder/lib/factory.ts +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -12,8 +12,8 @@ import {ApiPromise} from "@polkadot/api"; export type NestedTransactionBuilderFactory = (shard: AccountInWallet) => TransactionBuilder export function createTransactionBuilder( - activeWallet: Wallet, - activeAccounts: Account[], + selectedWallet: Wallet, + selectedAccounts: Account[], allWallets: Wallet[], allAccounts: Account[], api: ApiPromise @@ -50,10 +50,10 @@ export function createTransactionBuilder( } } - if (activeAccounts.length > 1) { - return new CompoundWalletTransactionBuilder(api, activeWallet, activeAccounts, createInner) + if (selectedAccounts.length > 1) { + return new CompoundWalletTransactionBuilder(api, selectedWallet, selectedAccounts) } else { - return createInner({ wallet: activeWallet, account: activeAccounts[0] }) + return createInner({ wallet: selectedWallet, account: selectedAccounts[0] }) } } diff --git a/src/renderer/entities/transactionBuilder/lib/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts index bafaf9e253..3fd482833b 100644 --- a/src/renderer/entities/transactionBuilder/lib/leaf.ts +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -27,8 +27,12 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { return this } - visit(_: TransactionVisitor): void { - // nothing interesting to visit in the leaf + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + } + + visitSelf(visitor: TransactionVisitor) { + visitor.visitLeaf({ account: this.accountInWallet }) } addCall(call: CallBuilding): void { diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts index 52558b9446..e74e4f15f2 100644 --- a/src/renderer/entities/transactionBuilder/lib/multisig.ts +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -51,14 +51,18 @@ export class MultisigTransactionBuilder implements TransactionBuilder { return this.#inner.effectiveCallBuilder() } - visit(visitor: TransactionVisitor): void { + visitAll(visitor: TransactionVisitor): void { + this.visitSelf(visitor) + + this.#inner.visitAll(visitor) + } + + visitSelf(visitor: TransactionVisitor) { visitor.visitMultisig({ knownSignatories: this.knownSignatoryAccounts, threshold: this.threshold, updateSelectedSignatory: this.updateSelectedSignatory, }) - - this.#inner.visit(visitor) } async submittableExtrinsic(): Promise | null> { diff --git a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts index 0730f54677..1b12dee124 100644 --- a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -10,7 +10,9 @@ export interface TransactionBuilder { effectiveCallBuilder(): CallBuilder - visit(visitor: TransactionVisitor): void + visitAll(visitor: TransactionVisitor): void + + visitSelf(visitor: TransactionVisitor): void submittableExtrinsic(): Promise | null> @@ -43,6 +45,8 @@ export interface TransactionVisitor { visitMultisig(visit: MultisigVisit): void visitCompoundWallet(visit: CompoundWalletVisit): void + + visitLeaf(visit: LeafVisit): void } export interface MultisigVisit { @@ -56,9 +60,16 @@ export interface MultisigVisit { export interface CompoundWalletVisit { - childrenAccounts: Account[] + allChildrenAccounts: Account[] + + selectedChildrenAccounts: Account[] wallet: Wallet - updateSelectedShard(shard: Account): void + updateSelectedChildren(selectedChildren: Account[]): void +} + +export interface LeafVisit { + + account: AccountInWallet } From d5c048265872eff8cff8105d3cff91b84be81f53 Mon Sep 17 00:00:00 2001 From: valentun Date: Wed, 29 Nov 2023 15:11:33 +0300 Subject: [PATCH 5/6] Integration helpers --- .../transactionBuilder/lib/compound-wallet.ts | 2 + .../lib/helpers/amount-reduction.ts | 48 ++++++++++ .../transactionBuilder/lib/helpers/helpers.ts | 88 +++++++++++++++++++ .../entities/transactionBuilder/lib/leaf.ts | 2 + .../transactionBuilder/lib/multisig.ts | 7 +- .../model/transaction-builder.ts | 12 ++- src/renderer/shared/core/types/wallet.ts | 12 +++ src/renderer/shared/lib/utils/substrate.ts | 66 ++++++++++++-- 8 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 src/renderer/entities/transactionBuilder/lib/helpers/amount-reduction.ts create mode 100644 src/renderer/entities/transactionBuilder/lib/helpers/helpers.ts diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts index dafc57dc09..6f4b6411d9 100644 --- a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -53,6 +53,8 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { } visitSelf(visitor: TransactionVisitor) { + if (visitor.visitCompoundWallet == undefined) return + visitor.visitCompoundWallet({ wallet: this.wallet, allChildrenAccounts: this.allChildrenAccounts, 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 index 3fd482833b..98e1be0c22 100644 --- a/src/renderer/entities/transactionBuilder/lib/leaf.ts +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -32,6 +32,8 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { } visitSelf(visitor: TransactionVisitor) { + if (visitor.visitLeaf == undefined) return + visitor.visitLeaf({ account: this.accountInWallet }) } diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts index e74e4f15f2..9cecda57b0 100644 --- a/src/renderer/entities/transactionBuilder/lib/multisig.ts +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -51,16 +51,19 @@ export class MultisigTransactionBuilder implements TransactionBuilder { return this.#inner.effectiveCallBuilder() } - visitAll(visitor: TransactionVisitor): void { + visitAll(visitor: Partial): void { this.visitSelf(visitor) this.#inner.visitAll(visitor) } - visitSelf(visitor: TransactionVisitor) { + visitSelf(visitor: Partial) { + if (visitor.visitMultisig == undefined) return + visitor.visitMultisig({ knownSignatories: this.knownSignatoryAccounts, threshold: this.threshold, + selectedSignatory: this.#selectedSignatory, updateSelectedSignatory: this.updateSelectedSignatory, }) } diff --git a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts index 1b12dee124..4bf46fa789 100644 --- a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -1,4 +1,4 @@ -import {Account, Wallet} from "@shared/core"; +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"; @@ -6,6 +6,8 @@ import {BaseTxInfo, OptionsWithMeta, UnsignedTransaction} from "@substrate/txwra export interface TransactionBuilder { + readonly chain: Chain + readonly api: ApiPromise effectiveCallBuilder(): CallBuilder @@ -42,11 +44,11 @@ export type CallBuilding = { export interface TransactionVisitor { - visitMultisig(visit: MultisigVisit): void + visitMultisig?(visit: MultisigVisit): void - visitCompoundWallet(visit: CompoundWalletVisit): void + visitCompoundWallet?(visit: CompoundWalletVisit): void - visitLeaf(visit: LeafVisit): void + visitLeaf?(visit: LeafVisit): void } export interface MultisigVisit { @@ -55,6 +57,8 @@ export interface MultisigVisit { threshold: number + selectedSignatory: AccountInWallet + updateSelectedSignatory(newSignatory: AccountInWallet): void } diff --git a/src/renderer/shared/core/types/wallet.ts b/src/renderer/shared/core/types/wallet.ts index 6e2ff57156..df1aae45cc 100644 --- a/src/renderer/shared/core/types/wallet.ts +++ b/src/renderer/shared/core/types/wallet.ts @@ -63,3 +63,15 @@ export type AccountInWallet = { wallet: Wallet account: Account } + +export type AccountsInWallet = { + wallet: Wallet + accounts: Account[] +} + +export function asMany(accountInWallet: AccountInWallet): AccountsInWallet { + return { + wallet: accountInWallet.wallet, + accounts: [accountInWallet.account] + } +} diff --git a/src/renderer/shared/lib/utils/substrate.ts b/src/renderer/shared/lib/utils/substrate.ts index 5481ba5977..94b7065b3c 100644 --- a/src/renderer/shared/lib/utils/substrate.ts +++ b/src/renderer/shared/lib/utils/substrate.ts @@ -1,10 +1,10 @@ -import { ApiPromise } from '@polkadot/api'; -import { BaseTxInfo, getRegistry, GetRegistryOpts, OptionsWithMeta, TypeRegistry } from '@substrate/txwrapper-polkadot'; -import { isHex, hexToU8a, bnMin, BN_TWO, BN } from '@polkadot/util'; -import { blake2AsHex } from '@polkadot/util-crypto'; +import {ApiPromise} from '@polkadot/api'; +import {BaseTxInfo, getRegistry, GetRegistryOpts, OptionsWithMeta, TypeRegistry} from '@substrate/txwrapper-polkadot'; +import {isHex, hexToU8a, bnMin, BN_TWO, BN} from '@polkadot/util'; +import {blake2AsHex} from '@polkadot/util-crypto'; -import { Address, CallData, CallHash } from '@shared/core'; -import { DEFAULT_TIME, ONE_DAY, THRESHOLD } from '@entities/network/lib/common/constants'; +import {Address, CallData, CallHash} from '@shared/core'; +import {DEFAULT_TIME, ONE_DAY, THRESHOLD} from '@entities/network/lib/common/constants'; const V3_LABEL = 'V3'; const UNUSED_LABEL = 'unused'; @@ -18,7 +18,7 @@ export const createTxMetadata = async ( accountId: Address, api: ApiPromise, ): Promise<{ registry: TypeRegistry; options: OptionsWithMeta; info: BaseTxInfo }> => { - 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(); }; From a07de6f45bceb766f756610b5992ba9599c6c817 Mon Sep 17 00:00:00 2001 From: valentun Date: Thu, 30 Nov 2023 17:19:50 +0300 Subject: [PATCH 6/6] Fix PR issues --- .../transactionBuilder/lib/compound-wallet.ts | 29 +- .../transactionBuilder/lib/factory.ts | 8 +- .../entities/transactionBuilder/lib/leaf.ts | 28 +- .../transactionBuilder/lib/multisig.ts | 45 +-- .../model/transaction-builder.ts | 4 +- .../Staking/Operations/Restake/Restake.tsx | 276 +++++++++--------- 6 files changed, 203 insertions(+), 187 deletions(-) diff --git a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts index 6f4b6411d9..d1ceb15303 100644 --- a/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts +++ b/src/renderer/entities/transactionBuilder/lib/compound-wallet.ts @@ -4,7 +4,7 @@ import { TransactionVisitor } from "@entities/transactionBuilder/model/transaction-builder"; import {AccountInWallet, Wallet} from "@shared/core/types/wallet"; -import {Account} from "@shared/core"; +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"; @@ -15,14 +15,16 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { readonly wallet: Wallet readonly allChildrenAccounts: Account[] - #selectedSChildrenAccounts: Account[] - readonly api: ApiPromise - #inner: TransactionBuilder + readonly chain: Chain + + private innerBuilder: TransactionBuilder + private selectedSChildrenAccounts: Account[] constructor( api: ApiPromise, + chain: Chain, wallet: Wallet, childrenAccounts: Account[], ) { @@ -37,19 +39,20 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { wallet: wallet, account: firstChild } - this.#selectedSChildrenAccounts = [firstChild] + this.selectedSChildrenAccounts = [firstChild] // We cannot have complex structure for wallets with multiple accounts per chain - this.#inner = new LeafTransactionBuilder(api, firstChildInWallet) + this.innerBuilder = new LeafTransactionBuilder(api, firstChildInWallet, chain) + this.chain = chain } effectiveCallBuilder(): CallBuilder { - return this.#inner.effectiveCallBuilder() + return this.innerBuilder.effectiveCallBuilder() } visitAll(visitor: TransactionVisitor): void { this.visitSelf(visitor) - this.#inner.visitAll(visitor) + this.innerBuilder.visitAll(visitor) } visitSelf(visitor: TransactionVisitor) { @@ -58,21 +61,21 @@ export class CompoundWalletTransactionBuilder implements TransactionBuilder { visitor.visitCompoundWallet({ wallet: this.wallet, allChildrenAccounts: this.allChildrenAccounts, - selectedChildrenAccounts: this.#selectedSChildrenAccounts, + selectedChildrenAccounts: this.selectedSChildrenAccounts, updateSelectedChildren: this.#updateSelectedSChildren, }) } unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { - return this.#inner.unsignedTransaction(options, info) + return this.innerBuilder.unsignedTransaction(options, info) } submittableExtrinsic(): Promise | null> { - return this.#inner.submittableExtrinsic() + return this.innerBuilder.submittableExtrinsic() } #updateSelectedSChildren(selectedChildren: Account[]) { - // No need to re-create `inner` since it is the leaf and won't change anyway - this.#selectedSChildrenAccounts = selectedChildren + // 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 index 75f343417d..d131bcab44 100644 --- a/src/renderer/entities/transactionBuilder/lib/factory.ts +++ b/src/renderer/entities/transactionBuilder/lib/factory.ts @@ -1,4 +1,4 @@ -import {Account, AccountId, MultisigAccount, Wallet, WalletType} from "@shared/core"; +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'; @@ -16,6 +16,7 @@ export function createTransactionBuilder( selectedAccounts: Account[], allWallets: Wallet[], allAccounts: Account[], + chain: Chain, api: ApiPromise ): TransactionBuilder { @@ -34,6 +35,7 @@ export function createTransactionBuilder( return new MultisigTransactionBuilder( api, + chain, multisigAccount.threshold, multisigAccount.signatories.map((signatory) => signatory.accountId), matchingAccounts, @@ -46,12 +48,12 @@ export function createTransactionBuilder( case WalletType.WALLET_CONNECT: case WalletType.NOVA_WALLET: case WalletType.POLKADOT_VAULT: - return new LeafTransactionBuilder(api, accountInWallet) + return new LeafTransactionBuilder(api, accountInWallet, chain) } } if (selectedAccounts.length > 1) { - return new CompoundWalletTransactionBuilder(api, selectedWallet, selectedAccounts) + return new CompoundWalletTransactionBuilder(api, chain, selectedWallet, selectedAccounts) } else { return createInner({ wallet: selectedWallet, account: selectedAccounts[0] }) } diff --git a/src/renderer/entities/transactionBuilder/lib/leaf.ts b/src/renderer/entities/transactionBuilder/lib/leaf.ts index 98e1be0c22..593cc8ccf6 100644 --- a/src/renderer/entities/transactionBuilder/lib/leaf.ts +++ b/src/renderer/entities/transactionBuilder/lib/leaf.ts @@ -7,18 +7,26 @@ 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[] - api: ApiPromise + readonly api: ApiPromise + readonly chain: Chain readonly accountInWallet: AccountInWallet - constructor(api: ApiPromise, accountInWallet: AccountInWallet) { + constructor( + api: ApiPromise, + accountInWallet: AccountInWallet, + chain: Chain, + ) { this.currentCalls = [] + this.api = api + this.chain = chain this.accountInWallet = accountInWallet } @@ -34,7 +42,7 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { visitSelf(visitor: TransactionVisitor) { if (visitor.visitLeaf == undefined) return - visitor.visitLeaf({ account: this.accountInWallet }) + visitor.visitLeaf({account: this.accountInWallet}) } addCall(call: CallBuilding): void { @@ -54,14 +62,14 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { } async unsignedTransaction(options: OptionsWithMeta, info: BaseTxInfo): Promise { - const nestedUnsignedTxs = this.currentCalls.map(call => call.signing(info, options)) + 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.batch({calls: innerMethods,}, info, options) + maybeWrappedInBatch = methods.utility.batchAll({calls: innerMethods,}, info, options) } else { maybeWrappedInBatch = nestedUnsignedTxs[0] } @@ -70,15 +78,15 @@ export class LeafTransactionBuilder implements TransactionBuilder, CallBuilder { } async submittableExtrinsic(): Promise | null> { - const feeCalls = this.currentCalls.map(call => call.fee) + const viaApiCalls = this.currentCalls.map(call => call.viaApi) - if (feeCalls.length == 0) return null + if (viaApiCalls.length == 0) return null let maybeWrappedInBatch: SubmittableExtrinsic<"promise"> - if (feeCalls.length > 1) { - maybeWrappedInBatch = this.api.tx.utility.batch(feeCalls) + if (viaApiCalls.length > 1) { + maybeWrappedInBatch = this.api.tx.utility.batchAll(viaApiCalls) } else { - maybeWrappedInBatch = feeCalls[0] + maybeWrappedInBatch = viaApiCalls[0] } return maybeWrappedInBatch diff --git a/src/renderer/entities/transactionBuilder/lib/multisig.ts b/src/renderer/entities/transactionBuilder/lib/multisig.ts index 9cecda57b0..ba439be768 100644 --- a/src/renderer/entities/transactionBuilder/lib/multisig.ts +++ b/src/renderer/entities/transactionBuilder/lib/multisig.ts @@ -8,62 +8,65 @@ 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} from "@shared/core"; +import {AccountId, Chain} from "@shared/core"; import {NestedTransactionBuilderFactory} from "@entities/transactionBuilder/lib/factory"; export class MultisigTransactionBuilder implements TransactionBuilder { - #inner: TransactionBuilder - readonly #innerFactory: NestedTransactionBuilderFactory - readonly knownSignatoryAccounts: AccountInWallet[] - #selectedSignatory: 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.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.#inner = innerFactory(this.#selectedSignatory) + this.selectedSignatory = knownSignatoryAccounts[0] + this.innerBuilder = innerFactory(this.selectedSignatory) } effectiveCallBuilder(): CallBuilder { - return this.#inner.effectiveCallBuilder() + return this.innerBuilder.effectiveCallBuilder() } - visitAll(visitor: Partial): void { + visitAll(visitor: TransactionVisitor): void { this.visitSelf(visitor) - this.#inner.visitAll(visitor) + this.innerBuilder.visitAll(visitor) } - visitSelf(visitor: Partial) { + visitSelf(visitor: TransactionVisitor) { if (visitor.visitMultisig == undefined) return visitor.visitMultisig({ knownSignatories: this.knownSignatoryAccounts, threshold: this.threshold, - selectedSignatory: this.#selectedSignatory, + selectedSignatory: this.selectedSignatory, updateSelectedSignatory: this.updateSelectedSignatory, }) } @@ -90,7 +93,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { const {innerWeight} = innerInfo const maybeTimepoint = null - const innerUnsignedTx = await this.#inner.unsignedTransaction(options, info) + const innerUnsignedTx = await this.innerBuilder.unsignedTransaction(options, info) return methods.multisig.asMulti( { @@ -107,20 +110,20 @@ export class MultisigTransactionBuilder implements TransactionBuilder { } updateSelectedSignatory(signatory: AccountInWallet) { - if (signatory === this.#selectedSignatory) return + if (signatory === this.selectedSignatory) return const currentCallBuilder = this.effectiveCallBuilder() - this.#selectedSignatory = signatory - this.#inner = this.#innerFactory(signatory) - this.#inner.effectiveCallBuilder().initFrom(currentCallBuilder) + 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.#inner.submittableExtrinsic() + const innerCall = await this.innerBuilder.submittableExtrinsic() if (innerCall == null) return null - const paymentInfo = await innerCall.paymentInfo(this.#selectedSignatory.account.accountId) + const paymentInfo = await innerCall.paymentInfo(this.selectedSignatory.account.accountId) const innerWeight = paymentInfo.weight return {innerCall, innerWeight} @@ -128,7 +131,7 @@ export class MultisigTransactionBuilder implements TransactionBuilder { #otherSignatories(): AccountId[] { return this.signatories - .filter((signatory) => signatory != this.#selectedSignatory.account.accountId) + .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 index 4bf46fa789..20588e22c8 100644 --- a/src/renderer/entities/transactionBuilder/model/transaction-builder.ts +++ b/src/renderer/entities/transactionBuilder/model/transaction-builder.ts @@ -38,8 +38,8 @@ export interface CallBuilder { } export type CallBuilding = { - fee: SubmittableExtrinsic - signing: (info: BaseTxInfo, options: OptionsWithMeta) => UnsignedTransaction + viaApi: SubmittableExtrinsic + viaTxWrapper: (info: BaseTxInfo, options: OptionsWithMeta) => UnsignedTransaction } export interface TransactionVisitor { 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 && ( +