Skip to content

Commit

Permalink
refactor: custom transaction types (#2247)
Browse files Browse the repository at this point in the history
  • Loading branch information
spkjp authored and faustbrian committed Mar 16, 2019
1 parent fca6888 commit eaf7bcd
Show file tree
Hide file tree
Showing 41 changed files with 616 additions and 299 deletions.
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

0 comments on commit eaf7bcd

Please sign in to comment.