Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/base/PolymeshTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,16 @@ export class PolymeshTransaction<

// eslint-disable-next-line require-jsdoc
protected composeTx(): SubmittableExtrinsic<'promise', ISubmittableResult> {
const { transaction, args } = this;
const baseTx = this.getBaseTransaction();

return this.wrapProposalIfNeeded(baseTx);
}

const tx = transaction(...args);
// eslint-disable-next-line require-jsdoc
protected getBaseTransaction(): SubmittableExtrinsic<'promise', ISubmittableResult> {
const { transaction, args } = this;

return this.wrapProposalIfNeeded(tx);
return transaction(...args);
}

// eslint-disable-next-line require-jsdoc
Expand Down
83 changes: 66 additions & 17 deletions src/base/PolymeshTransactionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,24 +557,27 @@ export abstract class PolymeshTransactionBase<
/**
* Retrieve a breakdown of the fees required to run this transaction, as well as the Account responsible for paying them
*
* @param asProposal - When `true` (default), treats the transaction as a MultiSig proposal if the signing account is a MultiSig signer.
* When `false`, treats the transaction as a direct transaction from the signing account, ignoring the MultiSig.
*
* @note these values might be inaccurate if the transaction is run at a later time. This can be due to a governance vote or other
* chain related factors (like modifications to a specific subsidizer relationship or a chain upgrade)
*/
public async getTotalFees(): Promise<PayingAccountFees> {
public async getTotalFees(asProposal = true): Promise<PayingAccountFees> {
const { signingAddress } = this;

const composedTx = this.composeTx();
const composedTx = this.composeTxForFees(asProposal);

const paymentInfoPromise = composedTx.paymentInfo(signingAddress);

const protocol = await this.getProtocolFees();

const [payingAccount, { partialFee }] = await Promise.all([
this.getPayingAccount(),
const payingAccount = await this.getPayingAccount(asProposal);

const [{ partialFee }, { free: balance }] = await Promise.all([
paymentInfoPromise,
payingAccount.account.getBalance(),
]);

const { free: balance } = await payingAccount.account.getBalance();
const gas = balanceToBigNumber(partialFee);

return {
Expand Down Expand Up @@ -850,21 +853,26 @@ export abstract class PolymeshTransactionBase<
/**
* Returns a representation intended for offline signers.
*
* @param metadata - Additional information attached to the payload, such as IDs or memos about the transaction
* @param asProposal - When `true` (default), treats the transaction as a MultiSig proposal if the signing account is a MultiSig signer.
* When `false`, treats the transaction as a direct transaction from the signing account, ignoring the MultiSig.
*
* @note Usually `.run()` should be preferred due to is simplicity.
*
* @note When using this method, details like account nonces, and transaction mortality require extra consideration. Generating a payload for offline sign implies asynchronicity. If using this API, be sure each procedure is created with the correct nonce, accounting for in flight transactions, and the lifetime is sufficient.
*
*/
public async toSignablePayload(
metadata: Record<string, string> = {}
metadata: Record<string, string> = {},
asProposal = true
): Promise<TransactionPayload> {
const {
mortality,
signingAddress,
context,
context: { polymeshApi },
} = this;
const tx = this.composeTx();
const tx = this.composeTxForFees(asProposal);

const [tipHash, latestBlockNumber] = await Promise.all([
polymeshApi.rpc.chain.getFinalizedHead(),
Expand Down Expand Up @@ -911,7 +919,7 @@ export abstract class PolymeshTransactionBase<
rawPayload: rawSignerPayload.toRaw(),
method: tx.toHex(),
metadata,
multiSig: this.multiSig?.address ?? null,
multiSig: asProposal ? this.multiSig?.address ?? null : null,
};
}

Expand All @@ -928,11 +936,14 @@ export abstract class PolymeshTransactionBase<
* Retrieve the Account that would pay fees for the transaction if it was run at this moment, as well as the total amount that can be
* charged to it (allowance) in case of a subsidy
*
* @param asProposal - When `true` (default), uses MultiSig payer if the signing account is a MultiSig signer.
* When `false`, uses the signing account directly, ignoring the MultiSig.
*
* @note the paying Account might change if, before running the transaction, the caller Account enters (or leaves)
* a subsidizer relationship. A governance vote or chain upgrade could also cause the value to change between the time
* this method is called and the time the transaction is run
*/
private async getPayingAccount(): Promise<PayingAccount> {
private async getPayingAccount(asProposal: boolean): Promise<PayingAccount> {
const { paidForBy, multiSig, context } = this;

if (paidForBy) {
Expand Down Expand Up @@ -960,16 +971,26 @@ export abstract class PolymeshTransactionBase<
}

// For MultiSig the fees come from the creator's primary key
if (multiSig) {
// Only use MultiSig payer when asProposal is true
if (multiSig && asProposal) {
const multiId = await multiSig.getPayer();

if (multiId) {
const { account } = await multiId.getPrimaryAccount();

return {
account,
type: PayingAccountType.MultiSigCreator,
};
try {
const { account } = await multiId.getPrimaryAccount();

return {
account,
type: PayingAccountType.MultiSigCreator,
};
} catch {
// If we can't get the primary account (e.g., it doesn't have an identity),
// fall back to using the MultiSig account directly
return {
type: PayingAccountType.Caller,
account: multiSig,
};
}
} else {
return {
type: PayingAccountType.Caller,
Expand Down Expand Up @@ -1012,4 +1033,32 @@ export abstract class PolymeshTransactionBase<

return tx;
}

/**
* @hidden
*
* Compose a transaction for fee calculation or payload generation, conditionally wrapping as a proposal
*
* @param asProposal - When `true` (default), wraps the transaction as a proposal if the signing account is a MultiSig signer.
* When `false`, returns the unwrapped transaction.
*/
protected composeTxForFees(
asProposal: boolean
): SubmittableExtrinsic<'promise', ISubmittableResult> {
// Get the base transaction without wrapping
const baseTx = this.getBaseTransaction();

if (asProposal) {
return this.wrapProposalIfNeeded(baseTx);
}

return baseTx;
}

/**
* @hidden
*
* Get the base transaction without any proposal wrapping
*/
protected abstract getBaseTransaction(): SubmittableExtrinsic<'promise', ISubmittableResult>;
}
11 changes: 8 additions & 3 deletions src/base/PolymeshTransactionBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export class PolymeshTransactionBatch<
* @hidden
*/
protected composeTx(): SubmittableExtrinsic<'promise', ISubmittableResult> {
const baseTx = this.getBaseTransaction();

return this.wrapProposalIfNeeded(baseTx);
}

// eslint-disable-next-line require-jsdoc
protected getBaseTransaction(): SubmittableExtrinsic<'promise', ISubmittableResult> {
const {
context: {
polymeshApi: {
Expand All @@ -94,11 +101,9 @@ export class PolymeshTransactionBatch<
},
} = this;

const tx = utility.batchAll(
return utility.batchAll(
this.transactionData.map(({ transaction, args }) => transaction(...args))
);

return this.wrapProposalIfNeeded(tx);
}

/**
Expand Down
Loading
Loading