Skip to content

Commit

Permalink
feat: 🎸 Support subsidized batches
Browse files Browse the repository at this point in the history
  • Loading branch information
prashantasdeveloper committed Nov 8, 2024
1 parent b55e637 commit 3541cd4
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 19 deletions.
17 changes: 15 additions & 2 deletions src/base/PolymeshTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ISubmittableResult } from '@polkadot/types/types';
import BigNumber from 'bignumber.js';

import { isMultiSigNoWrapTx } from '~/base/utils';
import { Context, PolymeshTransactionBase } from '~/internal';
import { TxTag, TxTags } from '~/types';
import { Context, PolymeshError, PolymeshTransactionBase } from '~/internal';
import { ErrorCode, TxTag, TxTags } from '~/types';
import { PolymeshTx, TransactionConstructionData, TransactionSpec } from '~/types/internal';
import { transactionToTxTag } from '~/utils/conversion';

Expand Down Expand Up @@ -133,4 +133,17 @@ export class PolymeshTransaction<

return context.supportsSubsidy({ tag });
}

// eslint-disable-next-line require-jsdoc
protected override assertTransactionSupportsSubsidy(): void {
if (!this.supportsSubsidy()) {
throw new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'This transaction cannot be run by a subsidized Account',
data: {
transaction: this.tag,
},
});
}
}
}
18 changes: 11 additions & 7 deletions src/base/PolymeshTransactionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,16 @@ export abstract class PolymeshTransactionBase<
*
* @note this depends on the type of transaction itself (e.g. `staking.bond` can't be subsidized, but `asset.createAsset` can)
*/
public abstract supportsSubsidy(): boolean;
public abstract supportsSubsidy(): void;

/**
* @hidden
*
* Asserts whether the transaction can be subsidized.
*
* @throws if transaction cannot be subsidized
*/
protected abstract assertTransactionSupportsSubsidy(): void;

/**
* @hidden
Expand Down Expand Up @@ -771,12 +780,7 @@ export abstract class PolymeshTransactionBase<

if (type === PayingAccountType.Subsidy) {
const { allowance } = payingAccountData;
if (!this.supportsSubsidy()) {
throw new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'This transaction cannot be run by a subsidized Account',
});
}
this.assertTransactionSupportsSubsidy();

if (allowance.lt(total)) {
throw new PolymeshError({
Expand Down
40 changes: 37 additions & 3 deletions src/base/PolymeshTransactionBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MapTxWithArgs,
TransactionConstructionData,
} from '~/types/internal';
import { MAX_BATCH_SIZE_SUPPORTING_SUBSIDY } from '~/utils/constants';
import { transactionToTxTag, u32ToBigNumber } from '~/utils/conversion';
import { filterEventRecords, mergeReceipts } from '~/utils/internal';

Expand Down Expand Up @@ -121,11 +122,44 @@ export class PolymeshTransactionBatch<
}

/**
* @note batches can't be subsidized. If the caller is subsidized, they should use `splitTransactions` and
* run each transaction separately
* @note batch can only be subsidized if -
* 1. Number of transactions in the batch are not more than 7
* 2. Every transaction in the batch can be subsidized
*/
public supportsSubsidy(): boolean {
return false;
return (
this.transactionData.length <= MAX_BATCH_SIZE_SUPPORTING_SUBSIDY &&
this.transactionData.every(({ tag }) => this.context.supportsSubsidy({ tag }))
);
}

/**
* @throws error if
* 1. Number of transactions in the batch are more than 7
* 2. Batch contains a transaction that cannot be subsidized
*/
protected override assertTransactionSupportsSubsidy(): void {
if (this.transactionData.length > MAX_BATCH_SIZE_SUPPORTING_SUBSIDY) {
throw new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: `Batch transactions can only be subsidized with a maximum of ${MAX_BATCH_SIZE_SUPPORTING_SUBSIDY} batched calls`,
data: {
currentBatchLength: this.transactionData.length,
},
});
}
const unsupportedTxs = this.transactionData.filter(
({ tag }) => !this.context.supportsSubsidy({ tag })
);
if (unsupportedTxs.length) {
throw new PolymeshError({
code: ErrorCode.UnmetPrerequisite,
message: 'Some of the transactions cannot be run by a subsidized Account',
data: {
transactions: unsupportedTxs,
},
});
}
}

/**
Expand Down
83 changes: 77 additions & 6 deletions src/base/__tests__/PolymeshTransactionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { createMockSigningPayload, MockTxStatus } from '~/testUtils/mocks/dataSo
import { Mocked } from '~/testUtils/types';
import { ErrorCode, MultiSig, PayingAccountType, TransactionStatus, TxTags } from '~/types';
import { tuple } from '~/types/utils';
import { DUMMY_ACCOUNT_ID } from '~/utils/constants';
import { DUMMY_ACCOUNT_ID, MAX_BATCH_SIZE_SUPPORTING_SUBSIDY } from '~/utils/constants';
import * as utilsConversionModule from '~/utils/conversion';
import * as utilsInternalModule from '~/utils/internal';

Expand Down Expand Up @@ -480,11 +480,11 @@ describe('Polymesh Transaction Base class', () => {
expect(tx.status).toBe(TransactionStatus.Rejected);
});

it('should throw an error if trying to run a transaction that cannot be subsidized with a subsidized Account', async () => {
const transaction = dsMockUtils.createTxMock('staking', 'bond', {
it('should throw an error if trying to run a transaction that cannot be subsidized', async () => {
const notSupportedTransaction = dsMockUtils.createTxMock('staking', 'bond', {
autoResolve: MockTxStatus.Succeeded,
});
const args = tuple('JUST_KIDDING');
const randomArgs = tuple('JUST_KIDDING');

context = dsMockUtils.getContextInstance({
subsidy: {
Expand All @@ -497,8 +497,8 @@ describe('Polymesh Transaction Base class', () => {
const tx = new PolymeshTransaction(
{
...txSpec,
transaction,
args,
transaction: notSupportedTransaction,
args: randomArgs,
resolver: undefined,
},
context
Expand All @@ -508,6 +508,77 @@ describe('Polymesh Transaction Base class', () => {
'This transaction cannot be run by a subsidized Account'
);
expect(tx.status).toBe(TransactionStatus.Failed);

dsMockUtils.createTxMock('utility', 'batchAll');

const transaction = dsMockUtils.createTxMock('asset', 'registerUniqueTicker');
const args = tuple('A_TICKER');

const transactions = [
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
];

const tooLongBatchTransaction = new PolymeshTransactionBatch(
{
...txSpec,
transactions,
resolver: undefined,
},
context
);

await expect(tooLongBatchTransaction.run()).rejects.toThrow(
`Batch transactions can only be subsidized with a maximum of ${MAX_BATCH_SIZE_SUPPORTING_SUBSIDY} batched calls`
);

const batchWithNotSupportedTransaction = new PolymeshTransactionBatch(
{
...txSpec,
transactions: [
{
transaction: notSupportedTransaction,
args: randomArgs,
},
],
resolver: undefined,
},
context
);

await expect(batchWithNotSupportedTransaction.run()).rejects.toThrow(
'Some of the transactions cannot be run by a subsidized Account'
);
});

it('should throw an error if the subsidy does not have enough allowance', async () => {
Expand Down
80 changes: 79 additions & 1 deletion src/base/__tests__/PolymeshTransactionBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe('Polymesh Transaction Batch class', () => {
});

describe('method: supportsSubsidy', () => {
it('should return false', () => {
it('should return true if all batch transaction can be subsidized', () => {
const transaction = dsMockUtils.createTxMock('asset', 'registerUniqueTicker');
const args = tuple('A_TICKER');

Expand All @@ -204,6 +204,84 @@ describe('Polymesh Transaction Batch class', () => {
},
];

context.supportsSubsidy.mockReturnValueOnce(true);

const tx = new PolymeshTransactionBatch(
{
...txSpec,
transactions,
resolver: undefined,
},
context
);

expect(tx.supportsSubsidy()).toBe(true);
});

it('should return false if batch has more than permissible max length', () => {
const transaction = dsMockUtils.createTxMock('asset', 'registerUniqueTicker');
const args = tuple('A_TICKER');

const transactions = [
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
{
transaction,
args,
},
];

const tx = new PolymeshTransactionBatch(
{
...txSpec,
transactions,
resolver: undefined,
},
context
);

expect(tx.supportsSubsidy()).toBe(false);
});

it('should return false if one or more transactions in the batch cannot be subsidized', () => {
const transaction = dsMockUtils.createTxMock('asset', 'registerUniqueTicker');
const args = tuple('A_TICKER');

const transactions = [
{
transaction,
args,
},
{
transaction: dsMockUtils.createTxMock('multisig', 'addAdmin'),
args: tuple('SOME_DID'),
},
];

context.supportsSubsidy.mockReturnValueOnce(true).mockReturnValueOnce(false);

const tx = new PolymeshTransactionBatch(
{
...txSpec,
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const MAX_TICKER_LENGTH = 12;
export const MAX_MODULE_LENGTH = 32;
export const MAX_MEMO_LENGTH = 32;
export const MAX_OFF_CHAIN_METADATA_LENGTH = 32;
export const MAX_BATCH_SIZE_SUPPORTING_SUBSIDY = 7;
/**
* Maximum amount of required mediators. See MESH-2156 to see if this is queryable instead
*/
Expand Down

0 comments on commit 3541cd4

Please sign in to comment.