diff --git a/__tests__/integration/core-transaction-pool/guard.test.ts b/__tests__/integration/core-transaction-pool/guard.test.ts index 107c72da7e..d0bc811f0b 100644 --- a/__tests__/integration/core-transaction-pool/guard.test.ts +++ b/__tests__/integration/core-transaction-pool/guard.test.ts @@ -1,5 +1,5 @@ import { Container } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { crypto, ITransactionData, models } from "@arkecosystem/crypto"; import bip39 from "bip39"; import "jest-extended"; @@ -151,8 +151,8 @@ describe("Transaction Guard", () => { expect(guard.errors).toEqual({}); // simulate forged transaction - const transactionService = TransactionServiceRegistry.get(transfers[0].type); - transactionService.applyToRecipient(transfers[0], newWallet); + const transactionHandler = TransactionHandlerRegistry.get(transfers[0].type); + transactionHandler.applyToRecipient(transfers[0], newWallet); expect(+delegateWallet.balance).toBe(+delegate1.balance - amount1 - fee); expect(+newWallet.balance).toBe(amount1); @@ -192,8 +192,8 @@ describe("Transaction Guard", () => { await guard.validate(transfers.map(tx => tx.data)); // simulate forged transaction - const transactionService = TransactionServiceRegistry.get(transfers[0].type); - transactionService.applyToRecipient(transfers[0], newWallet); + const transactionHandler = TransactionHandlerRegistry.get(transfers[0].type); + transactionHandler.applyToRecipient(transfers[0], newWallet); expect(guard.errors).toEqual({}); expect(+newWallet.balance).toBe(amount1); @@ -230,8 +230,8 @@ describe("Transaction Guard", () => { await guard.validate(transfers1.map(tx => tx.data)); // simulate forged transaction - const transactionService = TransactionServiceRegistry.get(transfers1[0].type); - transactionService.applyToRecipient(transfers1[0], newWallet); + const transactionHandler = TransactionHandlerRegistry.get(transfers1[0].type); + transactionHandler.applyToRecipient(transfers1[0], newWallet); expect(+delegateWallet.balance).toBe(+delegate3.balance - amount1 - fee); expect(+newWallet.balance).toBe(amount1); @@ -242,7 +242,7 @@ describe("Transaction Guard", () => { await guard.validate(transfers2.map(tx => tx.data)); // simulate forged transaction - transactionService.applyToRecipient(transfers2[0], delegateWallet); + transactionHandler.applyToRecipient(transfers2[0], delegateWallet); expect(+newWallet.balance).toBe(amount1 - amount2 - fee); diff --git a/__tests__/integration/core-transaction-pool/pool-wallet-manager.test.ts b/__tests__/integration/core-transaction-pool/pool-wallet-manager.test.ts index a1a0879eef..9a4d394348 100644 --- a/__tests__/integration/core-transaction-pool/pool-wallet-manager.test.ts +++ b/__tests__/integration/core-transaction-pool/pool-wallet-manager.test.ts @@ -1,5 +1,5 @@ import { Blockchain, Container, Database } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { crypto, models } from "@arkecosystem/crypto"; import bip39 from "bip39"; import { generators } from "../../utils"; @@ -65,8 +65,8 @@ describe("applyPoolTransactionToSender", () => { const amount1 = 123 * 10 ** 8; const transfer = generateTransfers("unitnet", delegate0.secret, newAddress, amount1, 1)[0]; - const transactionService = TransactionServiceRegistry.get(transfer.type); - transactionService.applyToSender(transfer, delegateWallet); + const transactionHandler = TransactionHandlerRegistry.get(transfer.type); + transactionHandler.applyToSender(transfer, delegateWallet); expect(+delegateWallet.balance).toBe(+delegate0.balance - amount1 - 0.1 * 10 ** 8); expect(newWallet.balance.isZero()).toBeTrue(); @@ -87,8 +87,8 @@ describe("applyPoolTransactionToSender", () => { const fee = 10; const transfer = generateTransfers("unitnet", delegate0.secret, newAddress, amount1, 1, false, fee)[0]; - const transactionService = TransactionServiceRegistry.get(transfer.type); - transactionService.applyToSender(transfer, delegateWallet); + const transactionHandler = TransactionHandlerRegistry.get(transfer.type); + transactionHandler.applyToSender(transfer, delegateWallet); expect(+delegateWallet.balance).toBe(+delegate0.balance - amount1 - fee); expect(newWallet.balance.isZero()).toBeTrue(); @@ -123,7 +123,7 @@ describe("applyPoolTransactionToSender", () => { transfers.forEach(t => { const transfer = generateTransfers("unitnet", t.from.passphrase, t.to.address, t.amount, 1)[0]; - const transactionService = TransactionServiceRegistry.get(transfer.type); + const transactionHandler = TransactionHandlerRegistry.get(transfer.type); // This is normally refused because it's a cold wallet, but since we want // to test if chained transfers are refused, pretent it is not a cold wallet. @@ -134,7 +134,7 @@ describe("applyPoolTransactionToSender", () => { const errors = []; if (poolWalletManager.canApply(transfer, errors)) { const senderWallet = poolWalletManager.findByPublicKey(transfer.data.senderPublicKey); - transactionService.applyToSender(transfer, senderWallet); + transactionHandler.applyToSender(transfer, senderWallet); expect(t.from).toBe(delegate); } else { diff --git a/__tests__/unit/core-database/database-service.test.ts b/__tests__/unit/core-database/database-service.test.ts index d82fdf0000..ddcb7f2f11 100644 --- a/__tests__/unit/core-database/database-service.test.ts +++ b/__tests__/unit/core-database/database-service.test.ts @@ -3,7 +3,7 @@ import "./mocks/core-container"; import { app } from "@arkecosystem/core-container"; import { Database, EventEmitter } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { Address, Bignum, constants, models, Transaction, transactionBuilder } from "@arkecosystem/crypto"; import { Wallet, WalletManager } from "../../../packages/core-database/src"; import { DatabaseService } from "../../../packages/core-database/src/database-service"; @@ -206,9 +206,9 @@ describe("Database Service", () => { const delegatesRound2 = walletManager.loadActiveDelegateList(51, initialHeight); // Prepare sender wallet - const transactionService = TransactionServiceRegistry.get(TransactionTypes.Transfer); - const originalApply = transactionService.canBeApplied; - transactionService.canBeApplied = jest.fn(() => true); + const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer); + const originalApply = transactionHandler.canBeApplied; + transactionHandler.canBeApplied = jest.fn(() => true); const sender = new Wallet(keys.address); sender.publicKey = keys.publicKey; @@ -278,7 +278,7 @@ describe("Database Service", () => { expect(restoredDelegatesRound2[i].publicKey).toBe(delegatesRound2[i].publicKey); } - transactionService.canBeApplied = originalApply; + transactionHandler.canBeApplied = originalApply; }); }); }); diff --git a/__tests__/unit/core-transaction-pool/connection.test.ts b/__tests__/unit/core-transaction-pool/connection.test.ts index ce291333ea..cad1149ecb 100644 --- a/__tests__/unit/core-transaction-pool/connection.test.ts +++ b/__tests__/unit/core-transaction-pool/connection.test.ts @@ -2,7 +2,7 @@ import "./mocks/core-container"; import { Wallet } from "@arkecosystem/core-database"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { bignumify } from "@arkecosystem/core-utils"; import { Bignum, constants, models, slots, Transaction } from "@arkecosystem/crypto"; import { dato } from "@faustbrian/dato"; @@ -555,8 +555,8 @@ describe("Connection", () => { describe("acceptChainedBlock", () => { let mockWallet; beforeEach(() => { - const transactionService = TransactionServiceRegistry.get(TransactionTypes.Transfer); - jest.spyOn(transactionService, "canBeApplied").mockReturnValue(true); + const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer); + jest.spyOn(transactionHandler, "canBeApplied").mockReturnValue(true); mockWallet = new Wallet(block2.transactions[0].recipientId); mockWallet.balance = new Bignum(1e12); @@ -595,8 +595,8 @@ describe("Connection", () => { }); it("should purge and block sender if canApply() failed for a transaction in the chained block", () => { - const transactionService = TransactionServiceRegistry.get(TransactionTypes.Transfer); - jest.spyOn(transactionService, "canBeApplied").mockImplementation(() => { + const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer); + jest.spyOn(transactionHandler, "canBeApplied").mockImplementation(() => { throw new Error("test error"); }); const purgeByPublicKey = jest.spyOn(connection, "purgeByPublicKey"); @@ -623,9 +623,9 @@ describe("Connection", () => { let applyToSender; const findByPublicKeyWallet = new Wallet("thisIsAnAddressIMadeUpJustLikeThis"); beforeEach(() => { - const transactionService = TransactionServiceRegistry.get(TransactionTypes.Transfer); - canBeApplied = jest.spyOn(transactionService, "canBeApplied").mockReturnValue(true); - applyToSender = jest.spyOn(transactionService, "applyToSender").mockReturnValue(); + const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer); + canBeApplied = jest.spyOn(transactionHandler, "canBeApplied").mockReturnValue(true); + applyToSender = jest.spyOn(transactionHandler, "applyToSender").mockReturnValue(); jest.spyOn(connection.walletManager, "exists").mockReturnValue(true); findByPublicKey = jest @@ -664,8 +664,8 @@ describe("Connection", () => { }); it("should not apply transaction to wallet if canBeApplied() failed", async () => { - const transactionService = TransactionServiceRegistry.get(TransactionTypes.Transfer); - canBeApplied = jest.spyOn(transactionService, "canBeApplied").mockImplementation(() => { + const transactionHandler = TransactionHandlerRegistry.get(TransactionTypes.Transfer); + canBeApplied = jest.spyOn(transactionHandler, "canBeApplied").mockImplementation(() => { throw new Error("throw from test"); }); const purgeByPublicKey = jest.spyOn(connection, "purgeByPublicKey").mockReturnValue(); diff --git a/__tests__/unit/core-transactions/handler-registry.test.ts b/__tests__/unit/core-transactions/handler-registry.test.ts new file mode 100644 index 0000000000..87270c03ab --- /dev/null +++ b/__tests__/unit/core-transactions/handler-registry.test.ts @@ -0,0 +1,159 @@ +import "jest-extended"; + +import { Database, TransactionPool } from "@arkecosystem/core-interfaces"; +import { + Bignum, + configManager, + constants, + crypto, + ITransactionData, + schemas, + slots, + Transaction, + TransactionConstructor, + TransactionRegistry, +} from "@arkecosystem/crypto"; +import bs58check from "bs58check"; +import ByteBuffer from "bytebuffer"; +import { errors, TransactionHandler, TransactionHandlerRegistry } from "../../../packages/core-transactions/src"; + +const { transactionBaseSchema, extend } = schemas; +const { TransactionTypes } = constants; + +const TEST_TRANSACTION_TYPE = 100; + +class TestTransaction extends Transaction { + public static type = TEST_TRANSACTION_TYPE; + + public static getSchema(): schemas.TransactionSchema { + return extend(transactionBaseSchema, { + $id: "test", + required: ["recipientId", "amount", "asset"], + properties: { + type: { transactionType: TEST_TRANSACTION_TYPE }, + recipientId: { $ref: "address" }, + asset: { + type: "object", + required: ["test"], + properties: { + test: { + type: "number", + }, + }, + }, + }, + }); + } + + public serialize(): ByteBuffer { + const { data } = this; + const buffer = new ByteBuffer(24, true); + buffer.writeUint64(+data.amount); + buffer.writeUint32(data.expiration || 0); + buffer.append(bs58check.decode(data.recipientId)); + buffer.writeInt32(data.asset.test); + + return buffer; + } + + public deserialize(buf: ByteBuffer): void { + const { data } = this; + data.amount = new Bignum(buf.readUint64().toString()); + data.expiration = buf.readUint32(); + data.recipientId = bs58check.encode(buf.readBytes(21).toBuffer()); + data.asset = { + test: buf.readInt32(), + }; + } +} + +// tslint:disable-next-line:max-classes-per-file +class TestTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return TestTransaction; + } + + public apply(transaction: Transaction, wallet: Database.IWallet): void { + return; + } + public revert(transaction: Transaction, wallet: Database.IWallet): void { + return; + } + + public canEnterTransactionPool(data: ITransactionData, guard: TransactionPool.ITransactionGuard): boolean { + return true; + } +} + +beforeAll(() => { + configManager.setFromPreset("testnet"); + configManager.milestone.data.fees.staticFees.test = 1234; +}); + +afterAll(() => { + delete configManager.milestone.data.fees.staticFees.test; +}); + +afterEach(() => { + TransactionHandlerRegistry.deregisterCustomTransactionHandler(TestTransactionHandler); +}); + +describe("TransactionHandlerRegistry", () => { + it("should register core transaction types", () => { + expect(() => { + TransactionHandlerRegistry.get(TransactionTypes.Transfer); + TransactionHandlerRegistry.get(TransactionTypes.SecondSignature); + TransactionHandlerRegistry.get(TransactionTypes.DelegateRegistration); + TransactionHandlerRegistry.get(TransactionTypes.Vote); + TransactionHandlerRegistry.get(TransactionTypes.MultiSignature); + }).not.toThrow(errors.InvalidTransactionTypeError); + }); + + it("should register a custom type", () => { + expect(() => + TransactionHandlerRegistry.registerCustomTransactionHandler(TestTransactionHandler), + ).not.toThrowError(); + + expect(TransactionHandlerRegistry.get(TEST_TRANSACTION_TYPE)).toBeInstanceOf(TestTransactionHandler); + expect(TransactionRegistry.get(TEST_TRANSACTION_TYPE)).toBe(TestTransaction); + }); + + it("should be able to instantiate a custom transaction", () => { + TransactionHandlerRegistry.registerCustomTransactionHandler(TestTransactionHandler); + + const keys = crypto.getKeys("secret"); + const data: ITransactionData = { + type: TEST_TRANSACTION_TYPE, + timestamp: slots.getTime(), + senderPublicKey: keys.publicKey, + fee: "10000000", + amount: "200000000", + recipientId: "APyFYXxXtUrvZFnEuwLopfst94GMY5Zkeq", + asset: { + test: 256, + }, + }; + + data.signature = crypto.sign(data, keys); + data.id = crypto.getId(data); + + const transaction = Transaction.fromData(data); + expect(transaction).toBeInstanceOf(TestTransaction); + expect(transaction.verified).toBeTrue(); + + const bytes = Transaction.toBytes(transaction.data); + const deserialized = Transaction.fromBytes(bytes); + expect(deserialized.verified); + expect(deserialized.data.asset.test).toBe(256); + }); + + it("should not be ok when registering the same type again", () => { + expect(() => + TransactionHandlerRegistry.registerCustomTransactionHandler(TestTransactionHandler), + ).not.toThrowError(errors.TransactionHandlerAlreadyRegisteredError); + + expect(() => TransactionHandlerRegistry.registerCustomTransactionHandler(TestTransactionHandler)).toThrowError( + errors.TransactionHandlerAlreadyRegisteredError, + ); + }); +}); diff --git a/__tests__/unit/core-transactions/handler.test.ts b/__tests__/unit/core-transactions/handler.test.ts index 31718afa8f..79e72d33ad 100644 --- a/__tests__/unit/core-transactions/handler.test.ts +++ b/__tests__/unit/core-transactions/handler.test.ts @@ -18,8 +18,8 @@ import { WalletNoUsernameError, WalletUsernameNotEmptyError, } from "../../../packages/core-transactions/src/errors"; -import { TransactionServiceRegistry } from "../../../packages/core-transactions/src/index"; -import { TransactionService } from "../../../packages/core-transactions/src/services/transaction"; +import { TransactionHandler } from "../../../packages/core-transactions/src/handlers/transaction"; +import { TransactionHandlerRegistry } from "../../../packages/core-transactions/src/index"; import { transaction as transactionFixture } from "../crypto/transactions/__fixtures__/transaction"; import { wallet as walletFixture } from "../crypto/transactions/__fixtures__/wallet"; @@ -28,7 +28,7 @@ const { ARKTOSHI } = constants; let wallet: Wallet; let transaction: ITransactionData; let transactionWithSecondSignature: ITransactionData; -let service: TransactionService; +let handler: TransactionHandler; let instance: any; beforeEach(() => { @@ -67,48 +67,48 @@ beforeEach(() => { describe("General Tests", () => { beforeEach(() => { - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canBeApplied", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be true if the transaction has a second signature but wallet does not, when ignoreInvalidSecondSignatureField=true", () => { configManager.getMilestone().ignoreInvalidSecondSignatureField = true; instance = Transaction.fromData(transactionWithSecondSignature); - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if wallet publicKey does not match tx senderPublicKey", () => { instance.data.senderPublicKey = "a".repeat(66); - expect(() => service.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); }); it("should be false if the transaction has a second signature but wallet does not", () => { delete configManager.getMilestone().ignoreInvalidSecondSignatureField; instance = Transaction.fromData(transactionWithSecondSignature); - expect(() => service.canBeApplied(instance, wallet)).toThrow(UnexpectedSecondSignatureError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(UnexpectedSecondSignatureError); }); it("should be false if the wallet has a second public key but the transaction second signature does not match", () => { wallet.secondPublicKey = "invalid-public-key"; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InvalidSecondSignatureError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InvalidSecondSignatureError); }); it("should be false if wallet has not enough balance", () => { // 1 arktoshi short wallet.balance = new Bignum(transaction.amount).plus(transaction.fee).minus(1); - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); it("should be true even with publicKey case mismatch", () => { transaction.senderPublicKey = transaction.senderPublicKey.toUpperCase(); wallet.publicKey = wallet.publicKey.toLowerCase(); instance = Transaction.fromData(transaction); - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); }); @@ -116,7 +116,7 @@ describe("General Tests", () => { it("should be ok", () => { const initialBalance = 1000 * ARKTOSHI; wallet.balance = new Bignum(initialBalance); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance).minus(transaction.amount).minus(transaction.fee)); }); @@ -125,7 +125,7 @@ describe("General Tests", () => { wallet.balance = new Bignum(initialBalance); instance.data.senderPublicKey = "a".repeat(66); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance)); }); @@ -137,7 +137,7 @@ describe("General Tests", () => { const instance = Transaction.fromData(transaction); wallet.publicKey = wallet.publicKey.toLowerCase(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance).minus(transaction.amount).minus(transaction.fee)); }); }); @@ -147,7 +147,7 @@ describe("General Tests", () => { const initialBalance = 1000 * ARKTOSHI; wallet.balance = new Bignum(initialBalance); - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance).plus(transaction.amount).plus(transaction.fee)); }); @@ -156,7 +156,7 @@ describe("General Tests", () => { wallet.balance = new Bignum(initialBalance); transaction.senderPublicKey = "a".repeat(66); - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance)); }); }); @@ -166,7 +166,7 @@ describe("General Tests", () => { const initialBalance = 1000 * ARKTOSHI; wallet.balance = new Bignum(initialBalance); - service.applyToRecipient(instance, wallet); + handler.applyToRecipient(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance).plus(transaction.amount)); }); @@ -175,7 +175,7 @@ describe("General Tests", () => { wallet.balance = new Bignum(initialBalance); transaction.recipientId = "invalid-recipientId"; - service.applyToRecipient(instance, wallet); + handler.applyToRecipient(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance)); }); }); @@ -185,7 +185,7 @@ describe("General Tests", () => { const initialBalance = 1000 * ARKTOSHI; wallet.balance = new Bignum(initialBalance); - service.revertForRecipient(instance, wallet); + handler.revertForRecipient(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance).minus(transaction.amount)); }); @@ -195,7 +195,7 @@ describe("General Tests", () => { transaction.recipientId = "invalid-recipientId"; - service.revertForRecipient(instance, wallet); + handler.revertForRecipient(instance, wallet); expect(wallet.balance).toEqual(new Bignum(initialBalance)); }); }); @@ -205,18 +205,18 @@ describe("TransferTransaction", () => { beforeEach(() => { wallet = walletFixture; transaction = transactionFixture; - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false", () => { instance.data.senderPublicKey = "a".repeat(66); - expect(() => service.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); }); }); }); @@ -249,55 +249,55 @@ describe("SecondSignatureRegistrationTransaction", () => { id: "e5a4cf622a24d459987f093e14a14c6b0492834358f86099afe1a2d14457cf31", }; - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if wallet already has a second signature", () => { wallet.secondPublicKey = "02d5cfcbc4920d041d2a54b29e1f69173536796fd50f62af0f88ad6adc6df07cb8"; - expect(() => service.canBeApplied(instance, wallet)).toThrow(SecondSignatureAlreadyRegisteredError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SecondSignatureAlreadyRegisteredError); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); describe("apply", () => { it("should apply second signature registration", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.secondPublicKey).toBe("02d5cfcbc4920d041d2a54b29e1f69173536796fd50f62af0f88ad6adc6df07cb8"); }); it("should be invalid to apply a second signature registration twice", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.secondPublicKey).toBe("02d5cfcbc4920d041d2a54b29e1f69173536796fd50f62af0f88ad6adc6df07cb8"); - expect(() => service.canBeApplied(instance, wallet)).toThrow(SecondSignatureAlreadyRegisteredError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SecondSignatureAlreadyRegisteredError); }); }); describe("revert", () => { it("should be ok", () => { expect(wallet.secondPublicKey).toBeNull(); - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.secondPublicKey).toBe("02d5cfcbc4920d041d2a54b29e1f69173536796fd50f62af0f88ad6adc6df07cb8"); - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.secondPublicKey).toBeNull(); }); }); @@ -326,39 +326,39 @@ describe("DelegateRegistrationTransaction", () => { }, }; - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if wallet already registered a username", () => { wallet.username = "dummy"; - expect(() => service.canBeApplied(instance, wallet)).toThrow(WalletUsernameNotEmptyError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(WalletUsernameNotEmptyError); }); it("should be false if wallet has insufficient funds", () => { wallet.username = ""; wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); describe("apply", () => { it("should set username", () => { - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.username).toBe("dummy"); }); }); describe("revert", () => { it("should unset username", () => { - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.username).toBeNull(); }); }); @@ -406,40 +406,40 @@ describe("VoteTransaction", () => { }, }; - service = TransactionServiceRegistry.get(voteTransaction.type); + handler = TransactionHandlerRegistry.get(voteTransaction.type); instance = Transaction.fromData(voteTransaction); }); describe("canApply", () => { it("should be true if the vote is valid and the wallet has not voted", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be true if the unvote is valid and the wallet has voted", () => { wallet.vote = "02d0d835266297f15c192be2636eb3fbc30b39b87fc583ff112062ef8ae1a1f2af"; instance = Transaction.fromData(unvoteTransaction); - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if wallet has already voted", () => { wallet.vote = "02d0d835266297f15c192be2636eb3fbc30b39b87fc583ff112062ef8ae1a1f2af"; - expect(() => service.canBeApplied(instance, wallet)).toThrow(AlreadyVotedError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(AlreadyVotedError); }); it("should be false if the asset public key differs from the currently voted one", () => { wallet.vote = "a310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0"; instance = Transaction.fromData(unvoteTransaction); - expect(() => service.canBeApplied(instance, wallet)).toThrow(UnvoteMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(UnvoteMismatchError); }); it("should be false if unvoting a non-voted wallet", () => { instance = Transaction.fromData(unvoteTransaction); - expect(() => service.canBeApplied(instance, wallet)).toThrow(NoVoteError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(NoVoteError); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); @@ -448,7 +448,7 @@ describe("VoteTransaction", () => { it("should be ok", () => { expect(wallet.vote).toBeNull(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.vote).not.toBeNull(); }); @@ -457,7 +457,7 @@ describe("VoteTransaction", () => { expect(wallet.vote).not.toBeNull(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.vote).not.toBeNull(); }); @@ -470,7 +470,7 @@ describe("VoteTransaction", () => { expect(wallet.vote).not.toBeNull(); instance = Transaction.fromData(unvoteTransaction); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.vote).toBeNull(); }); @@ -484,7 +484,7 @@ describe("VoteTransaction", () => { expect(wallet.vote).not.toBeNull(); - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.vote).toBeNull(); }); @@ -495,7 +495,7 @@ describe("VoteTransaction", () => { expect(wallet.vote).toBeNull(); instance = Transaction.fromData(unvoteTransaction); - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.vote).toBe("02d0d835266297f15c192be2636eb3fbc30b39b87fc583ff112062ef8ae1a1f2af"); }); @@ -590,35 +590,35 @@ describe.skip("MultiSignatureRegistrationTransaction", () => { ], }; - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { delete wallet.multisignature; - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if the wallet already has multisignatures", () => { wallet.verifySignatures = jest.fn(() => true); wallet.multisignature = multisignatureTest; - expect(() => service.canBeApplied(instance, wallet)).toThrow(MultiSignatureAlreadyRegisteredError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(MultiSignatureAlreadyRegisteredError); }); it("should be false if failure to verify signatures", () => { wallet.verifySignatures = jest.fn(() => false); delete wallet.multisignature; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InvalidMultiSignatureError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InvalidMultiSignatureError); }); it("should be false if failure to verify signatures in asset", () => { wallet.verifySignatures = jest.fn(() => false); delete wallet.multisignature; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InvalidMultiSignatureError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InvalidMultiSignatureError); }); it("should be false if the number of keys is less than minimum", () => { @@ -628,7 +628,7 @@ describe.skip("MultiSignatureRegistrationTransaction", () => { crypto.verifySecondSignature = jest.fn(() => true); instance.data.asset.multisignature.keysgroup.splice(0, 5); - expect(() => service.canBeApplied(instance, wallet)).toThrow(MultiSignatureMinimumKeysError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(MultiSignatureMinimumKeysError); }); it("should be false if the number of keys does not equal the signature count", () => { @@ -638,14 +638,14 @@ describe.skip("MultiSignatureRegistrationTransaction", () => { crypto.verifySecondSignature = jest.fn(() => true); instance.data.signatures.splice(0, 5); - expect(() => service.canBeApplied(instance, wallet)).toThrow(MultiSignatureKeyCountMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(MultiSignatureKeyCountMismatchError); }); it("should be false if wallet has insufficient funds", () => { delete wallet.multisignature; wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); @@ -655,7 +655,7 @@ describe.skip("MultiSignatureRegistrationTransaction", () => { expect(wallet.multisignature).toBeNull(); - service.applyToSender(instance, wallet); + handler.applyToSender(instance, wallet); expect(wallet.multisignature).toEqual(transaction.asset.multisignature); }); @@ -663,7 +663,7 @@ describe.skip("MultiSignatureRegistrationTransaction", () => { describe("revert", () => { it("should be ok", () => { - service.revertForSender(instance, wallet); + handler.revertForSender(instance, wallet); expect(wallet.multisignature).toBeNull(); }); @@ -675,23 +675,23 @@ describe.skip("IpfsTransaction", () => { transaction = transactionFixture; wallet = walletFixture; wallet.balance = new Bignum(transaction.amount).plus(transaction.fee); - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false", () => { instance.data.senderPublicKey = "a".repeat(66); - expect(() => service.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); }); @@ -701,23 +701,23 @@ describe.skip("TimelockTransferTransaction", () => { transaction = transactionFixture; wallet = walletFixture; wallet.balance = new Bignum(transaction.amount).plus(transaction.fee); - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false", () => { instance.data.senderPublicKey = "a".repeat(66); - expect(() => service.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(SenderWalletMismatchError); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); }); @@ -763,23 +763,23 @@ describe.skip("MultiPaymentTransaction", () => { wallet = walletFixture; wallet.balance = new Bignum(transaction.amount).plus(transaction.fee); - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be true", () => { - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); it("should be false if wallet has insufficient funds send all payouts", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); }); @@ -789,24 +789,24 @@ describe.skip("DelegateResignationTransaction", () => { transaction = transactionFixture; wallet = walletFixture; wallet.balance = new Bignum(transaction.amount).plus(transaction.fee); - service = TransactionServiceRegistry.get(transaction.type); + handler = TransactionHandlerRegistry.get(transaction.type); instance = Transaction.fromData(transaction); }); describe("canApply", () => { it("should be truth", () => { wallet.username = "dummy"; - expect(service.canBeApplied(instance, wallet)).toBeTrue(); + expect(handler.canBeApplied(instance, wallet)).toBeTrue(); }); it.skip("should be false if wallet has no registered username", () => { wallet.username = null; - expect(() => service.canBeApplied(instance, wallet)).toThrow(WalletNoUsernameError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(WalletNoUsernameError); }); it("should be false if wallet has insufficient funds", () => { wallet.balance = Bignum.ZERO; - expect(() => service.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); + expect(() => handler.canBeApplied(instance, wallet)).toThrow(InsufficientBalanceError); }); }); }); diff --git a/__tests__/unit/crypto/managers/config.test.ts b/__tests__/unit/crypto/managers/config.test.ts index 25790f7fa3..f775a80ca6 100644 --- a/__tests__/unit/crypto/managers/config.test.ts +++ b/__tests__/unit/crypto/managers/config.test.ts @@ -49,6 +49,40 @@ describe("Configuration", () => { expect(feeManager.get(TransactionTypes.DelegateResignation)).toEqual(feesStatic.delegateResignation); }); + it("should update fees on milestone change", () => { + devnet.milestones.push({ + height: 100_000_000, + fees: { staticFees: { transfer: 1234 } }, + } as any); + + configManager.setHeight(100_000_000); + + let { staticFees } = configManager.getMilestone().fees; + expect(feeManager.get(TransactionTypes.Transfer)).toEqual(1234); + expect(feeManager.get(TransactionTypes.SecondSignature)).toEqual(staticFees.secondSignature); + expect(feeManager.get(TransactionTypes.DelegateRegistration)).toEqual(staticFees.delegateRegistration); + expect(feeManager.get(TransactionTypes.Vote)).toEqual(staticFees.vote); + expect(feeManager.get(TransactionTypes.MultiSignature)).toEqual(staticFees.multiSignature); + expect(feeManager.get(TransactionTypes.Ipfs)).toEqual(staticFees.ipfs); + expect(feeManager.get(TransactionTypes.TimelockTransfer)).toEqual(staticFees.timelockTransfer); + expect(feeManager.get(TransactionTypes.MultiPayment)).toEqual(staticFees.multiPayment); + expect(feeManager.get(TransactionTypes.DelegateResignation)).toEqual(staticFees.delegateResignation); + + configManager.setHeight(1); + staticFees = configManager.getMilestone().fees.staticFees; + expect(feeManager.get(TransactionTypes.Transfer)).toEqual(staticFees.transfer); + expect(feeManager.get(TransactionTypes.SecondSignature)).toEqual(staticFees.secondSignature); + expect(feeManager.get(TransactionTypes.DelegateRegistration)).toEqual(staticFees.delegateRegistration); + expect(feeManager.get(TransactionTypes.Vote)).toEqual(staticFees.vote); + expect(feeManager.get(TransactionTypes.MultiSignature)).toEqual(staticFees.multiSignature); + expect(feeManager.get(TransactionTypes.Ipfs)).toEqual(staticFees.ipfs); + expect(feeManager.get(TransactionTypes.TimelockTransfer)).toEqual(staticFees.timelockTransfer); + expect(feeManager.get(TransactionTypes.MultiPayment)).toEqual(staticFees.multiPayment); + expect(feeManager.get(TransactionTypes.DelegateResignation)).toEqual(staticFees.delegateResignation); + + devnet.milestones.pop(); + }); + it("should get milestone for height", () => { expect(configManager.getMilestone(21600)).toEqual(devnet.milestones[2]); }); diff --git a/packages/core-blockchain/src/state-storage.ts b/packages/core-blockchain/src/state-storage.ts index 1a48b11d8e..2f6f3189aa 100644 --- a/packages/core-blockchain/src/state-storage.ts +++ b/packages/core-blockchain/src/state-storage.ts @@ -2,7 +2,7 @@ import { app } from "@arkecosystem/core-container"; import { Blockchain, Logger } from "@arkecosystem/core-interfaces"; -import { configManager, ITransactionData, models } from "@arkecosystem/crypto"; +import { configManager, ITransactionData, models, TransactionRegistry } from "@arkecosystem/crypto"; import assert from "assert"; import immutable from "immutable"; import { config } from "./config"; @@ -96,6 +96,7 @@ export class StateStorage implements Blockchain.IStateStorage { _lastBlocks = _lastBlocks.set(block.data.height, block); configManager.setHeight(block.data.height); + TransactionRegistry.updateStaticFees(block.data.height); // Delete oldest block if size exceeds the maximum if (_lastBlocks.size > config.get("state.maxLastBlocks")) { diff --git a/packages/core-database/src/database-service.ts b/packages/core-database/src/database-service.ts index b2ea1dc221..c2be937477 100644 --- a/packages/core-database/src/database-service.ts +++ b/packages/core-database/src/database-service.ts @@ -1,6 +1,6 @@ import { app } from "@arkecosystem/core-container"; import { Blockchain, Database, EventEmitter, Logger } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { roundCalculator } from "@arkecosystem/core-utils"; import { Bignum, crypto, HashAlgorithms, models, Transaction } from "@arkecosystem/crypto"; import assert from "assert"; @@ -491,7 +491,7 @@ export class DatabaseService implements Database.IDatabaseService { const senderId = crypto.getAddress(transaction.data.senderPublicKey, this.config.get("network.pubKeyHash")); const sender = this.walletManager.findByAddress(senderId); // should exist - const transactionService = TransactionServiceRegistry.get(transaction.type); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); if (!sender.publicKey) { sender.publicKey = transaction.data.senderPublicKey; @@ -501,7 +501,7 @@ export class DatabaseService implements Database.IDatabaseService { const dbTransaction = await this.getTransaction(transaction.data.id); try { - return transactionService.canBeApplied(transaction, sender) && !dbTransaction; + return transactionHandler.canBeApplied(transaction, sender) && !dbTransaction; } catch { return false; } @@ -538,8 +538,8 @@ export class DatabaseService implements Database.IDatabaseService { private emitTransactionEvents(transaction: Transaction) { this.emitter.emit("transaction.applied", transaction.data); - const service = TransactionServiceRegistry.get(transaction.type); - service.emitEvents(transaction, this.emitter); + const handler = TransactionHandlerRegistry.get(transaction.type); + handler.emitEvents(transaction, this.emitter); } private registerListeners() { diff --git a/packages/core-database/src/wallet-manager.ts b/packages/core-database/src/wallet-manager.ts index b64f4f716b..64da614419 100644 --- a/packages/core-database/src/wallet-manager.ts +++ b/packages/core-database/src/wallet-manager.ts @@ -1,6 +1,6 @@ import { app } from "@arkecosystem/core-container"; import { Database, Logger } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { roundCalculator } from "@arkecosystem/core-utils"; import { Bignum, constants, crypto, formatSatoshi, isException, models, Transaction } from "@arkecosystem/crypto"; import pluralize from "pluralize"; @@ -391,7 +391,7 @@ export class WalletManager implements Database.IWalletManager { const { data } = transaction; const { type, recipientId, senderPublicKey } = data; - const transactionService = TransactionServiceRegistry.get(transaction.type); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); const sender = this.findByPublicKey(senderPublicKey); const recipient = this.findByAddress(recipientId); const errors = []; @@ -406,7 +406,7 @@ export class WalletManager implements Database.IWalletManager { this.logger.warn(`Transaction ${data.id} forcibly applied because it has been added as an exception.`); } else { try { - transactionService.canBeApplied(transaction, sender, this); + transactionHandler.canBeApplied(transaction, sender, this); } catch (error) { this.logger.error( `Can't apply transaction id:${data.id} from sender:${sender.address} due to ${error.message}`, @@ -416,7 +416,7 @@ export class WalletManager implements Database.IWalletManager { } } - transactionService.applyToSender(transaction, sender); + transactionHandler.applyToSender(transaction, sender); if (type === TransactionTypes.DelegateRegistration) { this.reindex(sender); @@ -424,7 +424,7 @@ export class WalletManager implements Database.IWalletManager { // TODO: make more generic if (recipient && type === TransactionTypes.Transfer) { - transactionService.applyToRecipient(transaction, recipient); + transactionHandler.applyToRecipient(transaction, recipient); } this._updateVoteBalances(sender, recipient, data); @@ -484,11 +484,11 @@ export class WalletManager implements Database.IWalletManager { */ public revertTransaction(transaction: Transaction) { const { type, data } = transaction; - const transactionService = TransactionServiceRegistry.get(transaction.type); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); const sender = this.findByPublicKey(data.senderPublicKey); // Should exist const recipient = this.byAddress[data.recipientId]; - transactionService.revertForSender(transaction, sender); + transactionHandler.revertForSender(transaction, sender); // removing the wallet from the delegates index if (type === TransactionTypes.DelegateRegistration) { @@ -496,7 +496,7 @@ export class WalletManager implements Database.IWalletManager { } if (recipient && type === TransactionTypes.Transfer) { - transactionService.revertForRecipient(transaction, recipient); + transactionHandler.revertForRecipient(transaction, recipient); } // Revert vote balance updates diff --git a/packages/core-transaction-pool/src/connection.ts b/packages/core-transaction-pool/src/connection.ts index bfedd8d8fc..f30d8abf3e 100644 --- a/packages/core-transaction-pool/src/connection.ts +++ b/packages/core-transaction-pool/src/connection.ts @@ -6,7 +6,7 @@ import { Logger, TransactionPool as transactionPool, } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { dato, Dato } from "@faustbrian/dato"; import assert from "assert"; @@ -190,8 +190,8 @@ export class TransactionPool implements transactionPool.ITransactionPool { // TODO: rework error handling const errors = []; if (this.walletManager.canApply(transaction, errors)) { - const transactionService = TransactionServiceRegistry.get(transaction.type); - transactionService.applyToSender(transaction, senderWallet); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); + transactionHandler.applyToSender(transaction, senderWallet); } else { // Remove tx again from the pool this.mem.remove(transaction.id); @@ -385,7 +385,7 @@ export class TransactionPool implements transactionPool.ITransactionPool { const { data } = transaction; const exists = this.transactionExists(data.id); const senderPublicKey = data.senderPublicKey; - const transactionService = TransactionServiceRegistry.get(transaction.type); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); const senderWallet = this.walletManager.exists(senderPublicKey) ? this.walletManager.findByPublicKey(senderPublicKey) @@ -396,7 +396,7 @@ export class TransactionPool implements transactionPool.ITransactionPool { : false; if (recipientWallet) { - transactionService.applyToRecipient(transaction, recipientWallet); + transactionHandler.applyToRecipient(transaction, recipientWallet); } if (exists) { @@ -404,7 +404,7 @@ export class TransactionPool implements transactionPool.ITransactionPool { } else if (senderWallet) { // TODO: rework error handling try { - transactionService.canBeApplied(transaction, senderWallet); + transactionHandler.canBeApplied(transaction, senderWallet); } catch (error) { this.purgeByPublicKey(data.senderPublicKey); this.blockSender(data.senderPublicKey); @@ -417,7 +417,7 @@ export class TransactionPool implements transactionPool.ITransactionPool { return; } - transactionService.applyToSender(transaction, senderWallet); + transactionHandler.applyToSender(transaction, senderWallet); } if ( @@ -457,13 +457,13 @@ export class TransactionPool implements transactionPool.ITransactionPool { return; } - const transactionService = TransactionServiceRegistry.get(transaction.type); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); const senderWallet = this.walletManager.findByPublicKey(transaction.data.senderPublicKey); // TODO: rework error handling try { - transactionService.canBeApplied(transaction, senderWallet); - transactionService.applyToSender(transaction, senderWallet); + transactionHandler.canBeApplied(transaction, senderWallet); + transactionHandler.applyToSender(transaction, senderWallet); } catch (error) { this.logger.error(`BuildWallets from pool: ${error.message}`); this.purgeByPublicKey(transaction.data.senderPublicKey); diff --git a/packages/core-transaction-pool/src/dynamic-fee/matcher.ts b/packages/core-transaction-pool/src/dynamic-fee/matcher.ts index 12732455e4..71c55fe266 100644 --- a/packages/core-transaction-pool/src/dynamic-fee/matcher.ts +++ b/packages/core-transaction-pool/src/dynamic-fee/matcher.ts @@ -12,9 +12,14 @@ export function calculateFee(satoshiPerByte: number, transaction: Transaction): satoshiPerByte = 1; } - const addonBytes = localConfig.get("dynamicFees.addonBytes")[ - camelCase(constants.TransactionTypes[transaction.type]) - ]; + let key; + if (transaction.type in constants.TransactionTypes) { + key = camelCase(constants.TransactionTypes[transaction.type]); + } else { + key = camelCase(transaction.constructor.name.replace("Transaction", "")); + } + + const addonBytes = localConfig.get("dynamicFees.addonBytes")[key]; // serialized is in hex const transactionSizeInBytes = transaction.serialized.length / 2; diff --git a/packages/core-transaction-pool/src/guard.ts b/packages/core-transaction-pool/src/guard.ts index 5099ad4a0e..15a5fa6ca0 100644 --- a/packages/core-transaction-pool/src/guard.ts +++ b/packages/core-transaction-pool/src/guard.ts @@ -1,7 +1,14 @@ import { app } from "@arkecosystem/core-container"; import { Blockchain, Database, Logger, TransactionPool as transanctionPool } from "@arkecosystem/core-interfaces"; -import { InvalidTransactionTypeError, TransactionServiceRegistry } from "@arkecosystem/core-transactions"; -import { configManager, constants, errors, ITransactionData, slots, Transaction } from "@arkecosystem/crypto"; +import { errors, TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; +import { + configManager, + constants, + errors as cryptoErrors, + ITransactionData, + slots, + Transaction, +} from "@arkecosystem/crypto"; import pluralize from "pluralize"; import { TransactionPool } from "./connection"; import { dynamicFeeMatcher } from "./dynamic-fee"; @@ -133,7 +140,7 @@ export class TransactionGuard implements transanctionPool.ITransactionGuard { ); } } catch (error) { - if (error instanceof errors.TransactionSchemaError) { + if (error instanceof cryptoErrors.TransactionSchemaError) { this.pushError(transaction, "ERR_TRANSACTION_SCHEMA", error.message); } else { this.pushError(transaction, "ERR_UNKNOWN", error.message); @@ -176,10 +183,10 @@ export class TransactionGuard implements transanctionPool.ITransactionGuard { const { type } = transaction; try { - const service = TransactionServiceRegistry.get(type); - return service.canEnterTransactionPool(transaction, this); + const handler = TransactionHandlerRegistry.get(type); + return handler.canEnterTransactionPool(transaction, this); } catch (error) { - if (error instanceof InvalidTransactionTypeError) { + if (error instanceof errors.InvalidTransactionTypeError) { this.pushError( transaction, "ERR_UNSUPPORTED", diff --git a/packages/core-transaction-pool/src/pool-wallet-manager.ts b/packages/core-transaction-pool/src/pool-wallet-manager.ts index e349a52d4e..b0a42790fa 100644 --- a/packages/core-transaction-pool/src/pool-wallet-manager.ts +++ b/packages/core-transaction-pool/src/pool-wallet-manager.ts @@ -1,7 +1,7 @@ import { app } from "@arkecosystem/core-container"; import { Wallet, WalletManager } from "@arkecosystem/core-database"; import { Database } from "@arkecosystem/core-interfaces"; -import { TransactionServiceRegistry } from "@arkecosystem/core-transactions"; +import { TransactionHandlerRegistry } from "@arkecosystem/core-transactions"; import { crypto, isException, Transaction } from "@arkecosystem/crypto"; export class PoolWalletManager extends WalletManager { @@ -63,8 +63,8 @@ export class PoolWalletManager extends WalletManager { ); } else { try { - const transactionService = TransactionServiceRegistry.get(transaction.type); - transactionService.canBeApplied(transaction, sender, this.databaseService.walletManager); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); + transactionHandler.canBeApplied(transaction, sender, this.databaseService.walletManager); } catch (error) { const message = `[PoolWalletManager] Can't apply transaction ${transaction.id} from ${sender.address}`; this.logger.error(`${message} due to ${JSON.stringify(error.message)}`); @@ -82,7 +82,7 @@ export class PoolWalletManager extends WalletManager { const { data } = transaction; const sender = this.findByPublicKey(data.senderPublicKey); // Should exist - const transactionService = TransactionServiceRegistry.get(transaction.type); - transactionService.revertForSender(transaction, sender); + const transactionHandler = TransactionHandlerRegistry.get(transaction.type); + transactionHandler.revertForSender(transaction, sender); } } diff --git a/packages/core-transactions/src/errors.ts b/packages/core-transactions/src/errors.ts index 91a9ef59fd..ec5dc8beba 100644 --- a/packages/core-transactions/src/errors.ts +++ b/packages/core-transactions/src/errors.ts @@ -24,7 +24,7 @@ export class NotImplementedError extends TransactionError { } } -export class TransactionServiceAlreadyRegisteredError extends TransactionError { +export class TransactionHandlerAlreadyRegisteredError extends TransactionError { constructor(type: number) { super(`Transaction service for type ${type} is already registered.`); } diff --git a/packages/core-transactions/src/handler-registry.ts b/packages/core-transactions/src/handler-registry.ts new file mode 100644 index 0000000000..d290fa4ea1 --- /dev/null +++ b/packages/core-transactions/src/handler-registry.ts @@ -0,0 +1,68 @@ +import { constants, TransactionRegistry } from "@arkecosystem/crypto"; +import { InvalidTransactionTypeError, TransactionHandlerAlreadyRegisteredError } from "./errors"; +import { transactionHandlers } from "./handlers"; +import { TransactionHandler } from "./handlers/transaction"; + +export type TransactionHandlerConstructor = new () => TransactionHandler; + +class TransactionHandlerRegistry { + private readonly coreTransactionHandlers = new Map(); + private readonly customTransactionHandlers = new Map(); + + constructor() { + transactionHandlers.forEach((service: TransactionHandlerConstructor) => { + this.registerCoreTransactionHandler(service); + }); + } + + public get(type: constants.TransactionTypes | number): TransactionHandler { + if (this.coreTransactionHandlers.has(type)) { + return this.coreTransactionHandlers.get(type); + } + + if (this.customTransactionHandlers.has(type)) { + return this.customTransactionHandlers.get(type); + } + + throw new InvalidTransactionTypeError(type); + } + + public registerCustomTransactionHandler(constructor: TransactionHandlerConstructor): void { + const service = new constructor(); + const transactionConstructor = service.getConstructor(); + const { type } = transactionConstructor; + + if (this.customTransactionHandlers.has(type)) { + throw new TransactionHandlerAlreadyRegisteredError(type); + } + + TransactionRegistry.registerCustomType(transactionConstructor); + + this.customTransactionHandlers.set(type, service); + } + + public deregisterCustomTransactionHandler(constructor: TransactionHandlerConstructor): void { + const service = new constructor(); + const transactionConstructor = service.getConstructor(); + const { type } = transactionConstructor; + + if (this.customTransactionHandlers.has(type)) { + TransactionRegistry.deregisterCustomType(type); + this.customTransactionHandlers.delete(type); + } + } + + private registerCoreTransactionHandler(constructor: TransactionHandlerConstructor) { + const service = new constructor(); + const transactionConstructor = service.getConstructor(); + const { type } = transactionConstructor; + + if (this.coreTransactionHandlers.has(type)) { + throw new TransactionHandlerAlreadyRegisteredError(type); + } + + this.coreTransactionHandlers.set(type, service); + } +} + +export const transactionHandlerRegistry = new TransactionHandlerRegistry(); diff --git a/packages/core-transactions/src/services/delegate-registration.ts b/packages/core-transactions/src/handlers/delegate-registration.ts similarity index 87% rename from packages/core-transactions/src/services/delegate-registration.ts rename to packages/core-transactions/src/handlers/delegate-registration.ts index 6f49495ccd..bc9d4681b9 100644 --- a/packages/core-transactions/src/services/delegate-registration.ts +++ b/packages/core-transactions/src/handlers/delegate-registration.ts @@ -1,13 +1,19 @@ import { Database, EventEmitter, TransactionPool } from "@arkecosystem/core-interfaces"; -import { constants, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { + constants, + DelegateRegistrationTransaction, + ITransactionData, + Transaction, + TransactionConstructor, +} from "@arkecosystem/crypto"; import { WalletUsernameAlreadyRegisteredError, WalletUsernameEmptyError, WalletUsernameNotEmptyError } from "../errors"; -import { TransactionService } from "./transaction"; +import { TransactionHandler } from "./transaction"; const { TransactionTypes } = constants; -export class DelegateRegistrationTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.DelegateRegistration; +export class DelegateRegistrationTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return DelegateRegistrationTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/delegate-resignation.ts b/packages/core-transactions/src/handlers/delegate-resignation.ts similarity index 66% rename from packages/core-transactions/src/services/delegate-resignation.ts rename to packages/core-transactions/src/handlers/delegate-resignation.ts index c267235026..008210052d 100644 --- a/packages/core-transactions/src/services/delegate-resignation.ts +++ b/packages/core-transactions/src/handlers/delegate-resignation.ts @@ -1,10 +1,10 @@ import { Database, EventEmitter } from "@arkecosystem/core-interfaces"; -import { constants, Transaction } from "@arkecosystem/crypto"; -import { TransactionService } from "./transaction"; +import { DelegateResignationTransaction, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; +import { TransactionHandler } from "./transaction"; -export class DelegateResignationTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.DelegateResignation; +export class DelegateResignationTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return DelegateResignationTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/handlers/index.ts b/packages/core-transactions/src/handlers/index.ts new file mode 100644 index 0000000000..c423e36c92 --- /dev/null +++ b/packages/core-transactions/src/handlers/index.ts @@ -0,0 +1,21 @@ +import { DelegateRegistrationTransactionHandler } from "./delegate-registration"; +import { DelegateResignationTransactionHandler } from "./delegate-resignation"; +import { IpfsTransactionHandler } from "./ipfs"; +import { MultiPaymentTransactionHandler } from "./multi-payment"; +import { MultiSignatureTransactionHandler } from "./multi-signature"; +import { SecondSignatureTransactionHandler } from "./second-signature"; +import { TimelockTransferTransactionHandler } from "./timelock-transfer"; +import { TransferTransactionHandler } from "./transfer"; +import { VoteTransactionHandler } from "./vote"; + +export const transactionHandlers = [ + TransferTransactionHandler, + SecondSignatureTransactionHandler, + VoteTransactionHandler, + DelegateRegistrationTransactionHandler, + MultiSignatureTransactionHandler, + IpfsTransactionHandler, + TimelockTransferTransactionHandler, + MultiPaymentTransactionHandler, + DelegateResignationTransactionHandler, +]; diff --git a/packages/core-transactions/src/services/ipfs.ts b/packages/core-transactions/src/handlers/ipfs.ts similarity index 63% rename from packages/core-transactions/src/services/ipfs.ts rename to packages/core-transactions/src/handlers/ipfs.ts index 2a63d4fd32..103a082ecc 100644 --- a/packages/core-transactions/src/services/ipfs.ts +++ b/packages/core-transactions/src/handlers/ipfs.ts @@ -1,10 +1,10 @@ import { Database } from "@arkecosystem/core-interfaces"; -import { constants, Transaction } from "@arkecosystem/crypto"; -import { TransactionService } from "./transaction"; +import { IpfsTransaction, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; +import { TransactionHandler } from "./transaction"; -export class IpfsTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.Ipfs; +export class IpfsTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return IpfsTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/multi-payment.ts b/packages/core-transactions/src/handlers/multi-payment.ts similarity index 61% rename from packages/core-transactions/src/services/multi-payment.ts rename to packages/core-transactions/src/handlers/multi-payment.ts index cab9377c18..a82bf8c9c2 100644 --- a/packages/core-transactions/src/services/multi-payment.ts +++ b/packages/core-transactions/src/handlers/multi-payment.ts @@ -1,10 +1,10 @@ import { Database } from "@arkecosystem/core-interfaces"; -import { constants, Transaction } from "@arkecosystem/crypto"; -import { TransactionService } from "./transaction"; +import { MultiPaymentTransaction, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; +import { TransactionHandler } from "./transaction"; -export class MultiPaymentTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.MultiPayment; +export class MultiPaymentTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return MultiPaymentTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/multi-signature.ts b/packages/core-transactions/src/handlers/multi-signature.ts similarity index 79% rename from packages/core-transactions/src/services/multi-signature.ts rename to packages/core-transactions/src/handlers/multi-signature.ts index fb424eb4d5..fc7fd5dda7 100644 --- a/packages/core-transactions/src/services/multi-signature.ts +++ b/packages/core-transactions/src/handlers/multi-signature.ts @@ -1,16 +1,16 @@ import { Database } from "@arkecosystem/core-interfaces"; -import { constants, Transaction } from "@arkecosystem/crypto"; +import { MultiSignatureRegistrationTransaction, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; import { InvalidMultiSignatureError, MultiSignatureAlreadyRegisteredError, MultiSignatureKeyCountMismatchError, MultiSignatureMinimumKeysError, } from "../errors"; -import { TransactionService } from "./transaction"; +import { TransactionHandler } from "./transaction"; -export class MultiSignatureTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.MultiSignature; +export class MultiSignatureTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return MultiSignatureRegistrationTransaction; } // TODO: AIP18 diff --git a/packages/core-transactions/src/services/second-signature.ts b/packages/core-transactions/src/handlers/second-signature.ts similarity index 71% rename from packages/core-transactions/src/services/second-signature.ts rename to packages/core-transactions/src/handlers/second-signature.ts index aacc8ac548..9a17ae9f28 100644 --- a/packages/core-transactions/src/services/second-signature.ts +++ b/packages/core-transactions/src/handlers/second-signature.ts @@ -1,11 +1,16 @@ import { Database, TransactionPool } from "@arkecosystem/core-interfaces"; -import { constants, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { + ITransactionData, + SecondSignatureRegistrationTransaction, + Transaction, + TransactionConstructor, +} from "@arkecosystem/crypto"; import { SecondSignatureAlreadyRegisteredError } from "../errors"; -import { TransactionService } from "./transaction"; +import { TransactionHandler } from "./transaction"; -export class SecondSignatureTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.SecondSignature; +export class SecondSignatureTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return SecondSignatureRegistrationTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/timelock-transfer.ts b/packages/core-transactions/src/handlers/timelock-transfer.ts similarity index 60% rename from packages/core-transactions/src/services/timelock-transfer.ts rename to packages/core-transactions/src/handlers/timelock-transfer.ts index d878ff5741..ec41bd8e76 100644 --- a/packages/core-transactions/src/services/timelock-transfer.ts +++ b/packages/core-transactions/src/handlers/timelock-transfer.ts @@ -1,10 +1,10 @@ import { Database } from "@arkecosystem/core-interfaces"; -import { constants, models, Transaction } from "@arkecosystem/crypto"; -import { TransactionService } from "./transaction"; +import { TimelockTransferTransaction, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; +import { TransactionHandler } from "./transaction"; -export class TimelockTransferTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.TimelockTransfer; +export class TimelockTransferTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return TimelockTransferTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/transaction.ts b/packages/core-transactions/src/handlers/transaction.ts similarity index 92% rename from packages/core-transactions/src/services/transaction.ts rename to packages/core-transactions/src/handlers/transaction.ts index ac0cc90969..8660b6136e 100644 --- a/packages/core-transactions/src/services/transaction.ts +++ b/packages/core-transactions/src/handlers/transaction.ts @@ -1,7 +1,14 @@ // tslint:disable:max-classes-per-file import { Database, EventEmitter, TransactionPool } from "@arkecosystem/core-interfaces"; -import { configManager, constants, crypto, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { + configManager, + constants, + crypto, + ITransactionData, + Transaction, + TransactionConstructor, +} from "@arkecosystem/crypto"; import { InsufficientBalanceError, @@ -10,12 +17,12 @@ import { UnexpectedMultiSignatureError, UnexpectedSecondSignatureError, } from "../errors"; -import { ITransactionService } from "../interfaces"; +import { ITransactionHandler } from "../interfaces"; const { TransactionTypes } = constants; -export abstract class TransactionService implements ITransactionService { - public abstract getType(): number; +export abstract class TransactionHandler implements ITransactionHandler { + public abstract getConstructor(): TransactionConstructor; /** * Wallet logic diff --git a/packages/core-transactions/src/services/transfer.ts b/packages/core-transactions/src/handlers/transfer.ts similarity index 75% rename from packages/core-transactions/src/services/transfer.ts rename to packages/core-transactions/src/handlers/transfer.ts index 2e75b2320d..b5fc7a3b75 100644 --- a/packages/core-transactions/src/services/transfer.ts +++ b/packages/core-transactions/src/handlers/transfer.ts @@ -1,11 +1,17 @@ import { Database, TransactionPool } from "@arkecosystem/core-interfaces"; -import { configManager, constants, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { + configManager, + ITransactionData, + Transaction, + TransactionConstructor, + TransferTransaction, +} from "@arkecosystem/crypto"; import { isRecipientOnActiveNetwork } from "../utils"; -import { TransactionService } from "./transaction"; +import { TransactionHandler } from "./transaction"; -export class TransferTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.Transfer; +export class TransferTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return TransferTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/services/vote.ts b/packages/core-transactions/src/handlers/vote.ts similarity index 87% rename from packages/core-transactions/src/services/vote.ts rename to packages/core-transactions/src/handlers/vote.ts index ac7af48b66..facfe4adf1 100644 --- a/packages/core-transactions/src/services/vote.ts +++ b/packages/core-transactions/src/handlers/vote.ts @@ -1,11 +1,11 @@ import { Database, EventEmitter, TransactionPool } from "@arkecosystem/core-interfaces"; -import { constants, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { ITransactionData, Transaction, TransactionConstructor, VoteTransaction } from "@arkecosystem/crypto"; import { AlreadyVotedError, NoVoteError, UnvoteMismatchError, VotedForNonDelegateError } from "../errors"; -import { TransactionService } from "./transaction"; +import { TransactionHandler } from "./transaction"; -export class VoteTransactionService extends TransactionService { - public getType(): number { - return constants.TransactionTypes.Vote; +export class VoteTransactionHandler extends TransactionHandler { + public getConstructor(): TransactionConstructor { + return VoteTransaction; } public canBeApplied( diff --git a/packages/core-transactions/src/index.ts b/packages/core-transactions/src/index.ts index 488f433898..4b843d599f 100644 --- a/packages/core-transactions/src/index.ts +++ b/packages/core-transactions/src/index.ts @@ -1,2 +1,7 @@ -export * from "./errors"; -export { transactionServiceRegistry as TransactionServiceRegistry } from "./service-registry"; +import * as errors from "./errors"; + +export * from "./handlers/transaction"; +export * from "./interfaces"; + +export { errors }; +export { transactionHandlerRegistry as TransactionHandlerRegistry } from "./handler-registry"; diff --git a/packages/core-transactions/src/interfaces.ts b/packages/core-transactions/src/interfaces.ts index 0c156f1ce9..0629274fa3 100644 --- a/packages/core-transactions/src/interfaces.ts +++ b/packages/core-transactions/src/interfaces.ts @@ -1,8 +1,8 @@ import { Database, EventEmitter, TransactionPool } from "@arkecosystem/core-interfaces"; -import { constants, ITransactionData, Transaction } from "@arkecosystem/crypto"; +import { ITransactionData, Transaction, TransactionConstructor } from "@arkecosystem/crypto"; -export interface ITransactionService { - getType(): constants.TransactionTypes | number; +export interface ITransactionHandler { + getConstructor(): TransactionConstructor; canBeApplied(transaction: Transaction, wallet: Database.IWallet, walletManager?: Database.IWalletManager): boolean; applyToSender(transaction: Transaction, wallet: Database.IWallet): void; diff --git a/packages/core-transactions/src/service-registry.ts b/packages/core-transactions/src/service-registry.ts deleted file mode 100644 index a9483f151d..0000000000 --- a/packages/core-transactions/src/service-registry.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { constants } from "@arkecosystem/crypto"; -import { InvalidTransactionTypeError, NotImplementedError, TransactionServiceAlreadyRegisteredError } from "./errors"; -import { transactionServices } from "./services"; -import { TransactionService } from "./services/transaction"; - -export type TransactionServiceConstructor = new () => TransactionService; - -class TransactionServiceRegistry { - private readonly coreTransactionServices = new Map(); - private readonly customTransactionServices = new Map(); - - constructor() { - transactionServices.forEach((service: TransactionServiceConstructor) => { - this.registerCoreTransactionService(service); - }); - } - - public get(type: constants.TransactionTypes): TransactionService { - if (!this.coreTransactionServices.has(type)) { - throw new InvalidTransactionTypeError(type); - } - - return this.coreTransactionServices.get(type); - } - - public registerCustomTransactionService(service: TransactionServiceConstructor): void { - throw new NotImplementedError(); - } - - public deregisterCustomTransactionService(service: TransactionServiceConstructor): void { - throw new NotImplementedError(); - } - - private registerCoreTransactionService(constructor: TransactionServiceConstructor) { - const service = new constructor(); - const type = service.getType(); - - if (this.coreTransactionServices.has(type)) { - throw new TransactionServiceAlreadyRegisteredError(type); - } - - this.coreTransactionServices.set(type, service); - } -} - -export const transactionServiceRegistry = new TransactionServiceRegistry(); diff --git a/packages/core-transactions/src/services/index.ts b/packages/core-transactions/src/services/index.ts deleted file mode 100644 index baaa5219b1..0000000000 --- a/packages/core-transactions/src/services/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DelegateRegistrationTransactionService } from "./delegate-registration"; -import { DelegateResignationTransactionService } from "./delegate-resignation"; -import { IpfsTransactionService } from "./ipfs"; -import { MultiPaymentTransactionService } from "./multi-payment"; -import { MultiSignatureTransactionService } from "./multi-signature"; -import { SecondSignatureTransactionService } from "./second-signature"; -import { TimelockTransferTransactionService } from "./timelock-transfer"; -import { TransferTransactionService } from "./transfer"; -import { VoteTransactionService } from "./vote"; - -export const transactionServices = [ - TransferTransactionService, - SecondSignatureTransactionService, - VoteTransactionService, - DelegateRegistrationTransactionService, - MultiSignatureTransactionService, - IpfsTransactionService, - TimelockTransferTransactionService, - MultiPaymentTransactionService, - DelegateResignationTransactionService, -]; diff --git a/packages/crypto/src/errors.ts b/packages/crypto/src/errors.ts index 2d4f0d6367..b21ca7464e 100644 --- a/packages/crypto/src/errors.ts +++ b/packages/crypto/src/errors.ts @@ -102,9 +102,15 @@ export class TransactionAlreadyRegisteredError extends CryptoError { } } -export class TransactionSchemaAlreadyExistsError extends CryptoError { +export class TransactionTypeInvalidRangeError extends CryptoError { + constructor(given: number) { + super(`Custom transaction type must be in the range 100-255 (${given}).`); + } +} + +export class MissingMilestoneFeeError extends CryptoError { constructor(name: string) { - super(`Schema ${name} is already registered.`); + super(`Missing milestone fee for '${name}'.`); } } @@ -130,8 +136,8 @@ export class PreviousBlockIdFormatError extends CryptoError { constructor(thisBlockHeight: number, previousBlockId: string) { super( `The config denotes that the block at height ${thisBlockHeight - 1} ` + - `must use full SHA256 block id, but the next block (at ${thisBlockHeight}) ` + - `contains previous block id "${previousBlockId}"`, + `must use full SHA256 block id, but the next block (at ${thisBlockHeight}) ` + + `contains previous block id "${previousBlockId}"`, ); } } diff --git a/packages/crypto/src/managers/config.ts b/packages/crypto/src/managers/config.ts index fe3cc75b5d..28c229e6f6 100644 --- a/packages/crypto/src/managers/config.ts +++ b/packages/crypto/src/managers/config.ts @@ -87,6 +87,7 @@ export class ConfigManager { */ public setHeight(value: number): void { this.height = value; + this.buildFees(); } /** @@ -146,8 +147,11 @@ export class ConfigManager { * Build fees from config constants. */ private buildFees(): void { - for (const type of Object.keys(TransactionTypes)) { - feeManager.set(TransactionTypes[type], this.getMilestone().fees.staticFees[camelCase(type)]); + for (const key of Object.keys(TransactionTypes)) { + const type = TransactionTypes[key]; + if (typeof type === "number") { + feeManager.set(type, this.getMilestone().fees.staticFees[camelCase(key)]); + } } } } diff --git a/packages/crypto/src/managers/fee.ts b/packages/crypto/src/managers/fee.ts index 9f4babd0a3..0003394a57 100644 --- a/packages/crypto/src/managers/fee.ts +++ b/packages/crypto/src/managers/fee.ts @@ -2,19 +2,19 @@ import { TransactionTypes } from "../constants"; import { ITransactionData } from "../transactions"; export class FeeManager { - public fees: { [key in TransactionTypes]?: number } = {}; + public fees: { [key: number]: number } = {}; /** * Set fee value based on type. */ - public set(type: TransactionTypes, value: number) { + public set(type: TransactionTypes | number, value: number) { this.fees[type] = value; } /** * Get fee value based on type. */ - public get(type: TransactionTypes): number { + public get(type: TransactionTypes | number): number { return this.fees[type]; } diff --git a/packages/crypto/src/transactions/index.ts b/packages/crypto/src/transactions/index.ts index 6f31f004e0..d2c3cad173 100644 --- a/packages/crypto/src/transactions/index.ts +++ b/packages/crypto/src/transactions/index.ts @@ -2,4 +2,4 @@ export * from "./interfaces"; export * from "./types"; export * from "./deserializers"; export * from "./serializers"; -export { transactionRegistry as TransactionRegistry } from "./registry"; +export { transactionRegistry as TransactionRegistry, TransactionConstructor } from "./registry"; diff --git a/packages/crypto/src/transactions/interfaces.ts b/packages/crypto/src/transactions/interfaces.ts index cf843001b9..fa1ba09bba 100644 --- a/packages/crypto/src/transactions/interfaces.ts +++ b/packages/crypto/src/transactions/interfaces.ts @@ -15,6 +15,7 @@ export interface ITransactionAsset { dag: string; }; payments?: any; + [custom: string]: any; } export interface IMultiSignatureAsset { diff --git a/packages/crypto/src/transactions/registry.ts b/packages/crypto/src/transactions/registry.ts index f9ba65ce1c..fa83e7b9b5 100644 --- a/packages/crypto/src/transactions/registry.ts +++ b/packages/crypto/src/transactions/registry.ts @@ -1,5 +1,13 @@ +import camelCase from "lodash/camelCase"; import { TransactionTypes } from "../constants"; -import { NotImplementedError, TransactionAlreadyRegisteredError, UnkownTransactionError } from "../errors"; +import { + MissingMilestoneFeeError, + TransactionAlreadyRegisteredError, + TransactionTypeInvalidRangeError, + UnkownTransactionError, +} from "../errors"; +import { configManager } from "../managers"; +import { feeManager } from "../managers/fee"; import { AjvWrapper } from "../validation"; import { ITransactionData } from "./interfaces"; import { @@ -15,7 +23,7 @@ import { VoteTransaction, } from "./types"; -type TransactionConstructor = typeof Transaction; +export type TransactionConstructor = typeof Transaction; class TransactionRegistry { private readonly coreTypes = new Map(); @@ -40,20 +48,56 @@ class TransactionRegistry { return instance; } - public get(type: TransactionTypes): TransactionConstructor { + public get(type: TransactionTypes | number): TransactionConstructor { if (this.coreTypes.has(type)) { return this.coreTypes.get(type); } + if (this.customTypes.has(type)) { + return this.customTypes.get(type); + } + throw new UnkownTransactionError(type); } public registerCustomType(constructor: TransactionConstructor): void { - throw new NotImplementedError(); + const { type } = constructor; + if (this.customTypes.has(type)) { + throw new TransactionAlreadyRegisteredError(constructor.name); + } + + if (type < 100) { + throw new TransactionTypeInvalidRangeError(type); + } + + this.customTypes.set(type, constructor); + this.updateSchemas(constructor); + this.updateStaticFees(); } - public deregisterCustomType(constructor: TransactionConstructor): void { - throw new NotImplementedError(); + public deregisterCustomType(type: number): void { + if (this.customTypes.has(type)) { + const schema = this.customTypes.get(type); + this.updateSchemas(schema, true); + this.customTypes.delete(type); + } + } + + public updateStaticFees(height?: number): void { + const customConstructors = Array.from(this.customTypes.values()); + const milestone = configManager.getMilestone(height); + const { staticFees } = milestone.fees; + for (const constructor of customConstructors) { + const { type, name } = constructor; + if (milestone.fees && milestone.fees.staticFees) { + const value = staticFees[camelCase(name.replace("Transaction", ""))]; + if (!value) { + throw new MissingMilestoneFeeError(name); + } + + feeManager.set(type, value); + } + } } private registerCoreType(constructor: TransactionConstructor) { @@ -66,8 +110,8 @@ class TransactionRegistry { this.updateSchemas(constructor); } - private updateSchemas(transaction: TransactionConstructor) { - AjvWrapper.extendTransaction(transaction.getSchema()); + private updateSchemas(transaction: TransactionConstructor, remove?: boolean) { + AjvWrapper.extendTransaction(transaction.getSchema(), remove); } } diff --git a/packages/crypto/src/transactions/types/index.ts b/packages/crypto/src/transactions/types/index.ts index 8dd8c00a1b..e2dfe4ae6b 100644 --- a/packages/crypto/src/transactions/types/index.ts +++ b/packages/crypto/src/transactions/types/index.ts @@ -8,3 +8,6 @@ export * from "./ipfs"; export * from "./timelock-transfer"; export * from "./multi-payment"; export * from "./delegate-resignation"; + +import * as schemas from "./schemas"; +export { schemas }; diff --git a/packages/crypto/src/transactions/types/schemas.ts b/packages/crypto/src/transactions/types/schemas.ts index 8e3835eaa3..0fb165ce91 100644 --- a/packages/crypto/src/transactions/types/schemas.ts +++ b/packages/crypto/src/transactions/types/schemas.ts @@ -1,7 +1,7 @@ import deepmerge = require("deepmerge"); import { TransactionTypes } from "../../constants"; -const extend = (parent, properties): TransactionSchema => { +export const extend = (parent, properties): TransactionSchema => { return deepmerge(parent, properties); }; @@ -20,7 +20,7 @@ export const strictSchema = (schema: TransactionSchema): TransactionSchema => { return strict; }; -const transactionBaseSchema = { +export const transactionBaseSchema = { $id: null, type: "object", required: ["type", "senderPublicKey", "fee", "timestamp"], diff --git a/packages/crypto/src/transactions/types/transaction.ts b/packages/crypto/src/transactions/types/transaction.ts index 3155f9c346..7a8ff4346b 100644 --- a/packages/crypto/src/transactions/types/transaction.ts +++ b/packages/crypto/src/transactions/types/transaction.ts @@ -98,7 +98,7 @@ export abstract class Transaction { return true; } - if (data.type >= 4) { + if (data.type >= 4 && data.type <= 99) { return false; } diff --git a/packages/crypto/src/validation/ajv-wrapper.ts b/packages/crypto/src/validation/ajv-wrapper.ts index 9ae6d7ab32..742b033c70 100644 --- a/packages/crypto/src/validation/ajv-wrapper.ts +++ b/packages/crypto/src/validation/ajv-wrapper.ts @@ -1,7 +1,6 @@ import Ajv from "ajv"; import ajvKeywords from "ajv-keywords"; -import { TransactionSchemaAlreadyExistsError } from "../errors"; import { ISchemaValidationResult } from "../transactions/interfaces"; import { signedSchema, strictSchema, TransactionSchema } from "../transactions/types/schemas"; import { formats } from "./formats"; @@ -37,16 +36,19 @@ class AjvWrapper { return { value: data, error }; } - public extendTransaction(schema: TransactionSchema) { - if (this.transactionSchemas.has(schema.$id)) { - throw new TransactionSchemaAlreadyExistsError(schema.$id); + public extendTransaction(schema: TransactionSchema, remove?: boolean) { + if (remove) { + this.transactionSchemas.delete(schema.$id); + this.ajv.removeSchema(schema.$id); + this.ajv.removeSchema(`${schema.$id}Signed`); + this.ajv.removeSchema(`${schema.$id}Strict`); + } else { + this.transactionSchemas.add(schema.$id); + this.ajv.addSchema(schema); + this.ajv.addSchema(signedSchema(schema)); + this.ajv.addSchema(strictSchema(schema)); } - this.transactionSchemas.add(schema.$id); - this.ajv.addSchema(schema); - this.ajv.addSchema(signedSchema(schema)); - this.ajv.addSchema(strictSchema(schema)); - this.updateTransactionArray(); }