Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: transaction registry #2859

Merged
merged 3 commits into from
Aug 2, 2019
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe("Connection", () => {
});

it("should remove transactions that have have data of a another transaction type", async () => {
const handlers: Handlers.TransactionHandler[] = Handlers.Registry.all();
const handlers: Handlers.TransactionHandler[] = await Handlers.Registry.getActivatedTransactions();
const transactions: Interfaces.ITransaction[] = TransactionFactory.transfer().build(handlers.length);

for (let i = 0; i < handlers.length; i++) {
Expand Down
65 changes: 50 additions & 15 deletions __tests__/unit/core-transactions/handler-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Crypto, Enums, Identities, Interfaces, Managers, Transactions, Utils }
import ByteBuffer from "bytebuffer";
import { Errors } from "../../../packages/core-transactions/src";
import { Registry, TransactionHandler } from "../../../packages/core-transactions/src/handlers";
import { TransactionHandlerConstructor } from "../../../packages/core-transactions/src/handlers/transaction";
import { TransferTransactionHandler } from "../../../packages/core-transactions/src/handlers/transfer";

import { testnet } from "../../../packages/crypto/src/networks";

Expand Down Expand Up @@ -61,10 +63,18 @@ class TestTransaction extends Transactions.Transaction {

// tslint:disable-next-line:max-classes-per-file
class TestTransactionHandler extends TransactionHandler {
public dependencies(): TransactionHandlerConstructor[] {
return [];
}

public async bootstrap(connection: Database.IConnection, walletManager: State.IWalletManager): Promise<void> {
return;
}

public async isActivated(): Promise<boolean> {
return true;
}

public getConstructor(): Transactions.TransactionConstructor {
return TestTransaction;
}
Expand All @@ -85,10 +95,10 @@ class TestTransactionHandler extends TransactionHandler {
}

// tslint:disable-next-line: no-empty
public applyToRecipient(transaction: Interfaces.ITransaction, walletManager: State.IWalletManager): void {}
public applyToRecipient(transaction: Interfaces.ITransaction, walletManager: State.IWalletManager): void { }

// tslint:disable-next-line: no-empty
public revertForRecipient(transaction: Interfaces.ITransaction, walletManager: State.IWalletManager): void {}
public revertForRecipient(transaction: Interfaces.ITransaction, walletManager: State.IWalletManager): void { }
}

beforeAll(() => {
Expand All @@ -98,10 +108,6 @@ beforeAll(() => {
Managers.configManager.setConfig(testnet);
});

afterEach(() => {
Registry.deregisterCustomTransactionHandler(TestTransactionHandler);
});

describe("Registry", () => {
it("should register core transaction types", () => {
expect(() => {
Expand All @@ -119,13 +125,14 @@ describe("Registry", () => {
});

it("should register a custom type", () => {
expect(() => Registry.registerCustomTransactionHandler(TestTransactionHandler)).not.toThrowError();
expect(() => Registry.registerTransactionHandler(TestTransactionHandler)).not.toThrowError();
expect(Registry.get(TEST_TRANSACTION_TYPE)).toBeInstanceOf(TestTransactionHandler);
expect(Transactions.TransactionTypeFactory.get(TEST_TRANSACTION_TYPE)).toBe(TestTransaction);
expect(() => Registry.deregisterTransactionHandler(TestTransactionHandler)).not.toThrowError();
});

it("should be able to instantiate a custom transaction", () => {
Registry.registerCustomTransactionHandler(TestTransactionHandler);
Registry.registerTransactionHandler(TestTransactionHandler);

const keys = Identities.Keys.fromPassphrase("secret");
const data: Interfaces.ITransactionData = {
Expand All @@ -151,15 +158,43 @@ describe("Registry", () => {
const deserialized = Transactions.TransactionFactory.fromBytes(bytes);
expect(deserialized.verified);
expect(deserialized.data.asset.test).toBe(256);

expect(() => Registry.deregisterTransactionHandler(TestTransactionHandler)).not.toThrowError();
});

it("should throw when trying to deregister a Core transaction type", () => {
expect(() => Registry.deregisterTransactionHandler(TransferTransactionHandler)).toThrowError();
});

it("should not be ok when registering the same type again", () => {
expect(() => Registry.registerCustomTransactionHandler(TestTransactionHandler)).not.toThrowError(
Errors.TransactionHandlerAlreadyRegisteredError,
);
it("should return all activated transactions", async () => {
let handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(Object.keys(Enums.TransactionTypes).length / 2);

Registry.registerTransactionHandler(TestTransactionHandler);

handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(Object.keys(Enums.TransactionTypes).length / 2 + 1);

jest.spyOn(Registry.get(TEST_TRANSACTION_TYPE), "isActivated").mockResolvedValueOnce(false);

handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(Object.keys(Enums.TransactionTypes).length / 2);

handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(Object.keys(Enums.TransactionTypes).length / 2 + 1);

Registry.deregisterTransactionHandler(TestTransactionHandler);
});

it("should only return V1 transactions when AIP11 is off", async () => {
Managers.configManager.getMilestone().aip11 = false;

let handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(4);

Managers.configManager.getMilestone().aip11 = true;

expect(() => Registry.registerCustomTransactionHandler(TestTransactionHandler)).toThrowError(
Errors.TransactionHandlerAlreadyRegisteredError,
);
handlers = await Registry.getActivatedTransactions();
expect(handlers).toHaveLength(Object.keys(Enums.TransactionTypes).length / 2);
});
});
23 changes: 8 additions & 15 deletions packages/core-database-postgres/src/state-builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { app } from "@arkecosystem/core-container";
import { Database, EventEmitter, Logger, State } from "@arkecosystem/core-interfaces";
import { Handlers } from "@arkecosystem/core-transactions";
import { Interfaces, Managers, Utils } from "@arkecosystem/crypto";
import { Interfaces, Utils } from "@arkecosystem/crypto";

export class StateBuilder {
private readonly logger: Logger.ILogger = app.resolvePlugin<Logger.ILogger>("logger");
Expand All @@ -13,28 +13,21 @@ export class StateBuilder {
) {}

public async run(): Promise<void> {
const transactionHandlers: Handlers.TransactionHandler[] = Handlers.Registry.all();
let steps = transactionHandlers.length + 2;

// FIXME: skip state generation of new tx types unless we are on testnet (until develop is on 2.6)
const aip11 =
Managers.configManager.getMilestone().aip11 &&
["testnet", "unitnet"].includes(Managers.configManager.get("network.name"));
if (!aip11) {
steps -= 5;
}
const transactionHandlers: Handlers.TransactionHandler[] = await Handlers.Registry.getActivatedTransactions();
const steps = transactionHandlers.length + 2;

this.logger.info(`State Generation - Step 1 of ${steps}: Block Rewards`);
await this.buildBlockRewards();

this.logger.info(`State Generation - Step 2 of ${steps}: Fees & Nonces`);
await this.buildSentTransactions();

for (let i = 0; i < (aip11 ? transactionHandlers.length : 4); i++) {
const capitalize = (key: string) => key[0].toUpperCase() + key.slice(1);
for (let i = 0; i < transactionHandlers.length; i++) {
const transactionHandler = transactionHandlers[i];
const transactionName = transactionHandler.constructor.name.replace("TransactionHandler", "");

this.logger.info(`State Generation - Step ${3 + i} of ${steps}: ${transactionName}`);
this.logger.info(
`State Generation - Step ${3 + i} of ${steps}: ${capitalize(transactionHandler.getConstructor().key)}`,
);

await transactionHandler.bootstrap(this.connection, this.walletManager);
}
Expand Down
6 changes: 0 additions & 6 deletions packages/core-transactions/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ export class NotImplementedError extends TransactionError {
}
}

export class TransactionHandlerAlreadyRegisteredError extends TransactionError {
constructor(type: number) {
super(`Transaction service for type ${type} is already registered.`);
}
}

export class InvalidTransactionTypeError extends TransactionError {
constructor(type: number) {
super(`Transaction type ${type} does not exist.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
WalletNotADelegateError,
WalletUsernameAlreadyRegisteredError,
} from "../errors";
import { TransactionHandler } from "./transaction";
import { TransactionHandler, TransactionHandlerConstructor } from "./transaction";

const { TransactionTypes } = Enums;

Expand All @@ -16,6 +16,10 @@ export class DelegateRegistrationTransactionHandler extends TransactionHandler {
return Transactions.DelegateRegistrationTransaction;
}

public dependencies(): ReadonlyArray<TransactionHandlerConstructor> {
return [];
}

public async bootstrap(connection: Database.IConnection, walletManager: State.IWalletManager): Promise<void> {
const transactions = await connection.transactionsRepository.getAssetsByType(this.getConstructor().type);
const forgedBlocks = await connection.blocksRepository.getDelegatesForgedBlocks();
Expand Down Expand Up @@ -57,6 +61,10 @@ export class DelegateRegistrationTransactionHandler extends TransactionHandler {
walletManager.buildDelegateRanking();
}

public async isActivated(): Promise<boolean> {
return true;
}

public throwIfCannotBeApplied(
transaction: Interfaces.ITransaction,
wallet: State.IWallet,
Expand Down
13 changes: 11 additions & 2 deletions packages/core-transactions/src/handlers/delegate-resignation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { ApplicationEvents } from "@arkecosystem/core-event-emitter";
import { Database, EventEmitter, State, TransactionPool } from "@arkecosystem/core-interfaces";
import { Interfaces, Transactions } from "@arkecosystem/crypto";
import { Interfaces, Managers, Transactions } from "@arkecosystem/crypto";
import { WalletAlreadyResignedError, WalletNotADelegateError } from "../errors";
import { TransactionHandler } from "./transaction";
import { DelegateRegistrationTransactionHandler } from "./delegate-registration";
import { TransactionHandler, TransactionHandlerConstructor } from "./transaction";

export class DelegateResignationTransactionHandler extends TransactionHandler {
public getConstructor(): Transactions.TransactionConstructor {
return Transactions.DelegateResignationTransaction;
}

public dependencies(): ReadonlyArray<TransactionHandlerConstructor> {
return [DelegateRegistrationTransactionHandler];
}

public async bootstrap(connection: Database.IConnection, walletManager: State.IWalletManager): Promise<void> {
const transactions = await connection.transactionsRepository.getAssetsByType(this.getConstructor().type);

Expand All @@ -17,6 +22,10 @@ export class DelegateResignationTransactionHandler extends TransactionHandler {
}
}

public async isActivated(): Promise<boolean> {
return !!Managers.configManager.getMilestone().aip11;
}

public throwIfCannotBeApplied(
transaction: Interfaces.ITransaction,
wallet: State.IWallet,
Expand Down
87 changes: 44 additions & 43 deletions packages/core-transactions/src/handlers/handler-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,83 +12,84 @@ import { SecondSignatureTransactionHandler } from "./second-signature";
import { TransferTransactionHandler } from "./transfer";
import { VoteTransactionHandler } from "./vote";

import { InvalidTransactionTypeError, TransactionHandlerAlreadyRegisteredError } from "../errors";
import { TransactionHandler } from "./transaction";

export type TransactionHandlerConstructor = new () => TransactionHandler;
import { InvalidTransactionTypeError } from "../errors";
import { TransactionHandler, TransactionHandlerConstructor } from "./transaction";

export class TransactionHandlerRegistry {
private readonly coreTransactionHandlers: Map<Enums.TransactionTypes, TransactionHandler> = new Map<
private readonly registeredTransactionHandlers: Map<number, TransactionHandler> = new Map<
Enums.TransactionTypes,
TransactionHandler
>();
private readonly customTransactionHandlers: Map<number, TransactionHandler> = new Map<number, TransactionHandler>();

constructor() {
this.registerCoreTransactionHandler(TransferTransactionHandler);
this.registerCoreTransactionHandler(SecondSignatureTransactionHandler);
this.registerCoreTransactionHandler(DelegateRegistrationTransactionHandler);
this.registerCoreTransactionHandler(VoteTransactionHandler);
this.registerCoreTransactionHandler(MultiSignatureTransactionHandler);
this.registerCoreTransactionHandler(IpfsTransactionHandler);
this.registerCoreTransactionHandler(MultiPaymentTransactionHandler);
this.registerCoreTransactionHandler(DelegateResignationTransactionHandler);
this.registerCoreTransactionHandler(HtlcLockTransactionHandler);
this.registerCoreTransactionHandler(HtlcClaimTransactionHandler);
this.registerCoreTransactionHandler(HtlcRefundTransactionHandler);
this.registerTransactionHandler(TransferTransactionHandler);
this.registerTransactionHandler(SecondSignatureTransactionHandler);
this.registerTransactionHandler(DelegateRegistrationTransactionHandler);
this.registerTransactionHandler(VoteTransactionHandler);
this.registerTransactionHandler(MultiSignatureTransactionHandler);
this.registerTransactionHandler(IpfsTransactionHandler);
this.registerTransactionHandler(MultiPaymentTransactionHandler);
this.registerTransactionHandler(DelegateResignationTransactionHandler);
this.registerTransactionHandler(HtlcLockTransactionHandler);
this.registerTransactionHandler(HtlcClaimTransactionHandler);
this.registerTransactionHandler(HtlcRefundTransactionHandler);
}

public get(type: Enums.TransactionTypes | number): TransactionHandler {
if (this.coreTransactionHandlers.has(type)) {
return this.coreTransactionHandlers.get(type);
}

if (this.customTransactionHandlers.has(type)) {
return this.customTransactionHandlers.get(type);
if (this.registeredTransactionHandlers.has(type)) {
return this.registeredTransactionHandlers.get(type);
}

throw new InvalidTransactionTypeError(type);
}

public all(): TransactionHandler[] {
return [...this.coreTransactionHandlers.values(), ...this.customTransactionHandlers.values()];
public async getActivatedTransactions(): Promise<TransactionHandler[]> {
const activatedTransactions: TransactionHandler[] = [];

for (const handler of this.registeredTransactionHandlers.values()) {
if (await handler.isActivated()) {
activatedTransactions.push(handler);
}
}

return activatedTransactions;
}

public registerCustomTransactionHandler(constructor: TransactionHandlerConstructor): void {
public registerTransactionHandler(constructor: TransactionHandlerConstructor) {
const service: TransactionHandler = new constructor();
const transactionConstructor = service.getConstructor();
const { type } = transactionConstructor;

if (this.customTransactionHandlers.has(type)) {
throw new TransactionHandlerAlreadyRegisteredError(type);
for (const dependency of service.dependencies()) {
this.registerTransactionHandler(dependency);
}

if (this.registeredTransactionHandlers.has(type)) {
return;
}

Transactions.TransactionRegistry.registerCustomType(transactionConstructor);
if (!(type in Enums.TransactionTypes)) {
Transactions.TransactionRegistry.registerTransactionType(transactionConstructor);
}

this.customTransactionHandlers.set(type, service);
this.registeredTransactionHandlers.set(type, service);
}

public deregisterCustomTransactionHandler(constructor: TransactionHandlerConstructor): void {
public deregisterTransactionHandler(constructor: TransactionHandlerConstructor): void {
const service: TransactionHandler = new constructor();
const transactionConstructor = service.getConstructor();
const { type } = transactionConstructor;

if (this.customTransactionHandlers.has(type)) {
Transactions.TransactionRegistry.deregisterCustomType(type);
this.customTransactionHandlers.delete(type);
if (type in Enums.TransactionTypes) {
throw new Error("Cannot deregister Core transaction.");
}
}

private registerCoreTransactionHandler(constructor: TransactionHandlerConstructor) {
const service: TransactionHandler = new constructor();
const transactionConstructor = service.getConstructor();
const { type } = transactionConstructor;

if (this.coreTransactionHandlers.has(type)) {
throw new TransactionHandlerAlreadyRegisteredError(type);
if (!this.registeredTransactionHandlers.has(type)) {
throw new InvalidTransactionTypeError(type);
}

this.coreTransactionHandlers.set(type, service);
Transactions.TransactionRegistry.deregisterTransactionType(type);
this.registeredTransactionHandlers.delete(type);
}
}

Expand Down
Loading