Skip to content

Commit

Permalink
chore(core-magistrate-transactions): implement throwIfCannotEnterPool…
Browse files Browse the repository at this point in the history
… on Entity (#4368)
  • Loading branch information
sebastijankuzner authored Apr 11, 2021
1 parent 04a1bba commit 1d5a5c5
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 93 deletions.
245 changes: 153 additions & 92 deletions __tests__/unit/core-magistrate-transactions/handlers/entity.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import "jest-extended";

import { Container, Utils } from "@arkecosystem/core-kernel";
import { Enums } from "@arkecosystem/core-magistrate-crypto";
import { EntityBuilder } from "@arkecosystem/core-magistrate-crypto/src/builders";
import { EntityType, EntityAction } from "@arkecosystem/core-magistrate-crypto/src/enums";
import { EntityTransaction } from "@arkecosystem/core-magistrate-crypto/src/transactions";
import { Container, Utils } from "@packages/core-kernel";
import { PoolError } from "@packages/core-kernel/dist/contracts/transaction-pool";
import { Enums } from "@packages/core-magistrate-crypto";
import { EntityBuilder } from "@packages/core-magistrate-crypto/src/builders";
import { EntityAction, EntityType } from "@packages/core-magistrate-crypto/src/enums";
import { EntityTransaction } from "@packages/core-magistrate-crypto/src/transactions";
import {
EntityAlreadyRegisteredError,
EntityAlreadyResignedError,
EntityNameAlreadyRegisteredError,
EntityNameDoesNotMatchDelegateError,
EntityNotRegisteredError,
EntitySenderIsNotDelegateError,
EntityWrongSubTypeError,
EntityWrongTypeError,
StaticFeeMismatchError,
EntitySenderIsNotDelegateError,
EntityNameDoesNotMatchDelegateError,
EntityNameAlreadyRegisteredError,
} from "@arkecosystem/core-magistrate-transactions/src/errors";
import { EntityTransactionHandler } from "@arkecosystem/core-magistrate-transactions/src/handlers/entity";
import { Utils as CryptoUtils } from "@arkecosystem/crypto";
import { Managers, Transactions } from "@arkecosystem/crypto";

import { validRegisters } from "./__fixtures__/entity/register";
import { validResigns } from "./__fixtures__/entity/resign";
import { validUpdates } from "./__fixtures__/entity/update";
} from "@packages/core-magistrate-transactions/src/errors";
import { EntityTransactionHandler } from "@packages/core-magistrate-transactions/src/handlers/entity";
import { Utils as CryptoUtils } from "@packages/crypto";
import { Managers, Transactions } from "@packages/crypto";
import { cloneDeep } from "lodash";

import { validRegisters, validResigns, validUpdates } from "./__fixtures__/entity";
import { walletRepository } from "./__mocks__/wallet-repository";

// mocking the abstract TransactionHandler class
Expand Down Expand Up @@ -52,31 +52,39 @@ describe("Entity handler", () => {
streamByCriteria: jest.fn(),
};

const poolQuery = {
getAll: jest.fn(),
whereKind: jest.fn(),
wherePredicate: jest.fn(),
has: jest.fn().mockReturnValue(false),
};

poolQuery.getAll.mockReturnValue(poolQuery);
poolQuery.whereKind.mockReturnValue(poolQuery);
poolQuery.wherePredicate.mockReturnValue(poolQuery);

beforeAll(() => {
container.unbindAll();
container.bind(Container.Identifiers.TransactionHistoryService).toConstantValue(transactionHistoryService);
container.bind(Container.Identifiers.TransactionPoolQuery).toConstantValue(poolQuery);
});

let wallet, walletAttributes;
beforeEach(() => {
walletAttributes = {};
wallet = {
getAttribute: jest
.fn()
.mockImplementation((attribute, defaultValue) => {
const splitAttribute = attribute.split(".");
return splitAttribute.length === 1
? walletAttributes[splitAttribute[0]] || defaultValue
: (walletAttributes[splitAttribute[0]] || {})[splitAttribute[1]] || defaultValue;
}),
hasAttribute: jest
.fn()
.mockImplementation((attribute) => {
const splitAttribute = attribute.split(".");
return splitAttribute.length === 1
? !!walletAttributes[splitAttribute[0]]
: !!walletAttributes[splitAttribute[0]] && !!walletAttributes[splitAttribute[0]][splitAttribute[1]];
}),
getAttribute: jest.fn().mockImplementation((attribute, defaultValue) => {
const splitAttribute = attribute.split(".");
return splitAttribute.length === 1
? walletAttributes[splitAttribute[0]] || defaultValue
: (walletAttributes[splitAttribute[0]] || {})[splitAttribute[1]] || defaultValue;
}),
hasAttribute: jest.fn().mockImplementation((attribute) => {
const splitAttribute = attribute.split(".");
return splitAttribute.length === 1
? !!walletAttributes[splitAttribute[0]]
: !!walletAttributes[splitAttribute[0]] && !!walletAttributes[splitAttribute[0]][splitAttribute[1]];
}),
setAttribute: jest.fn().mockImplementation((attribute, value) => (walletAttributes[attribute] = value)),
forgetAttribute: jest.fn().mockImplementation((attribute) => delete walletAttributes[attribute]),
};
Expand All @@ -86,6 +94,10 @@ describe("Entity handler", () => {
entityHandler = container.resolve(EntityTransactionHandler);
});

afterEach(() => {
jest.clearAllMocks();
});

const registerFee = "5000000000";
const updateAndResignFee = "500000000";

Expand Down Expand Up @@ -121,15 +133,14 @@ describe("Entity handler", () => {
});

describe("dynamicFee", () => {
const registerTx = (new EntityBuilder()).asset(validRegisters[0]).sign("passphrase").build();
const updateTx = (new EntityBuilder()).asset(validUpdates[0]).sign("passphrase").build();
const resignTx = (new EntityBuilder()).asset(validResigns[0]).sign("passphrase").build();
const registerTx = new EntityBuilder().asset(validRegisters[0]).sign("passphrase").build();
const updateTx = new EntityBuilder().asset(validUpdates[0]).sign("passphrase").build();
const resignTx = new EntityBuilder().asset(validResigns[0]).sign("passphrase").build();
it.each([
[registerTx, registerFee],
[updateTx, updateAndResignFee],
[resignTx, updateAndResignFee],
])("should return correct static fee", async (tx, fee) => {

entityHandler = container.resolve(EntityTransactionHandler);
// @ts-ignore
const result = await entityHandler.dynamicFee({ transaction: tx });
Expand Down Expand Up @@ -169,6 +180,54 @@ describe("Entity handler", () => {
});
});

describe("throwIfCannotEnterPool", () => {
let transaction: EntityTransaction;
let entityHandler: EntityTransactionHandler;

beforeEach(() => {
const builder = new EntityBuilder();
transaction = builder.asset(validRegisters[0]).sign("passphrase").build() as EntityTransaction;

entityHandler = container.resolve(EntityTransactionHandler);
});

it("should resolve", async () => {
await expect(entityHandler.throwIfCannotEnterPool(transaction)).toResolve();
expect(poolQuery.getAll).toHaveBeenCalled();
});

it("should resolve if transaction action is update or resign", async () => {
transaction.data.asset!.action = Enums.EntityAction.Update;

await expect(entityHandler.throwIfCannotEnterPool(transaction)).toResolve();
expect(poolQuery.getAll).not.toHaveBeenCalled();

transaction.data.asset!.action = Enums.EntityAction.Resign;

await expect(entityHandler.throwIfCannotEnterPool(transaction)).toResolve();
expect(poolQuery.getAll).not.toHaveBeenCalled();
});

it("should throw if transaction with same type and name is already in the pool", async () => {
poolQuery.has.mockReturnValue(true);

await expect(entityHandler.throwIfCannotEnterPool(transaction)).rejects.toBeInstanceOf(PoolError);

expect(poolQuery.wherePredicate).toHaveBeenCalledTimes(2);

const transactionClone = cloneDeep(transaction);
const wherePredicateCallback1 = poolQuery.wherePredicate.mock.calls[0][0];
const wherePredicateCallback2 = poolQuery.wherePredicate.mock.calls[1][0];

expect(wherePredicateCallback1(transactionClone)).toBeTrue();
expect(wherePredicateCallback2(transactionClone)).toBeTrue();

delete transactionClone.data.asset;
expect(wherePredicateCallback1(transactionClone)).toBeFalse();
expect(wherePredicateCallback2(transactionClone)).toBeFalse();
});
});

describe("throwIfCannotBeApplied", () => {
let transaction: EntityTransaction;
let entityHandler: EntityTransactionHandler;
Expand Down Expand Up @@ -338,47 +397,55 @@ describe("Entity handler", () => {
);
});

it.each([validRegisters])("should throw when entity name is already registered for same type", async (asset) => {
const builder = new EntityBuilder();
const transaction = builder.asset(asset).sign("passphrase").build();

const walletSameEntityName = {
hasAttribute: () => true,
getAttribute: () => ({
"7950c6a0d096eeb4883237feec12b9f37f36ab9343ff3640904befc75ce32ec2": {
type: asset.type,
subType: (asset.subType + 1) % 255, // different subType but still in the range [0, 255]
data: asset.data,
}}),
}
//@ts-ignore
jest.spyOn(walletRepository, "getIndex").mockReturnValueOnce([walletSameEntityName]);

entityHandler = container.resolve(EntityTransactionHandler);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).rejects.toBeInstanceOf(
EntityNameAlreadyRegisteredError,
);
});

it.each([validRegisters])("should not throw when entity name is registered for a different type", async (asset) => {
const builder = new EntityBuilder();
const transaction = builder.asset(asset).sign("passphrase").build();
it.each([validRegisters])(
"should throw when entity name is already registered for same type",
async (asset) => {
const builder = new EntityBuilder();
const transaction = builder.asset(asset).sign("passphrase").build();

const walletSameEntityName = {
hasAttribute: () => true,
getAttribute: () => ({
"7950c6a0d096eeb4883237feec12b9f37f36ab9343ff3640904befc75ce32ec2": {
type: asset.type,
subType: (asset.subType + 1) % 255, // different subType but still in the range [0, 255]
data: asset.data,
},
}),
};
//@ts-ignore
jest.spyOn(walletRepository, "getIndex").mockReturnValueOnce([walletSameEntityName]);

entityHandler = container.resolve(EntityTransactionHandler);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).rejects.toBeInstanceOf(
EntityNameAlreadyRegisteredError,
);
},
);

const walletSameEntityName = {
hasAttribute: () => true,
getAttribute: () => ({
"7950c6a0d096eeb4883237feec12b9f37f36ab9343ff3640904befc75ce32ec2": {
type: (asset.type + 1) % 255, // different type but still in the range [0, 255]
subType: asset.subType,
data: asset.data,
}}),
}
//@ts-ignore
jest.spyOn(walletRepository, "getIndex").mockReturnValueOnce([walletSameEntityName]);
it.each([validRegisters])(
"should not throw when entity name is registered for a different type",
async (asset) => {
const builder = new EntityBuilder();
const transaction = builder.asset(asset).sign("passphrase").build();

const walletSameEntityName = {
hasAttribute: () => true,
getAttribute: () => ({
"7950c6a0d096eeb4883237feec12b9f37f36ab9343ff3640904befc75ce32ec2": {
type: (asset.type + 1) % 255, // different type but still in the range [0, 255]
subType: asset.subType,
data: asset.data,
},
}),
};
//@ts-ignore
jest.spyOn(walletRepository, "getIndex").mockReturnValueOnce([walletSameEntityName]);

entityHandler = container.resolve(EntityTransactionHandler);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).toResolve();
});
entityHandler = container.resolve(EntityTransactionHandler);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).toResolve();
},
);

describe("Entity delegate", () => {
const entityId = "533384534cd561fc17f72be0bb57bf39961954ba0741f53c08e3f463ef19118c";
Expand All @@ -394,7 +461,7 @@ describe("Entity handler", () => {
asset.data = { ipfsData: "Qmbw6QmF6tuZpyV6WyEsTmExkEG3rW4khbttQidPfbpmNZ" };
}

return (new EntityBuilder())
return new EntityBuilder()
.asset(asset)
.fee(action === EntityAction.Register ? registerFee : updateAndResignFee)
.sign("passphrase")
Expand All @@ -404,9 +471,9 @@ describe("Entity handler", () => {
it("should throw when the sender wallet is not a delegate", async () => {
const transaction = createEntityDelegateTx("anyname");

await expect(
entityHandler.throwIfCannotBeApplied(transaction, wallet),
).rejects.toBeInstanceOf(EntitySenderIsNotDelegateError);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).rejects.toBeInstanceOf(
EntitySenderIsNotDelegateError,
);
});

it("should throw when the sender delegate name does not match the entity name", async () => {
Expand All @@ -415,9 +482,9 @@ describe("Entity handler", () => {

walletAttributes.delegate = { username: username + "s" };

await expect(
entityHandler.throwIfCannotBeApplied(transaction, wallet),
).rejects.toBeInstanceOf(EntityNameDoesNotMatchDelegateError);
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).rejects.toBeInstanceOf(
EntityNameDoesNotMatchDelegateError,
);
});

it("should not throw on update or resign even when delegate does not match", async () => {
Expand All @@ -431,12 +498,8 @@ describe("Entity handler", () => {
[entityId]: { name: "somename", type, subType, data: {} },
};

await expect(
entityHandler.throwIfCannotBeApplied(transactionResign, wallet),
).toResolve();
await expect(
entityHandler.throwIfCannotBeApplied(transactionUpdate, wallet),
).toResolve();
await expect(entityHandler.throwIfCannotBeApplied(transactionResign, wallet)).toResolve();
await expect(entityHandler.throwIfCannotBeApplied(transactionUpdate, wallet)).toResolve();
});

it("should not throw otherwise", async () => {
Expand All @@ -445,11 +508,9 @@ describe("Entity handler", () => {

walletAttributes.delegate = { username };

await expect(
entityHandler.throwIfCannotBeApplied(transaction, wallet),
).toResolve();
await expect(entityHandler.throwIfCannotBeApplied(transaction, wallet)).toResolve();
});
})
});
});
});

Expand Down
28 changes: 27 additions & 1 deletion packages/core-magistrate-transactions/src/handlers/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import { MagistrateIndex } from "../wallet-indexes";

@Container.injectable()
export class EntityTransactionHandler extends Handlers.TransactionHandler {
@Container.inject(Container.Identifiers.TransactionPoolQuery)
private readonly poolQuery!: Contracts.TransactionPool.Query;

@Container.inject(Container.Identifiers.TransactionHistoryService)
protected readonly transactionHistoryService!: Contracts.Shared.TransactionHistoryService;
private readonly transactionHistoryService!: Contracts.Shared.TransactionHistoryService;

public dependencies(): ReadonlyArray<Handlers.TransactionHandlerConstructor> {
return [];
Expand Down Expand Up @@ -61,6 +64,29 @@ export class EntityTransactionHandler extends Handlers.TransactionHandler {
}
}

public async throwIfCannotEnterPool(transaction: Interfaces.ITransaction): Promise<void> {
KernelUtils.assert.defined<object>(transaction.data.asset);

if (transaction.data.asset.action === Enums.EntityAction.Register) {
KernelUtils.assert.defined<object>(transaction.data.asset.data.name);
const name = transaction.data.asset.data.name;

const hasName: boolean = this.poolQuery
.getAll()
.whereKind(transaction)
.wherePredicate((t) => t.data.asset?.type === transaction.data.asset!.type)
.wherePredicate((t) => t.data.asset?.data.name.toLowerCase() === name.toLowerCase())
.has();

if (hasName) {
throw new Contracts.TransactionPool.PoolError(
`Entity registration for "${name}" already in the pool`,
"ERR_PENDING",
);
}
}
}

public async throwIfCannotBeApplied(
transaction: CryptoInterfaces.ITransaction,
wallet: Contracts.State.Wallet,
Expand Down

0 comments on commit 1d5a5c5

Please sign in to comment.