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: custom transaction types #2247

Merged
merged 16 commits into from
Mar 16, 2019
16 changes: 8 additions & 8 deletions __tests__/integration/core-transaction-pool/guard.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions __tests__/unit/core-database/database-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -278,7 +278,7 @@ describe("Database Service", () => {
expect(restoredDelegatesRound2[i].publicKey).toBe(delegatesRound2[i].publicKey);
}

transactionService.canBeApplied = originalApply;
transactionHandler.canBeApplied = originalApply;
});
});
});
20 changes: 10 additions & 10 deletions __tests__/unit/core-transaction-pool/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
159 changes: 159 additions & 0 deletions __tests__/unit/core-transactions/handler-registry.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
Loading