diff --git a/core-graphql/.gitattributes b/core-graphql/.gitattributes
new file mode 100644
index 0000000..63f6a5b
--- /dev/null
+++ b/core-graphql/.gitattributes
@@ -0,0 +1,7 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/README.md export-ignore
diff --git a/core-graphql/README.md b/core-graphql/README.md
new file mode 100644
index 0000000..bc17713
--- /dev/null
+++ b/core-graphql/README.md
@@ -0,0 +1,28 @@
+# ARK Core - GraphQL
+
+
+
+
+
+## Deprecated
+
+Note that this plugin is deprecated and should no longer be used.
+
+## Documentation
+
+You can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://docs.ark.io/guidebook/core/plugins/deprecated/core-graphql.html).
+
+## Security
+
+If you discover a security vulnerability within this package, please send an e-mail to security@ark.io. All security vulnerabilities will be promptly addressed.
+
+## Credits
+
+- [Brian Faust](https://github.com/faustbrian)
+- [Joshua Noack](https://github.com/supaiku0)
+- [Lúcio Rubens](https://github.com/luciorubeens)
+- [All Contributors](../../../../contributors)
+
+## License
+
+[MIT](LICENSE) © [ARK Ecosystem](https://ark.io)
diff --git a/core-graphql/__tests__/__support__/setup.ts b/core-graphql/__tests__/__support__/setup.ts
new file mode 100644
index 0000000..e2e1c50
--- /dev/null
+++ b/core-graphql/__tests__/__support__/setup.ts
@@ -0,0 +1,30 @@
+import { app } from "@arkecosystem/core-container";
+import { registerWithContainer, setUpContainer } from "../../../core-test-utils/src/helpers/container";
+
+jest.setTimeout(60000);
+
+const options = {
+ enabled: true,
+ host: "0.0.0.0",
+ port: 4005,
+};
+
+export const setUp = async () => {
+ process.env.CORE_GRAPHQL_ENABLED = "true";
+
+ await setUpContainer({
+ exclude: ["@arkecosystem/core-api", "@arkecosystem/core-forger"],
+ });
+
+ const { plugin } = require("../../src");
+ await registerWithContainer(plugin, options);
+
+ return app;
+};
+
+export const tearDown = async () => {
+ await app.tearDown();
+
+ const { plugin } = require("../../src");
+ await plugin.deregister(app, options);
+};
diff --git a/core-graphql/__tests__/__support__/utils.ts b/core-graphql/__tests__/__support__/utils.ts
new file mode 100644
index 0000000..c968552
--- /dev/null
+++ b/core-graphql/__tests__/__support__/utils.ts
@@ -0,0 +1,16 @@
+import { app } from "@arkecosystem/core-container";
+import { ApiHelpers } from "../../../core-test-utils/src/helpers/api";
+
+class Helpers {
+ public async request(query) {
+ const url = "http://localhost:4005/graphql";
+ const server = app.resolvePlugin("graphql");
+
+ return ApiHelpers.request(server, "POST", url, {}, { query });
+ }
+}
+
+/**
+ * @type {Helpers}
+ */
+export const utils = new Helpers();
diff --git a/core-graphql/__tests__/api/address.test.ts b/core-graphql/__tests__/api/address.test.ts
new file mode 100644
index 0000000..0a3335b
--- /dev/null
+++ b/core-graphql/__tests__/api/address.test.ts
@@ -0,0 +1,40 @@
+import "@arkecosystem/core-test-utils";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { address }", () => {
+ describe("GraphQL resolver for Address", () => {
+ it("should get wallter for a correctly formatted Address", async () => {
+ const query = '{ wallet(address: "APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn") { producedBlocks } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallet).toBeObject();
+
+ expect(data.wallet.producedBlocks).toBe(0);
+ });
+ it("should return an error for an incorrectly formatted Address", async () => {
+ const query = '{ wallet(address: "bad address") { producedBlocks } }';
+ const response = await utils.request(query);
+
+ expect(response).not.toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeFalsy();
+ expect(response.data.errors[0]).toBeObject();
+ expect(response.data.errors[0].message).not.toBeNull();
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/block.test.ts b/core-graphql/__tests__/api/block.test.ts
new file mode 100644
index 0000000..0ee445b
--- /dev/null
+++ b/core-graphql/__tests__/api/block.test.ts
@@ -0,0 +1,29 @@
+import "@arkecosystem/core-test-utils";
+import genesisBlock from "../../../core-test-utils/src/config/testnet/genesisBlock.json";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { block }", () => {
+ describe("GraphQL queries for Block", () => {
+ it("should get a block by its id", async () => {
+ const query = `{ block(id:"${genesisBlock.id}") { id } }`;
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.block).toBeObject();
+ expect(data.block.id).toBe(genesisBlock.id);
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/blocks.test.ts b/core-graphql/__tests__/api/blocks.test.ts
new file mode 100644
index 0000000..1403cdd
--- /dev/null
+++ b/core-graphql/__tests__/api/blocks.test.ts
@@ -0,0 +1,54 @@
+import "@arkecosystem/core-test-utils";
+import genesisBlock from "../../../core-test-utils/src/config/testnet/genesisBlock.json";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { blocks }", () => {
+ describe("GraphQL queries for Blocks - filter by generatorPublicKey", () => {
+ it("should get blocks by generatorPublicKey", async () => {
+ const query = `{ blocks(filter: { generatorPublicKey: "${genesisBlock.generatorPublicKey}" }) { id } }`;
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.blocks).toEqual([{ id: genesisBlock.id }]);
+ });
+ });
+
+ describe("GraphQL queries for Blocks - testing relationships", () => {
+ it("should verify that relationships are valid", async () => {
+ const query = "{ blocks(limit: 1) { generator { address } } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.blocks[0].generator.address).toEqual("AP6kAVdX1zQ3S8mfDnnHx9GaAohEqQUins");
+ });
+ });
+
+ describe("GraphQL queries for Blocks - testing api errors", () => {
+ it("should not be a successful query", async () => {
+ const query = "{ blocks(filter: { vers } ) { id } }";
+ const response = await utils.request(query);
+
+ expect(response).not.toBeSuccessfulResponse();
+
+ const error = response.data.errors;
+ expect(error).toBeArray();
+ expect(response.status).toEqual(400);
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/transaction.test.ts b/core-graphql/__tests__/api/transaction.test.ts
new file mode 100644
index 0000000..68d3d62
--- /dev/null
+++ b/core-graphql/__tests__/api/transaction.test.ts
@@ -0,0 +1,29 @@
+import "@arkecosystem/core-test-utils";
+import genesisBlock from "../../../core-test-utils/src/config/testnet/genesisBlock.json";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(async () => {
+ await tearDown();
+});
+
+describe("GraphQL API { transaction }", () => {
+ describe("GraphQL queries for Transaction", () => {
+ it("should get a transaction by its id", async () => {
+ const query = `{ transaction(id:"${genesisBlock.transactions[0].id}") { id } }`;
+ const response = await utils.request(query);
+
+ await expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transaction).toBeObject();
+ expect(data.transaction.id).toBe(genesisBlock.transactions[0].id);
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/transactions.test.ts b/core-graphql/__tests__/api/transactions.test.ts
new file mode 100644
index 0000000..6ed7807
--- /dev/null
+++ b/core-graphql/__tests__/api/transactions.test.ts
@@ -0,0 +1,174 @@
+import "@arkecosystem/core-test-utils";
+import genesisBlock from "../../../core-test-utils/src/config/testnet/genesisBlock.json";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { transactions }", () => {
+ describe("GraphQL queries for Transactions - all", () => {
+ it("should get 100 transactions", async () => {
+ const query = "{ transactions { id } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions.length).toBe(100);
+ });
+ });
+
+ describe("GraphQL queries for Transactions - orderBy", () => {
+ it("should get 100 transactionsin ascending order of their id", async () => {
+ const query = '{ transactions(orderBy: { field: "id", direction: ASC }) { id } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions.length).toBe(100);
+ expect(data.transactions.sort((a, b) => (+a <= +b ? -1 : 0))).toEqual(data.transactions);
+ });
+ });
+
+ describe("GraphQL queries for Transactions - filter by fee", () => {
+ it("should get all transactions with fee = 0", async () => {
+ const query = "{ transactions(filter: { fee: 0 }) { id } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions.length).toBe(100); // because of default limit = 100
+ });
+
+ it("should get no transaction with fee = 987", async () => {
+ const query = "{ transactions(filter: { fee: 987 }) { id } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions.length).toBe(0);
+ });
+ });
+
+ describe("GraphQL queries for Transactions - filter by blockId", () => {
+ it("should get transactions for given blockId", async () => {
+ const query = `{ transactions(filter: { blockId: "${genesisBlock.id}" }) { id } }`;
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+
+ const genesisBlockTransactionIds = genesisBlock.transactions.map(transaction => transaction.id);
+ data.transactions.forEach(transaction => {
+ expect(genesisBlockTransactionIds).toContain(transaction.id);
+ });
+ });
+ });
+
+ describe("GraphQL queries for Transactions - filter by senderPublicKey", () => {
+ it("should get transactions for given senderPublicKey", async () => {
+ const query = `{ transactions(filter: { senderPublicKey: "${
+ genesisBlock.transactions[0].senderPublicKey
+ }" }) { id } }`;
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ // tslint:disable-next-line:max-line-length
+ expect(data.transactions.length).toEqual(51); // number of outgoing transactions for the 0th transaction's sender address
+
+ const genesisBlockTransactionIds = genesisBlock.transactions.map(transaction => transaction.id);
+
+ data.transactions.forEach(transaction => {
+ expect(genesisBlockTransactionIds).toContain(transaction.id);
+ });
+ });
+ });
+
+ describe("GraphQL queries for Transactions - filter by recipientId", () => {
+ it("should get transactions for given recipientId", async () => {
+ const query = '{ transactions(filter: { recipientId: "AHXtmB84sTZ9Zd35h9Y1vfFvPE2Xzqj8ri" }) { id } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+
+ expect(data.transactions.length).toBe(2);
+ });
+ });
+
+ describe("GraphQL queries for Transactions - filter by type", () => {
+ it("should get transactions for given type", async () => {
+ const query = "{ transactions(filter: { type: TRANSFER } ) { type } }";
+ const response = await utils.request(query);
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+
+ data.transactions.forEach(tx => {
+ expect(tx.type).toBe(Number(0));
+ });
+ });
+ });
+
+ describe("GraphQL queries for Transactions - using orderBy, limit", () => {
+ it("should get 5 transactions in order of ASCending address", async () => {
+ const query = '{ transactions(orderBy: { field: "id", direction: ASC }, limit: 5 ) { id } }';
+ const response = await utils.request(query);
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions.length).toBe(5);
+
+ expect(parseInt(data.transactions[0].id, 16)).toBeLessThan(parseInt(data.transactions[1].id, 16));
+ });
+ });
+
+ describe("GraphQL queries for Transactions - testing relationships", () => {
+ it("should verify that relationships are valid", async () => {
+ const query = "{ transactions(limit: 1) { recipient { address } } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.transactions[0].recipient.address).not.toBeNull();
+ });
+ });
+
+ describe("GraphQL queries for Transactions - testing api errors", () => {
+ it("should not be a successful query", async () => {
+ const query = "{ transaction(filter: { vers } ) { id } }";
+ const response = await utils.request(query);
+
+ expect(response).not.toBeSuccessfulResponse();
+
+ const error = response.data.errors;
+ expect(error).toBeArray();
+ expect(response.status).toEqual(400);
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/wallet.test.ts b/core-graphql/__tests__/api/wallet.test.ts
new file mode 100644
index 0000000..f0a12e6
--- /dev/null
+++ b/core-graphql/__tests__/api/wallet.test.ts
@@ -0,0 +1,29 @@
+import "@arkecosystem/core-test-utils";
+import genesisBlock from "../../../core-test-utils/src/config/testnet/genesisBlock.json";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { wallet }", () => {
+ describe("GraphQL queries for Wallet", () => {
+ it("should get a wallet by address", async () => {
+ const query = `{ wallet(address:"${genesisBlock.transactions[0].senderId}") { address } }`;
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallet).toBeObject();
+ expect(data.wallet.address).toBe(genesisBlock.transactions[0].senderId);
+ });
+ });
+});
diff --git a/core-graphql/__tests__/api/wallets.test.ts b/core-graphql/__tests__/api/wallets.test.ts
new file mode 100644
index 0000000..cb1dc40
--- /dev/null
+++ b/core-graphql/__tests__/api/wallets.test.ts
@@ -0,0 +1,93 @@
+/* tslint:disable:max-line-length */
+
+import "@arkecosystem/core-test-utils";
+
+import { setUp, tearDown } from "../__support__/setup";
+import { utils } from "../__support__/utils";
+
+beforeAll(async () => {
+ await setUp();
+});
+
+afterAll(() => {
+ tearDown();
+});
+
+describe("GraphQL API { wallets }", () => {
+ describe("GraphQL queries for Wallets - get all", () => {
+ it("should get all wallets", async () => {
+ const query = "{ wallets { address } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallets.length).toBe(53);
+ // TODO why 53 ? From genesis block I can count 52, but there is an additional "AP6kAVdX1zQ3S8mfDnnHx9GaAohEqQUins" wallet. What did I miss ?
+ });
+ });
+
+ describe("GraphQL queries for Wallets - filter by vote", () => {
+ it("should get all wallets with specific vote", async () => {
+ const query =
+ '{ wallets(filter: { vote: "036f612457adc81041662e664ca4ae64f844b412065f2b7d2f9f7d305e59c908cd" }) { address } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallets.length).toBe(1);
+ });
+
+ it("should get no wallet with unknown vote", async () => {
+ const query = '{ wallets(filter: { vote: "unknownPublicKey" }) { address } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallets.length).toBe(0);
+ });
+ });
+
+ describe("GraphQL queries for Wallets - using orderBy, limit", () => {
+ it("should get 5 wallets in order of ASCending address", async () => {
+ const query = '{ wallets(orderBy: { field: "address", direction: ASC }, limit: 5 ) { address } }';
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ const data = response.data.data;
+ expect(data).toBeObject();
+ expect(data.wallets.length).toBe(5);
+ expect(data.wallets.sort((a, b) => (+a <= +b ? -1 : 0))).toEqual(data.wallets);
+ });
+ });
+
+ describe("GraphQL queries for Wallets - testing relationships", () => {
+ it("should verify that relationships are valid", async () => {
+ const query = "{ wallets(limit: 1) { transactions { id } } }";
+ const response = await utils.request(query);
+
+ expect(response).toBeSuccessfulResponse();
+
+ expect(response.data.errors[0]).toBeObject(); // relationships doesn't function well (unimplemented)
+ });
+ });
+
+ describe("GraphQL queries for Wallets - testing api errors", () => {
+ it("should not be a successful query", async () => {
+ const query = "{ wallets(filter: { vers } ) { address } }";
+ const response = await utils.request(query);
+
+ expect(response).not.toBeSuccessfulResponse();
+
+ const error = response.data.errors;
+ expect(error).toBeArray();
+ expect(response.status).toEqual(400);
+ });
+ });
+});
diff --git a/core-graphql/package.json b/core-graphql/package.json
new file mode 100644
index 0000000..d80f94d
--- /dev/null
+++ b/core-graphql/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@arkecosystem/core-graphql",
+ "version": "2.2.2",
+ "description": "GraphQL Integration for ARK Core",
+ "license": "MIT",
+ "contributors": [
+ "Lúcio Rubens "
+ ],
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "yarn clean && yarn compile",
+ "build:watch": "yarn clean && yarn compile -w",
+ "clean": "del dist",
+ "compile": "../../node_modules/typescript/bin/tsc",
+ "prepublishOnly": "yarn build",
+ "test": "cross-env CORE_ENV=test jest --runInBand --forceExit",
+ "test:coverage": "cross-env CORE_ENV=test jest --coverage --coveragePathIgnorePatterns='/(defaults.ts|index.ts)$' --runInBand --forceExit",
+ "test:debug": "cross-env CORE_ENV=test node --inspect-brk ../../node_modules/.bin/jest --runInBand",
+ "test:watch": "cross-env CORE_ENV=test jest --runInBand --watch",
+ "test:watch:all": "cross-env CORE_ENV=test jest --runInBand --watchAll"
+ },
+ "dependencies": {
+ "@arkecosystem/core-container": "^2.2.2",
+ "@arkecosystem/core-http-utils": "^2.2.2",
+ "@arkecosystem/core-interfaces": "^2.2.2",
+ "@arkecosystem/crypto": "^2.2.2",
+ "apollo-server-hapi": "^2.4.0",
+ "graphql-tools-types": "^1.2.1"
+ },
+ "devDependencies": {},
+ "engines": {
+ "node": ">=10.x"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/core-graphql/src/apollo-server.ts b/core-graphql/src/apollo-server.ts
new file mode 100644
index 0000000..0e76dfa
--- /dev/null
+++ b/core-graphql/src/apollo-server.ts
@@ -0,0 +1,11 @@
+import { ApolloServer } from "apollo-server-hapi";
+import { typeDefs } from "./defs";
+import { resolvers } from "./resolvers";
+
+/**
+ * Schema used by the Apollo GraphQL plugin for the hapi.js server.
+ */
+export const apolloServer = new ApolloServer({
+ typeDefs,
+ resolvers,
+});
diff --git a/core-graphql/src/defaults.ts b/core-graphql/src/defaults.ts
new file mode 100644
index 0000000..a6ee749
--- /dev/null
+++ b/core-graphql/src/defaults.ts
@@ -0,0 +1,6 @@
+export const defaults = {
+ enabled: false,
+ host: process.env.CORE_GRAPHQL_HOST || "0.0.0.0",
+ port: process.env.CORE_GRAPHQL_PORT || 4005,
+ path: "/graphql",
+};
diff --git a/core-graphql/src/defs/index.ts b/core-graphql/src/defs/index.ts
new file mode 100644
index 0000000..de6af74
--- /dev/null
+++ b/core-graphql/src/defs/index.ts
@@ -0,0 +1,9 @@
+import { inputs } from "./inputs";
+import { root } from "./root";
+import { types } from "./types";
+
+export const typeDefs = `
+ ${inputs}
+ ${root}
+ ${types}
+`;
diff --git a/core-graphql/src/defs/inputs.ts b/core-graphql/src/defs/inputs.ts
new file mode 100644
index 0000000..d6e0872
--- /dev/null
+++ b/core-graphql/src/defs/inputs.ts
@@ -0,0 +1,44 @@
+export const inputs = `
+ scalar JSON
+ scalar Limit
+ scalar Offset
+ scalar Address
+
+ enum OrderDirection {
+ ASC
+ DESC
+ }
+
+ enum TransactionType {
+ TRANSFER,
+ SECOND_SIGNATURE,
+ DELEGATE,
+ VOTE,
+ MULTI_SIGNATURE,
+ IPFS,
+ TIMELOCK_TRANSFER,
+ MULTI_PAYMENT,
+ DELEGATE_RESIGNATION
+ }
+
+ input TransactionFilter {
+ fee: Float
+ blockId: String
+ senderPublicKey: String
+ recipientId: String
+ type: TransactionType
+ }
+
+ input BlockFilter {
+ generatorPublicKey: String
+ }
+
+ input WalletFilter {
+ vote: String
+ }
+
+ input OrderByInput {
+ field: String
+ direction: OrderDirection
+ }
+`;
diff --git a/core-graphql/src/defs/root.ts b/core-graphql/src/defs/root.ts
new file mode 100644
index 0000000..25b2243
--- /dev/null
+++ b/core-graphql/src/defs/root.ts
@@ -0,0 +1,14 @@
+export const root = `
+ type Query {
+ block(id: String): Block
+ blocks(limit: Limit, offset: Offset, orderBy: OrderByInput, filter: BlockFilter): [Block]
+ transaction(id: String): Transaction
+ transactions(limit: Limit, orderBy: OrderByInput, filter: TransactionFilter): [Transaction]
+ wallet(address: Address, publicKey: String, username: String): Wallet
+ wallets(limit: Limit, orderBy: OrderByInput, filter: WalletFilter): [Wallet]
+ }
+
+ schema {
+ query: Query
+ }
+`;
diff --git a/core-graphql/src/defs/types.ts b/core-graphql/src/defs/types.ts
new file mode 100644
index 0000000..a9197aa
--- /dev/null
+++ b/core-graphql/src/defs/types.ts
@@ -0,0 +1,48 @@
+export const types = `
+ type Block {
+ id: String
+ version: Int!
+ timestamp: Int!
+ previousBlock: String
+ height: Int!
+ numberOfTransactions: Int!
+ totalAmount: Float
+ totalFee: Float
+ reward: Float
+ payloadLength: Int!
+ payloadHash: String
+ generatorPublicKey: String
+ blockSignature: String
+ transactions(limit: Limit, offset: Offset, orderBy: OrderByInput, filter: TransactionFilter): [Transaction]
+ generator: Wallet
+ }
+
+ type Transaction {
+ id: String
+ version: Int!
+ timestamp: Int!
+ senderPublicKey: String
+ recipientId: Address
+ type: Int!
+ vendorField: String
+ amount: Float
+ fee: Float
+ signature: String
+ block: Block
+ recipient: Wallet
+ sender: Wallet
+ }
+
+ type Wallet {
+ address: Address
+ publicKey: String
+ secondPublicKey: String
+ vote: String
+ username: String
+ balance: Float
+ voteBalance: Float
+ producedBlocks: Float
+ transactions(limit: Limit, offset: Offset, orderBy: OrderByInput): [Transaction]
+ blocks(limit: Limit, offset: Offset, orderBy: OrderByInput): [Block]
+ }
+`;
diff --git a/core-graphql/src/helpers/format-orderBy.ts b/core-graphql/src/helpers/format-orderBy.ts
new file mode 100644
index 0000000..61b3255
--- /dev/null
+++ b/core-graphql/src/helpers/format-orderBy.ts
@@ -0,0 +1,15 @@
+/**
+ * Logic used by our orderBy input
+ * @param {Object} parameter
+ * @param {String} defaultValue
+ * @return {String}
+ */
+export function formatOrderBy(parameter, defaultValue) {
+ let order;
+
+ if (parameter) {
+ order = `${parameter.field}:${parameter.direction.toLowerCase()}`;
+ }
+
+ return order || defaultValue;
+}
diff --git a/core-graphql/src/helpers/index.ts b/core-graphql/src/helpers/index.ts
new file mode 100644
index 0000000..d667ee2
--- /dev/null
+++ b/core-graphql/src/helpers/index.ts
@@ -0,0 +1,4 @@
+import { formatOrderBy } from "./format-orderBy";
+import { unserializeTransactions } from "./unserialize-transactions";
+
+export { formatOrderBy, unserializeTransactions };
diff --git a/core-graphql/src/helpers/unserialize-transactions.ts b/core-graphql/src/helpers/unserialize-transactions.ts
new file mode 100644
index 0000000..36ad005
--- /dev/null
+++ b/core-graphql/src/helpers/unserialize-transactions.ts
@@ -0,0 +1,19 @@
+import { Transaction } from "@arkecosystem/crypto";
+
+/**
+ * Deserialize multiple transactions
+ */
+export async function unserializeTransactions(data) {
+ const deserialize = buffer => {
+ return Transaction.fromBytes(buffer);
+ };
+
+ if (Array.isArray(data)) {
+ return data.reduce((total, value, key) => {
+ total.push(deserialize(value.serialized));
+
+ return total;
+ }, []);
+ }
+ return deserialize(data);
+}
diff --git a/core-graphql/src/index.ts b/core-graphql/src/index.ts
new file mode 100644
index 0000000..8989a6c
--- /dev/null
+++ b/core-graphql/src/index.ts
@@ -0,0 +1,28 @@
+import { Container, Logger } from "@arkecosystem/core-interfaces";
+import { defaults } from "./defaults";
+import { startServer } from "./server";
+
+/**
+ * The struct used by the plugin manager.
+ * @type {Object}
+ */
+export const plugin: Container.PluginDescriptor = {
+ pkg: require("../package.json"),
+ defaults,
+ alias: "graphql",
+ async register(container: Container.IContainer, options) {
+ if (!options.enabled) {
+ container.resolvePlugin("logger").info("GraphQL API is disabled");
+ return;
+ }
+
+ return startServer(options);
+ },
+ async deregister(container: Container.IContainer, options) {
+ if (options.enabled) {
+ container.resolvePlugin("logger").info("Stopping GraphQL API");
+
+ return container.resolvePlugin("graphql").stop();
+ }
+ },
+};
diff --git a/core-graphql/src/repositories/blocks.ts b/core-graphql/src/repositories/blocks.ts
new file mode 100644
index 0000000..f1e12c6
--- /dev/null
+++ b/core-graphql/src/repositories/blocks.ts
@@ -0,0 +1,133 @@
+import { Repository } from "./repository";
+import { buildFilterQuery } from "./utils/filter-query";
+
+class BlocksRepository extends Repository {
+ /**
+ * Get all blocks for the given parameters.
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAll(parameters: any = {}) {
+ const selectQuery = this.query.select().from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ const applyConditions = queries => {
+ const conditions = Object.entries(this._formatConditions(parameters));
+
+ if (conditions.length) {
+ const first = conditions.shift();
+
+ for (const item of queries) {
+ item.where(this.query[first[0]].equals(first[1]));
+
+ for (const condition of conditions) {
+ item.and(this.query[condition[0]].equals(condition[1]));
+ }
+ }
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ return this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit || 100,
+ offset: parameters.offset || 0,
+ orderBy: this.__orderBy(parameters),
+ });
+ }
+
+ /**
+ * Get all blocks for the given generator.
+ * @param {String} generatorPublicKey
+ * @param {Object} paginator
+ * @return {Object}
+ */
+ public async findAllByGenerator(generatorPublicKey, paginator) {
+ return this.findAll({ ...{ generatorPublicKey }, ...paginator });
+ }
+
+ /**
+ * Get a block.
+ * @param {Number} id
+ * @return {Object}
+ */
+ public async findById(id) {
+ const query = this.query
+ .select()
+ .from(this.query)
+ .where(this.query.id.equals(id));
+
+ return this._find(query);
+ }
+
+ /**
+ * Get the last block for the given generator.
+ * TODO is this right?
+ * @param {String} generatorPublicKey
+ * @return {Object}
+ */
+ public async findLastByPublicKey(generatorPublicKey) {
+ const query = this.query
+ .select(this.query.id, this.query.timestamp)
+ .from(this.query)
+ .where(this.query.generator_public_key.equals(generatorPublicKey))
+ .order(this.query.height.desc);
+
+ return this._find(query);
+ }
+
+ /**
+ * Search all blocks.
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async search(parameters) {
+ const selectQuery = this.query.select().from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ const applyConditions = queries => {
+ const conditions = buildFilterQuery(this._formatConditions(parameters), {
+ exact: ["id", "version", "previous_block", "payload_hash", "generator_public_key", "block_signature"],
+ between: [
+ "timestamp",
+ "height",
+ "number_of_transactions",
+ "total_amount",
+ "total_fee",
+ "reward",
+ "payload_length",
+ ],
+ });
+
+ if (conditions.length) {
+ const first = conditions.shift();
+
+ for (const item of queries) {
+ item.where(this.query[first.column][first.method](first.value));
+
+ for (const condition of conditions) {
+ item.and(this.query[condition.column][condition.method](condition.value));
+ }
+ }
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ return this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit,
+ offset: parameters.offset,
+ orderBy: this.__orderBy(parameters),
+ });
+ }
+
+ public getModel() {
+ return (this.databaseService.connection as any).models.block;
+ }
+
+ public __orderBy(parameters) {
+ return parameters.orderBy ? parameters.orderBy.split(":").map(p => p.toLowerCase()) : ["height", "desc"];
+ }
+}
+
+export const blockRepository = new BlocksRepository();
diff --git a/core-graphql/src/repositories/index.ts b/core-graphql/src/repositories/index.ts
new file mode 100644
index 0000000..d747df1
--- /dev/null
+++ b/core-graphql/src/repositories/index.ts
@@ -0,0 +1,4 @@
+import { blockRepository } from "./blocks";
+import { transactionRepository } from "./transactions";
+
+export { blockRepository, transactionRepository };
diff --git a/core-graphql/src/repositories/repository.ts b/core-graphql/src/repositories/repository.ts
new file mode 100644
index 0000000..2f5b554
--- /dev/null
+++ b/core-graphql/src/repositories/repository.ts
@@ -0,0 +1,65 @@
+import { app } from "@arkecosystem/core-container";
+import { Database, TransactionPool } from "@arkecosystem/core-interfaces";
+
+export abstract class Repository {
+ protected readonly databaseService = app.resolvePlugin("database");
+ protected readonly transactionPool = app.resolvePlugin("transaction-pool");
+ protected readonly cache = this.databaseService.cache;
+ protected readonly model = this.getModel();
+ protected readonly query = this.model.query();
+
+ public abstract getModel(): Database.IModel;
+
+ public async _find(query) {
+ return (this.databaseService.connection as any).query.oneOrNone(query.toQuery());
+ }
+
+ public async _findMany(query) {
+ return (this.databaseService.connection as any).query.manyOrNone(query.toQuery());
+ }
+
+ public async _findManyWithCount(selectQuery, countQuery, { limit, offset, orderBy }) {
+ const { count } = await this._find(countQuery);
+
+ selectQuery
+ .order(this.query[orderBy[0]][orderBy[1]])
+ .offset(offset)
+ .limit(limit);
+
+ limit = 100;
+ offset = 0;
+ const rows = await this._findMany(selectQuery);
+ return {
+ rows,
+ count: +count,
+ };
+ }
+
+ public _makeCountQuery() {
+ return this.query.select("count(*) AS count").from(this.query);
+ }
+
+ public _makeEstimateQuery() {
+ return this.query.select("count(*) AS count").from(`${this.model.getTable()} TABLESAMPLE SYSTEM (100)`);
+ }
+
+ public _formatConditions(parameters) {
+ const columns = this.model.getColumnSet().columns.map(column => ({
+ name: column.name,
+ prop: column.prop || column.name,
+ }));
+
+ const columnNames = columns.map(column => column.name);
+ const columnProps = columns.map(column => column.prop);
+
+ const filter = args => args.filter(arg => columnNames.includes(arg) || columnProps.includes(arg));
+
+ return filter(Object.keys(parameters)).reduce((items, item) => {
+ const columnName = columns.find(column => column.prop === item).name;
+
+ items[columnName] = parameters[item];
+
+ return items;
+ }, {});
+ }
+}
diff --git a/core-graphql/src/repositories/transactions.ts b/core-graphql/src/repositories/transactions.ts
new file mode 100644
index 0000000..c525317
--- /dev/null
+++ b/core-graphql/src/repositories/transactions.ts
@@ -0,0 +1,438 @@
+import { constants, slots } from "@arkecosystem/crypto";
+import { dato } from "@faustbrian/dato";
+import { Repository } from "./repository";
+import { buildFilterQuery } from "./utils/filter-query";
+
+const { TransactionType } = constants;
+
+class TransactionsRepository extends Repository {
+ /**
+ * Get all transactions.
+ * @param {Object} params
+ * @return {Object}
+ */
+ public async findAll(parameters: any = {}) {
+ const selectQuery = this.query.select().from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ if (parameters.senderId) {
+ const senderPublicKey = this.__publicKeyFromSenderId(parameters.senderId);
+
+ if (!senderPublicKey) {
+ return { rows: [], count: 0 };
+ }
+
+ parameters.senderPublicKey = senderPublicKey;
+ }
+
+ if (parameters.type) {
+ parameters.type = TransactionType[parameters.type];
+ }
+
+ const applyConditions = queries => {
+ const conditions = Object.entries(this._formatConditions(parameters));
+
+ if (conditions.length) {
+ const first = conditions.shift();
+
+ for (const item of queries) {
+ item.where(this.query[first[0]].equals(first[1]));
+
+ for (const condition of conditions) {
+ item.and(this.query[condition[0]].equals(condition[1]));
+ }
+ }
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ const results = await this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit || 100,
+ offset: parameters.offset || 0,
+ orderBy: this.__orderBy(parameters),
+ });
+
+ results.rows = await this.__mapBlocksToTransactions(results.rows);
+ return results;
+ }
+
+ /**
+ * Get all transactions (LEGACY, for V1 only).
+ * @param {Object} params
+ * @return {Object}
+ */
+ public async findAllLegacy(parameters: any = {}) {
+ const selectQuery = this.query.select(this.query.block_id, this.query.serialized).from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ if (parameters.senderId) {
+ parameters.senderPublicKey = this.__publicKeyFromSenderId(parameters.senderId);
+ }
+
+ const applyConditions = queries => {
+ const conditions = Object.entries(this._formatConditions(parameters));
+
+ if (conditions.length) {
+ const first = conditions.shift();
+
+ for (const item of queries) {
+ item.where(this.query[first[0]].equals(first[1]));
+
+ for (const [key, value] of conditions) {
+ item.or(this.query[key].equals(value));
+ }
+ }
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ const results = await this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit,
+ offset: parameters.offset,
+ orderBy: this.__orderBy(parameters),
+ });
+
+ results.rows = await this.__mapBlocksToTransactions(results.rows);
+
+ return results;
+ }
+
+ /**
+ * Get all transactions for the given Wallet object.
+ * @param {Wallet} wallet
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAllByWallet(wallet, parameters: any = {}) {
+ const selectQuery = this.query.select(this.query.block_id, this.query.serialized).from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ const applyConditions = queries => {
+ for (const item of queries) {
+ item.where(this.query.sender_public_key.equals(wallet.publicKey)).or(
+ this.query.recipient_id.equals(wallet.address),
+ );
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ const results = await this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit,
+ offset: parameters.offset,
+ orderBy: this.__orderBy(parameters),
+ });
+
+ results.rows = await this.__mapBlocksToTransactions(results.rows);
+
+ return results;
+ }
+
+ /**
+ * Get all transactions for the given sender public key.
+ * @param {String} senderPublicKey
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAllBySender(senderPublicKey, parameters = {}) {
+ return this.findAll({ ...{ senderPublicKey }, ...parameters });
+ }
+
+ /**
+ * Get all transactions for the given recipient address.
+ * @param {String} recipientId
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAllByRecipient(recipientId, parameters = {}) {
+ return this.findAll({ ...{ recipientId }, ...parameters });
+ }
+
+ /**
+ * Get all vote transactions for the given sender public key.
+ * TODO rename to findAllVotesBySender or not?
+ * @param {String} senderPublicKey
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async allVotesBySender(senderPublicKey, parameters = {}) {
+ return this.findAll({
+ ...{ senderPublicKey, type: TransactionType.Vote },
+ ...parameters,
+ });
+ }
+
+ /**
+ * Get all transactions for the given block.
+ * @param {Number} blockId
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAllByBlock(blockId, parameters = {}) {
+ return this.findAll({ ...{ blockId }, ...parameters });
+ }
+
+ /**
+ * Get all transactions for the given type.
+ * @param {Number} type
+ * @param {Object} parameters
+ * @return {Object}
+ */
+ public async findAllByType(type, parameters = {}) {
+ return this.findAll({ ...{ type }, ...parameters });
+ }
+
+ /**
+ * Get a transaction.
+ * @param {Number} id
+ * @return {Object}
+ */
+ public async findById(id) {
+ const query = this.query
+ .select(this.query.block_id, this.query.serialized)
+ .from(this.query)
+ .where(this.query.id.equals(id));
+
+ const transaction = await this._find(query);
+
+ return this.__mapBlocksToTransactions(transaction);
+ }
+
+ /**
+ * Get a transactions for the given type and id.
+ * @param {Number} type
+ * @param {Number} id
+ * @return {Object}
+ */
+ public async findByTypeAndId(type, id) {
+ const query = this.query
+ .select(this.query.block_id, this.query.serialized)
+ .from(this.query)
+ .where(this.query.id.equals(id).and(this.query.type.equals(type)));
+
+ const transaction = await this._find(query);
+
+ return this.__mapBlocksToTransactions(transaction);
+ }
+
+ /**
+ * Get transactions for the given ids.
+ * @param {Array} ids
+ * @return {Object}
+ */
+ public async findByIds(ids) {
+ const query = this.query
+ .select(this.query.block_id, this.query.serialized)
+ .from(this.query)
+ .where(this.query.id.in(ids));
+
+ return this._findMany(query);
+ }
+
+ /**
+ * Get all transactions that have a vendor field.
+ * @return {Object}
+ */
+ public async findWithVendorField() {
+ const query = this.query
+ .select(this.query.block_id, this.query.serialized)
+ .from(this.query)
+ .where(this.query.vendor_field_hex.isNotNull());
+
+ const rows = await this._findMany(query);
+
+ return this.__mapBlocksToTransactions(rows);
+ }
+
+ /**
+ * Calculates min, max and average fee statistics based on transactions table
+ * @return {Object}
+ */
+ public async getFeeStatistics() {
+ const query = this.query
+ .select(
+ this.query.type,
+ this.query.fee.min("minFee"),
+ this.query.fee.max("maxFee"),
+ this.query.fee.avg("avgFee"),
+ this.query.timestamp.max("timestamp"),
+ )
+ .from(this.query)
+ .where(
+ this.query.timestamp.gte(
+ slots.getTime(
+ dato()
+ .subDays(30)
+ .toMilliseconds(),
+ ),
+ ),
+ )
+ .and(this.query.fee.gte(this.transactionPool.options.dynamicFees.minFeeBroadcast))
+ .group(this.query.type)
+ .order('"timestamp" DESC');
+
+ return this._findMany(query);
+ }
+
+ /**
+ * Search all transactions.
+ *
+ * @param {Object} params
+ * @return {Object}
+ */
+ public async search(parameters) {
+ const selectQuery = this.query.select().from(this.query);
+ const countQuery = this._makeEstimateQuery();
+
+ if (parameters.senderId) {
+ const senderPublicKey = this.__publicKeyFromSenderId(parameters.senderId);
+
+ if (senderPublicKey) {
+ parameters.senderPublicKey = senderPublicKey;
+ }
+ }
+
+ const applyConditions = queries => {
+ const conditions = buildFilterQuery(this._formatConditions(parameters), {
+ exact: ["id", "block_id", "type", "version", "sender_public_key", "recipient_id"],
+ between: ["timestamp", "amount", "fee"],
+ wildcard: ["vendor_field_hex"],
+ });
+
+ if (conditions.length) {
+ const first = conditions.shift();
+
+ for (const item of queries) {
+ item.where(this.query[first.column][first.method](first.value));
+
+ for (const condition of conditions) {
+ item.and(this.query[condition.column][condition.method](condition.value));
+ }
+ }
+ }
+ };
+
+ applyConditions([selectQuery, countQuery]);
+
+ const results = await this._findManyWithCount(selectQuery, countQuery, {
+ limit: parameters.limit,
+ offset: parameters.offset,
+ orderBy: this.__orderBy(parameters),
+ });
+
+ results.rows = await this.__mapBlocksToTransactions(results.rows);
+
+ return results;
+ }
+
+ public getModel() {
+ return (this.databaseService.connection as any).models.transaction;
+ }
+
+ /**
+ * [__mapBlocksToTransactions description]
+ * @param {Array|Object} data
+ * @return {Object}
+ */
+ public async __mapBlocksToTransactions(data) {
+ const blockQuery = (this.databaseService.connection as any).models.block.query();
+
+ // Array...
+ if (Array.isArray(data)) {
+ // 1. get heights from cache
+ const missingFromCache = [];
+
+ for (let i = 0; i < data.length; i++) {
+ const cachedBlock = this.__getBlockCache(data[i].blockId);
+
+ if (cachedBlock) {
+ data[i].block = cachedBlock;
+ } else {
+ missingFromCache.push({
+ index: i,
+ blockId: data[i].blockId,
+ });
+ }
+ }
+
+ // 2. get missing heights from database
+ if (missingFromCache.length) {
+ const query = blockQuery
+ .select(blockQuery.id, blockQuery.height)
+ .from(blockQuery)
+ .where(blockQuery.id.in(missingFromCache.map(d => d.blockId)))
+ .group(blockQuery.id);
+
+ const blocks = await this._findMany(query);
+
+ for (const missing of missingFromCache) {
+ const block = blocks.find(item => item.id === missing.blockId);
+ if (block) {
+ data[missing.index].block = block;
+ this.__setBlockCache(block);
+ }
+ }
+ }
+
+ return data;
+ }
+
+ // Object...
+ if (data) {
+ const cachedBlock = this.__getBlockCache(data.blockId);
+
+ if (cachedBlock) {
+ data.block = cachedBlock;
+ } else {
+ const query = blockQuery
+ .select(blockQuery.id, blockQuery.height)
+ .from(blockQuery)
+ .where(blockQuery.id.equals(data.blockId));
+
+ data.block = await this._find(query);
+
+ this.__setBlockCache(data.block);
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Tries to retrieve the height of the block from the cache
+ * @param {String} blockId
+ * @return {Object|null}
+ */
+ public __getBlockCache(blockId) {
+ const height = this.cache.get(`heights:${blockId}`);
+
+ return height ? { height, id: blockId } : null;
+ }
+
+ /**
+ * Stores the height of the block on the cache
+ * @param {Object} block
+ * @param {String} block.id
+ * @param {Number} block.height
+ */
+ public __setBlockCache({ id, height }) {
+ this.cache.set(`heights:${id}`, height);
+ }
+
+ /**
+ * Retrieves the publicKey of the address from the WalletManager in-memory data
+ * @param {String} senderId
+ * @return {String}
+ */
+ public __publicKeyFromSenderId(senderId) {
+ return this.databaseService.walletManager.findByAddress(senderId).publicKey;
+ }
+
+ public __orderBy(parameters) {
+ return parameters.orderBy ? parameters.orderBy.split(":").map(p => p.toLowerCase()) : ["timestamp", "desc"];
+ }
+}
+
+export const transactionRepository = new TransactionsRepository();
diff --git a/core-graphql/src/repositories/utils/filter-query.ts b/core-graphql/src/repositories/utils/filter-query.ts
new file mode 100644
index 0000000..3c41c8c
--- /dev/null
+++ b/core-graphql/src/repositories/utils/filter-query.ts
@@ -0,0 +1,71 @@
+/**
+ * Create a "where" object for a sql query.
+ * @param {Object} parameters
+ * @param {Object} filters
+ * @return {Object}
+ */
+export function buildFilterQuery(parameters, filters) {
+ const where = [];
+
+ if (filters.exact) {
+ for (const elem of filters.exact) {
+ if (typeof parameters[elem] !== "undefined") {
+ where.push({
+ column: elem,
+ method: "equals",
+ value: parameters[elem],
+ });
+ }
+ }
+ }
+
+ if (filters.between) {
+ for (const elem of filters.between) {
+ if (!parameters[elem]) {
+ continue;
+ }
+
+ if (!parameters[elem].from && !parameters[elem].to) {
+ where.push({
+ column: elem,
+ method: "equals",
+ value: parameters[elem],
+ });
+ }
+
+ if (parameters[elem].from || parameters[elem].to) {
+ where[elem] = {};
+
+ if (parameters[elem].from) {
+ where.push({
+ column: elem,
+ method: "gte",
+ value: parameters[elem].from,
+ });
+ }
+
+ if (parameters[elem].to) {
+ where.push({
+ column: elem,
+ method: "lte",
+ value: parameters[elem].to,
+ });
+ }
+ }
+ }
+ }
+
+ if (filters.wildcard) {
+ for (const elem of filters.wildcard) {
+ if (parameters[elem]) {
+ where.push({
+ column: elem,
+ method: "like",
+ value: `%${parameters[elem]}%`,
+ });
+ }
+ }
+ }
+
+ return where;
+}
diff --git a/core-graphql/src/resolvers/index.ts b/core-graphql/src/resolvers/index.ts
new file mode 100644
index 0000000..91626b4
--- /dev/null
+++ b/core-graphql/src/resolvers/index.ts
@@ -0,0 +1,31 @@
+import GraphQLTypes from "graphql-tools-types";
+import * as queries from "./queries";
+import { Block } from "./relationship/block";
+import { Transaction } from "./relationship/transaction";
+import { Wallet } from "./relationship/wallet";
+
+/**
+ * Resolvers used by the executed schema when encountering a
+ * scalar or type.
+ *
+ * All of our scalars are based on graphql-tools-types which helps us with
+ * query standardization.
+ *
+ * We introduce relationships and queries for our own types,
+ * these hold the data processing responsibilities of the complete
+ * GraphQL query flow.
+ */
+
+export const resolvers = {
+ JSON: GraphQLTypes.JSON({ name: "Json" }),
+ Limit: GraphQLTypes.Int({ name: "Limit", min: 1, max: 100 }),
+ Offset: GraphQLTypes.Int({ name: "Offset", min: 0 }),
+ Address: GraphQLTypes.String({
+ name: "Address",
+ regex: /^[AaDd]{1}[0-9a-zA-Z]{33}/,
+ }),
+ Query: queries,
+ Block,
+ Transaction,
+ Wallet,
+};
diff --git a/core-graphql/src/resolvers/queries/block/block.ts b/core-graphql/src/resolvers/queries/block/block.ts
new file mode 100644
index 0000000..d6e1fcb
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/block/block.ts
@@ -0,0 +1,10 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+
+/**
+ * Get a single block from the database
+ * @return {Block}
+ */
+export async function block(_, { id }) {
+ return app.resolvePlugin("database").connection.blocksRepository.findById(id);
+}
diff --git a/core-graphql/src/resolvers/queries/block/blocks.ts b/core-graphql/src/resolvers/queries/block/blocks.ts
new file mode 100644
index 0000000..1b4db4a
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/block/blocks.ts
@@ -0,0 +1,16 @@
+import { formatOrderBy } from "../../../helpers";
+import { blockRepository } from "../../../repositories";
+
+/**
+ * Get multiple blocks from the database
+ * @return {Block[]}
+ */
+export async function blocks(_, args: any) {
+ const { orderBy, filter } = args;
+
+ const order = formatOrderBy(orderBy, "height:desc");
+
+ const result = await blockRepository.findAll({ ...filter, orderBy: order });
+
+ return result ? result.rows : [];
+}
diff --git a/core-graphql/src/resolvers/queries/index.ts b/core-graphql/src/resolvers/queries/index.ts
new file mode 100644
index 0000000..e89c5b4
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/index.ts
@@ -0,0 +1,8 @@
+import { block } from "./block/block";
+import { blocks } from "./block/blocks";
+import { transaction } from "./transaction/transaction";
+import { transactions } from "./transaction/transactions";
+import { wallet } from "./wallet/wallet";
+import { wallets } from "./wallet/wallets";
+
+export { block, blocks, transaction, transactions, wallet, wallets };
diff --git a/core-graphql/src/resolvers/queries/transaction/transaction.ts b/core-graphql/src/resolvers/queries/transaction/transaction.ts
new file mode 100644
index 0000000..e2c3b8d
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/transaction/transaction.ts
@@ -0,0 +1,10 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+
+/**
+ * Get a single transaction from the database
+ * @return {Transaction}
+ */
+export async function transaction(_, { id }) {
+ return app.resolvePlugin("database").connection.transactionsRepository.findById(id);
+}
diff --git a/core-graphql/src/resolvers/queries/transaction/transactions.ts b/core-graphql/src/resolvers/queries/transaction/transactions.ts
new file mode 100644
index 0000000..9c6b993
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/transaction/transactions.ts
@@ -0,0 +1,13 @@
+import { formatOrderBy } from "../../../helpers";
+import { transactionRepository } from "../../../repositories";
+
+/**
+ * Get multiple transactions from the database
+ * @return {Transaction[]}
+ */
+export async function transactions(_, args: any) {
+ const { orderBy, filter, limit } = args;
+ const order = formatOrderBy(orderBy, "timestamp:desc");
+ const result = await transactionRepository.findAll({ ...filter, orderBy: order, limit });
+ return result ? result.rows : [];
+}
diff --git a/core-graphql/src/resolvers/queries/wallet/wallet.ts b/core-graphql/src/resolvers/queries/wallet/wallet.ts
new file mode 100644
index 0000000..81feab2
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/wallet/wallet.ts
@@ -0,0 +1,13 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+
+const databaseService = app.resolvePlugin("database");
+
+/**
+ * Get a single wallet from the database
+ * @return {Wallet}
+ */
+export async function wallet(_, args: any) {
+ const param = args.address || args.publicKey || args.username;
+ return databaseService.wallets.findById(param);
+}
diff --git a/core-graphql/src/resolvers/queries/wallet/wallets.ts b/core-graphql/src/resolvers/queries/wallet/wallets.ts
new file mode 100644
index 0000000..0ae2f10
--- /dev/null
+++ b/core-graphql/src/resolvers/queries/wallet/wallets.ts
@@ -0,0 +1,24 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+import { formatOrderBy } from "../../../helpers";
+
+const databaseService = app.resolvePlugin("database");
+
+/**
+ * Get multiple wallets from the database
+ * @return {Wallet[]}
+ */
+export async function wallets(_, args: any) {
+ const { orderBy, filter, ...params } = args;
+
+ const order = formatOrderBy(orderBy, "height:desc");
+ const result =
+ filter && filter.vote
+ ? await databaseService.wallets.findAllByVote(filter.vote, {
+ orderBy: order,
+ ...params,
+ })
+ : await databaseService.wallets.findAll({ orderBy: order, ...params });
+
+ return result ? result.rows : [];
+}
diff --git a/core-graphql/src/resolvers/relationship/block.ts b/core-graphql/src/resolvers/relationship/block.ts
new file mode 100644
index 0000000..7457dfd
--- /dev/null
+++ b/core-graphql/src/resolvers/relationship/block.ts
@@ -0,0 +1,43 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+import { formatOrderBy, unserializeTransactions } from "../../helpers";
+
+const databaseService = app.resolvePlugin("database");
+
+/**
+ * Useful and common database operations with block data.
+ */
+export const Block = {
+ /**
+ * Get the transactions for a given block
+ * @param {Block}: block
+ * @param {Object}: args
+ * @return {Transaction[]}
+ */
+ async transactions(block, args) {
+ const { orderBy, filter, ...params } = args;
+
+ /* .findAll() method never existed on the TransactionRepository in core-database-postgres. This code would've blown chunks
+ const result = await database.connection.transactionsRepository.findAll(
+ {
+ ...filter,
+ orderBy: formatOrderBy(orderBy, "timestamp:DESC"),
+ ...params,
+ },
+ false,
+ );*/
+ const result = null;
+ const rows = result ? result.rows : [];
+
+ return unserializeTransactions(rows);
+ },
+
+ /**
+ * Get the generator wallet for a given block
+ * @param {Block} block
+ * @return {Wallet}
+ */
+ generator(block) {
+ return databaseService.wallets.findById(block.generatorPublicKey);
+ },
+};
diff --git a/core-graphql/src/resolvers/relationship/transaction.ts b/core-graphql/src/resolvers/relationship/transaction.ts
new file mode 100644
index 0000000..b17119f
--- /dev/null
+++ b/core-graphql/src/resolvers/relationship/transaction.ts
@@ -0,0 +1,32 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+
+const databaseService = app.resolvePlugin("database");
+
+/**
+ * Useful and common database operations with transaction data.
+ */
+export const Transaction = {
+ /**
+ * Get the block of a transaction
+ * @param {Transaction} transaction
+ * @return {Block}
+ */
+ block: transaction => databaseService.connection.blocksRepository.findById(transaction.blockId),
+
+ /**
+ * Get the recipient of a transaction
+ * @param {Transaction} transaction
+ * @return {Wallet}
+ */
+ recipient: transaction =>
+ transaction.recipientId ? databaseService.wallets.findById(transaction.recipientId) : [],
+
+ /**
+ * Get the sender of a transaction
+ * @param {Transaction} transaction
+ * @return {Wallet}
+ */
+ sender: transaction =>
+ transaction.senderPublicKey ? databaseService.wallets.findById(transaction.senderPublicKey) : [],
+};
diff --git a/core-graphql/src/resolvers/relationship/wallet.ts b/core-graphql/src/resolvers/relationship/wallet.ts
new file mode 100644
index 0000000..9fad8d5
--- /dev/null
+++ b/core-graphql/src/resolvers/relationship/wallet.ts
@@ -0,0 +1,68 @@
+import { app } from "@arkecosystem/core-container";
+import { Database } from "@arkecosystem/core-interfaces";
+import { formatOrderBy, unserializeTransactions } from "../../helpers";
+
+const databaseService = app.resolvePlugin("database");
+
+/**
+ * Useful and common database operations with wallet data.
+ */
+export const Wallet = {
+ /*
+ * Get the transactions for a given wallet.
+ * @param {Wallet} wallet
+ * @param {Object} args
+ * @return {Transaction[]}
+ */
+ async transactions(wallet, args) {
+ const { orderBy, filter, ...params } = args;
+
+ const walletOr = (databaseService.connection as any).createCondition("OR", [
+ {
+ senderPublicKey: wallet.publicKey,
+ },
+ {
+ recipientId: wallet.address,
+ },
+ ]);
+
+ /* TODO .findAll() method never existed on the TransactionRepository in core-database-postgres. This code would've blown chunks
+ const result = await databaseService.connection.transactionsRepository.findAll(
+ {
+ ...filter,
+ orderBy: formatOrderBy(orderBy, "timestamp:DESC"),
+ ...walletOr,
+ ...params,
+ },
+ false,
+ );*/
+ const result = null;
+ const rows = result ? result.rows : [];
+
+ return unserializeTransactions(rows);
+ },
+
+ /*
+ * Get the blocks generated for a given wallet.
+ * @param {Wallet} wallet
+ * @param {Object} args
+ * @return {Block[]}
+ */
+ blocks(wallet, args) {
+ const { orderBy, ...params } = args;
+
+ params.generatorPublickKey = wallet.publicKey;
+
+ /* TODO: .findAll() method never existed on the TransactionRepository in core-database-postgres. This code would've blown chunks
+ const result = databaseService.connection.blocksRepository.findAll(
+ {
+ orderBy: formatOrderBy(orderBy, "height:DESC"),
+ ...params,
+ },
+ false,
+ );*/
+ const result = null;
+ const rows = result ? result.rows : [];
+ return rows;
+ },
+};
diff --git a/core-graphql/src/server.ts b/core-graphql/src/server.ts
new file mode 100644
index 0000000..59cdba2
--- /dev/null
+++ b/core-graphql/src/server.ts
@@ -0,0 +1,18 @@
+import { createServer, mountServer } from "@arkecosystem/core-http-utils";
+import { apolloServer } from "./apollo-server";
+
+export async function startServer(config) {
+ const app = await createServer({
+ host: config.host,
+ port: config.port,
+ });
+
+ await apolloServer.applyMiddleware({
+ app,
+ path: config.path,
+ });
+
+ await apolloServer.installSubscriptionHandlers(app.listener);
+
+ return mountServer("GraphQL", app);
+}
diff --git a/core-graphql/tsconfig.json b/core-graphql/tsconfig.json
new file mode 100644
index 0000000..0b089c5
--- /dev/null
+++ b/core-graphql/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src/**/**.ts"]
+}
diff --git a/core-json-rpc/.gitattributes b/core-json-rpc/.gitattributes
new file mode 100644
index 0000000..63f6a5b
--- /dev/null
+++ b/core-json-rpc/.gitattributes
@@ -0,0 +1,7 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/README.md export-ignore
diff --git a/core-json-rpc/README.md b/core-json-rpc/README.md
new file mode 100644
index 0000000..304ee14
--- /dev/null
+++ b/core-json-rpc/README.md
@@ -0,0 +1,21 @@
+# ARK Core - JSON-RPC
+
+
+
+
+
+## Documentation
+
+You can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://docs.ark.io/guidebook/core/plugins/optional/core-json-rpc.html).
+
+## Security
+
+If you discover a security vulnerability within this package, please send an e-mail to security@ark.io. All security vulnerabilities will be promptly addressed.
+
+## Credits
+
+This project exists thanks to all the people who [contribute](../../../../contributors).
+
+## License
+
+[MIT](LICENSE) © [ARK Ecosystem](https://ark.io)
diff --git a/core-json-rpc/package.json b/core-json-rpc/package.json
new file mode 100644
index 0000000..9804e1c
--- /dev/null
+++ b/core-json-rpc/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@arkecosystem/core-json-rpc",
+ "version": "2.4.0-next.9",
+ "description": "A JSON-RPC 2.0 Specification compliant server to interact with the ARK Blockchain.",
+ "license": "MIT",
+ "contributors": [
+ "François-Xavier Thoorens ",
+ "Brian Faust "
+ ],
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index",
+ "scripts": {
+ "build": "yarn clean && yarn compile",
+ "build:watch": "yarn clean && yarn compile -w",
+ "clean": "del dist",
+ "compile": "../../node_modules/typescript/bin/tsc",
+ "prepublishOnly": "yarn build",
+ "pretest": "bash ../../scripts/pre-test.sh"
+ },
+ "dependencies": {
+ "@arkecosystem/core-container": "^2.4.0-next.9",
+ "@arkecosystem/core-http-utils": "^2.4.0-next.9",
+ "@arkecosystem/core-interfaces": "^2.4.0-next.9",
+ "@arkecosystem/core-utils": "^2.4.0-next.9",
+ "@arkecosystem/crypto": "^2.4.0-next.9",
+ "@hapi/boom": "^7.4.2",
+ "@keyv/sqlite": "^2.0.0",
+ "bip39": "^3.0.2",
+ "is-reachable": "^3.1.0",
+ "keyv": "^3.1.0",
+ "lodash.get": "^4.4.2",
+ "lodash.sample": "^4.2.1",
+ "uuid": "^3.3.2",
+ "wif": "^2.0.6"
+ },
+ "devDependencies": {
+ "@arkecosystem/core-p2p": "^2.4.0-next.9",
+ "@types/bip39": "^2.4.2",
+ "@types/hapi__boom": "^7.4.0",
+ "@types/is-reachable": "^3.1.0",
+ "@types/keyv": "^3.1.0",
+ "@types/keyv__sqlite": "^2.0.0",
+ "@types/lodash.get": "^4.4.6",
+ "@types/lodash.sample": "^4.2.6",
+ "@types/uuid": "^3.4.4",
+ "@types/wif": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10.x"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/core-json-rpc/src/defaults.ts b/core-json-rpc/src/defaults.ts
new file mode 100644
index 0000000..c52ef1b
--- /dev/null
+++ b/core-json-rpc/src/defaults.ts
@@ -0,0 +1,11 @@
+export const defaults = {
+ enabled: process.env.CORE_EXCHANGE_JSON_RPC_ENABLED,
+ host: process.env.CORE_EXCHANGE_JSON_RPC_HOST || "0.0.0.0",
+ port: process.env.CORE_EXCHANGE_JSON_RPC_PORT || 8080,
+ allowRemote: false,
+ whitelist: ["127.0.0.1", "::ffff:127.0.0.1"],
+ database: {
+ uri: process.env.CORE_EXCHANGE_JSON_RPC_DATABASE || `sqlite://${process.env.CORE_PATH_DATA}/json-rpc.sqlite`,
+ options: {},
+ },
+};
diff --git a/core-json-rpc/src/index.ts b/core-json-rpc/src/index.ts
new file mode 100644
index 0000000..ca2e9e2
--- /dev/null
+++ b/core-json-rpc/src/index.ts
@@ -0,0 +1,30 @@
+import { Container, Logger } from "@arkecosystem/core-interfaces";
+import { defaults } from "./defaults";
+import { startServer } from "./server";
+import { database } from "./server/services/database";
+
+export const plugin: Container.IPluginDescriptor = {
+ pkg: require("../package.json"),
+ defaults,
+ alias: "json-rpc",
+ async register(container: Container.IContainer, options) {
+ const logger = container.resolvePlugin("logger");
+
+ if (!options.enabled) {
+ logger.info("JSON-RPC Server is disabled");
+
+ return undefined;
+ }
+
+ database.init(options.database);
+
+ return startServer(options);
+ },
+ async deregister(container: Container.IContainer, options) {
+ if (options.enabled) {
+ container.resolvePlugin("logger").info("Stopping JSON-RPC Server");
+
+ return container.resolvePlugin("json-rpc").stop();
+ }
+ },
+};
diff --git a/core-json-rpc/src/interfaces.ts b/core-json-rpc/src/interfaces.ts
new file mode 100644
index 0000000..d2dcb00
--- /dev/null
+++ b/core-json-rpc/src/interfaces.ts
@@ -0,0 +1,29 @@
+import { Interfaces } from "@arkecosystem/crypto";
+
+export interface IRequestParameters {
+ jsonrpc: "2.0";
+ method: string;
+ id: string | number;
+ params: object;
+}
+
+export interface IResponse {
+ jsonrpc: "2.0";
+ id: string | number;
+ result: T;
+}
+
+export interface IResponseError {
+ jsonrpc: "2.0";
+ id: string | number;
+ error: {
+ code: number;
+ message: string;
+ data: string;
+ };
+}
+
+export interface IWallet {
+ keys: Interfaces.IKeyPair;
+ wif: string;
+}
diff --git a/core-json-rpc/src/server/index.ts b/core-json-rpc/src/server/index.ts
new file mode 100755
index 0000000..3529751
--- /dev/null
+++ b/core-json-rpc/src/server/index.ts
@@ -0,0 +1,56 @@
+import { app } from "@arkecosystem/core-container";
+import { createServer, mountServer, plugins } from "@arkecosystem/core-http-utils";
+import { Logger } from "@arkecosystem/core-interfaces";
+import { IRequestParameters } from "../interfaces";
+import * as modules from "./modules";
+import { Processor } from "./services/processor";
+
+export const startServer = async options => {
+ if (options.allowRemote) {
+ app.resolvePlugin("logger").warn(
+ "JSON-RPC server allows remote connections, this is a potential security risk",
+ );
+ }
+
+ const server = await createServer({
+ host: options.host,
+ port: options.port,
+ });
+
+ // @ts-ignore
+ server.app.schemas = {};
+
+ if (!options.allowRemote) {
+ await server.register({
+ plugin: plugins.whitelist,
+ options: {
+ whitelist: options.whitelist,
+ },
+ });
+ }
+
+ for (const module of Object.values(modules)) {
+ for (const method of Object.values(module)) {
+ // @ts-ignore
+ server.app.schemas[method.name] = method.schema;
+
+ delete method.schema;
+
+ server.method(method);
+ }
+ }
+
+ server.route({
+ method: "POST",
+ path: "/",
+ async handler(request) {
+ const processor = new Processor();
+
+ return Array.isArray(request.payload)
+ ? processor.collection(request.server, request.payload as IRequestParameters[])
+ : processor.resource(request.server, request.payload as IRequestParameters);
+ },
+ });
+
+ return mountServer("JSON-RPC", server);
+};
diff --git a/core-json-rpc/src/server/modules.ts b/core-json-rpc/src/server/modules.ts
new file mode 100644
index 0000000..fd35ef7
--- /dev/null
+++ b/core-json-rpc/src/server/modules.ts
@@ -0,0 +1,390 @@
+import { Crypto, Identities, Interfaces, Transactions } from "@arkecosystem/crypto";
+import Boom from "@hapi/boom";
+import { generateMnemonic } from "bip39";
+import { IWallet } from "../interfaces";
+import { database } from "./services/database";
+import { network } from "./services/network";
+import { decryptWIF, getBIP38Wallet } from "./utils";
+
+export const blocks = [
+ {
+ name: "blocks.info",
+ async method(params: { id: string }) {
+ const response = await network.sendGET({ path: `blocks/${params.id}` });
+
+ if (!response) {
+ return Boom.notFound(`Block ${params.id} could not be found.`);
+ }
+
+ return response.data;
+ },
+ schema: {
+ type: "object",
+ properties: {
+ id: { blockId: {} },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "blocks.latest",
+ async method() {
+ const response = await network.sendGET({
+ path: "blocks",
+ query: { orderBy: "height:desc", limit: 1 },
+ });
+
+ return response ? response.data[0] : Boom.notFound(`Latest block could not be found.`);
+ },
+ },
+ {
+ name: "blocks.transactions",
+ async method(params: { id: string; offset?: number }) {
+ const response = await network.sendGET({
+ path: `blocks/${params.id}/transactions`,
+ query: {
+ offset: params.offset,
+ orderBy: "timestamp:desc",
+ },
+ });
+
+ if (!response) {
+ return Boom.notFound(`Block ${params.id} could not be found.`);
+ }
+
+ return {
+ count: response.meta.totalCount,
+ data: response.data,
+ };
+ },
+ schema: {
+ type: "object",
+ properties: {
+ id: { blockId: {} },
+ offset: {
+ type: "number",
+ },
+ },
+ required: ["id"],
+ },
+ },
+];
+
+export const transactions = [
+ {
+ name: "transactions.broadcast",
+ async method(params: { id: string }) {
+ const transaction: Interfaces.ITransactionData = await database.get(params.id);
+
+ if (!transaction) {
+ return Boom.notFound(`Transaction ${params.id} could not be found.`);
+ }
+
+ const { data } = Transactions.TransactionFactory.fromData(transaction);
+
+ if (!Transactions.Verifier.verifyHash(data)) {
+ return Boom.badData();
+ }
+
+ await network.sendPOST({
+ path: "transactions",
+ body: { transactions: [transaction] },
+ });
+
+ return transaction;
+ },
+ schema: {
+ type: "object",
+ properties: {
+ id: {
+ $ref: "transactionId",
+ },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "transactions.create",
+ async method(params: { recipientId: string; amount: string; vendorField?: string; passphrase: string }) {
+ const transactionBuilder = Transactions.BuilderFactory.transfer()
+ .recipientId(params.recipientId)
+ .amount(params.amount);
+
+ if (params.vendorField) {
+ transactionBuilder.vendorField(params.vendorField);
+ }
+
+ const transaction: Interfaces.ITransactionData = transactionBuilder.sign(params.passphrase).getStruct();
+
+ if (!Transactions.Verifier.verifyHash(transaction)) {
+ return Boom.badData();
+ }
+
+ await database.set(transaction.id, transaction);
+
+ return transaction;
+ },
+ schema: {
+ type: "object",
+ properties: {
+ amount: {
+ type: "number",
+ },
+ recipientId: {
+ type: "string",
+ $ref: "address",
+ },
+ passphrase: {
+ type: "string",
+ },
+ vendorField: {
+ type: "string",
+ },
+ },
+ required: ["amount", "recipientId", "passphrase"],
+ },
+ },
+ {
+ name: "transactions.info",
+ async method(params: { id: string }) {
+ const response = await network.sendGET({ path: `transactions/${params.id}` });
+
+ if (!response) {
+ return Boom.notFound(`Transaction ${params.id} could not be found.`);
+ }
+
+ return response.data;
+ },
+ schema: {
+ type: "object",
+ properties: {
+ id: {
+ $ref: "transactionId",
+ },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "transactions.bip38.create",
+ async method(params: {
+ userId: string;
+ bip38: string;
+ recipientId: string;
+ amount: string;
+ vendorField?: string;
+ }) {
+ try {
+ const wallet: IWallet = await getBIP38Wallet(params.userId, params.bip38);
+
+ if (!wallet) {
+ return Boom.notFound(`User ${params.userId} could not be found.`);
+ }
+
+ const transactionBuilder = Transactions.BuilderFactory.transfer()
+ .recipientId(params.recipientId)
+ .amount(params.amount);
+
+ if (params.vendorField) {
+ transactionBuilder.vendorField(params.vendorField);
+ }
+
+ const transaction: Interfaces.ITransactionData = transactionBuilder.signWithWif(wallet.wif).getStruct();
+
+ if (!Transactions.Verifier.verifyHash(transaction)) {
+ return Boom.badData();
+ }
+
+ await database.set(transaction.id, transaction);
+
+ return transaction;
+ } catch (error) {
+ return Boom.badImplementation(error.message);
+ }
+ },
+ schema: {
+ type: "object",
+ properties: {
+ amount: {
+ type: "number",
+ },
+ recipientId: {
+ type: "string",
+ $ref: "address",
+ },
+ vendorField: {
+ type: "string",
+ },
+ bip38: {
+ type: "string",
+ },
+ userId: {
+ type: "string",
+ $ref: "hex",
+ },
+ },
+ required: ["amount", "recipientId", "bip38", "userId"],
+ },
+ },
+];
+
+export const wallets = [
+ {
+ name: "wallets.create",
+ async method(params: { passphrase: string }) {
+ const { publicKey }: Interfaces.IKeyPair = Identities.Keys.fromPassphrase(params.passphrase);
+
+ return {
+ publicKey,
+ address: Identities.Address.fromPublicKey(publicKey),
+ };
+ },
+ schema: {
+ type: "object",
+ properties: {
+ passphrase: {
+ type: "string",
+ },
+ },
+ required: ["passphrase"],
+ },
+ },
+ {
+ name: "wallets.info",
+ async method(params: { address: string }) {
+ const response = await network.sendGET({ path: `wallets/${params.address}` });
+
+ if (!response) {
+ return Boom.notFound(`Wallet ${params.address} could not be found.`);
+ }
+
+ return response.data;
+ },
+ schema: {
+ type: "object",
+ properties: {
+ address: {
+ type: "string",
+ $ref: "address",
+ },
+ },
+ required: ["address"],
+ },
+ },
+ {
+ name: "wallets.transactions",
+ async method(params: { offset?: number; address: string }) {
+ const response = await network.sendGET({
+ path: "transactions",
+ query: {
+ offset: params.offset || 0,
+ orderBy: "timestamp:desc",
+ ownerId: params.address,
+ },
+ });
+
+ if (!response.data || !response.data.length) {
+ return Boom.notFound(`Wallet ${params.address} could not be found.`);
+ }
+
+ return {
+ count: response.meta.totalCount,
+ data: response.data,
+ };
+ },
+ schema: {
+ type: "object",
+ properties: {
+ address: {
+ type: "string",
+ $ref: "address",
+ },
+ offset: {
+ type: "integer",
+ },
+ },
+ required: ["address"],
+ },
+ },
+ {
+ name: "wallets.bip38.create",
+ async method(params: { userId: string; bip38: string }) {
+ try {
+ const { keys, wif }: IWallet = await getBIP38Wallet(params.userId, params.bip38);
+
+ return {
+ publicKey: keys.publicKey,
+ address: Identities.Address.fromPublicKey(keys.publicKey),
+ wif,
+ };
+ } catch (error) {
+ const { publicKey, privateKey }: Interfaces.IKeyPair = Identities.Keys.fromPassphrase(
+ generateMnemonic(),
+ );
+
+ const encryptedWIF: string = Crypto.bip38.encrypt(
+ Buffer.from(privateKey, "hex"),
+ true,
+ params.bip38 + params.userId,
+ );
+
+ await database.set(
+ Crypto.HashAlgorithms.sha256(Buffer.from(params.userId)).toString("hex"),
+ encryptedWIF,
+ );
+
+ return {
+ publicKey,
+ address: Identities.Address.fromPublicKey(publicKey),
+ wif: decryptWIF(encryptedWIF, params.userId, params.bip38).wif,
+ };
+ }
+ },
+ schema: {
+ type: "object",
+ properties: {
+ bip38: {
+ type: "string",
+ },
+ userId: {
+ type: "string",
+ $ref: "hex",
+ },
+ },
+ required: ["bip38", "userId"],
+ },
+ },
+ {
+ name: "wallets.bip38.info",
+ async method(params: { userId: string; bip38: string }) {
+ const encryptedWIF: string = await database.get(
+ Crypto.HashAlgorithms.sha256(Buffer.from(params.userId)).toString("hex"),
+ );
+
+ if (!encryptedWIF) {
+ return Boom.notFound(`User ${params.userId} could not be found.`);
+ }
+
+ const { keys, wif }: IWallet = decryptWIF(encryptedWIF, params.userId, params.bip38);
+
+ return {
+ publicKey: keys.publicKey,
+ address: Identities.Address.fromPublicKey(keys.publicKey),
+ wif,
+ };
+ },
+ schema: {
+ type: "object",
+ properties: {
+ bip38: {
+ type: "string",
+ },
+ userId: {
+ type: "string",
+ $ref: "hex",
+ },
+ },
+ required: ["bip38", "userId"],
+ },
+ },
+];
diff --git a/core-json-rpc/src/server/services/database.ts b/core-json-rpc/src/server/services/database.ts
new file mode 100644
index 0000000..52aba33
--- /dev/null
+++ b/core-json-rpc/src/server/services/database.ts
@@ -0,0 +1,19 @@
+import Keyv from "keyv";
+
+class Database {
+ private database: Keyv;
+
+ public init(options) {
+ this.database = new Keyv(options);
+ }
+
+ public async get(id: string): Promise {
+ return this.database.get(id);
+ }
+
+ public async set(id: string, value: T): Promise {
+ await this.database.set(id, value);
+ }
+}
+
+export const database = new Database();
diff --git a/core-json-rpc/src/server/services/network.ts b/core-json-rpc/src/server/services/network.ts
new file mode 100644
index 0000000..f6affad
--- /dev/null
+++ b/core-json-rpc/src/server/services/network.ts
@@ -0,0 +1,81 @@
+import { app } from "@arkecosystem/core-container";
+import { Logger, P2P } from "@arkecosystem/core-interfaces";
+import { Peer } from "@arkecosystem/core-p2p";
+import { httpie } from "@arkecosystem/core-utils";
+import { Interfaces, Managers } from "@arkecosystem/crypto";
+import isReachable from "is-reachable";
+import sample from "lodash.sample";
+
+class Network {
+ private readonly network: Interfaces.INetwork = Managers.configManager.get("network");
+ private readonly logger: Logger.ILogger = app.resolvePlugin("logger");
+ private readonly p2p: P2P.IPeerService = app.resolvePlugin("p2p");
+
+ public async sendGET({ path, query = {} }: { path: string; query?: Record }) {
+ return this.sendRequest("get", path, { query });
+ }
+
+ public async sendPOST({ path, body }: { path: string; body: Record }) {
+ return this.sendRequest("post", path, { body });
+ }
+
+ private async sendRequest(method: string, path: string, opts, tries: number = 0) {
+ try {
+ const peer: P2P.IPeer = await this.getPeer();
+ const uri: string = `http://${peer.ip}:${peer.ports.api}/api/${path}`;
+
+ this.logger.info(`Sending request on "${this.network.name}" to "${uri}"`);
+
+ return (await httpie[method](uri, {
+ ...opts,
+ ...{
+ headers: {
+ Accept: "application/vnd.core-api.v2+json",
+ "Content-Type": "application/json",
+ },
+ timeout: 3000,
+ },
+ })).body;
+ } catch (error) {
+ this.logger.error(error.message);
+
+ if (tries > 3) {
+ this.logger.error(`Failed to find a responsive peer after 3 tries.`);
+
+ return undefined;
+ }
+
+ tries++;
+
+ return this.sendRequest(method, path, opts, tries);
+ }
+ }
+
+ private async getPeer(): Promise {
+ const peer: P2P.IPeer = sample(this.getPeers());
+
+ if (!(await isReachable(`${peer.ip}:${peer.port}`))) {
+ this.logger.warn(`${peer} is unresponsive. Choosing new peer.`);
+
+ return this.getPeer();
+ }
+
+ return peer;
+ }
+
+ private getPeers(): P2P.IPeer[] {
+ let peers: P2P.IPeer[] = this.p2p.getStorage().getPeers();
+
+ if (!peers.length && this.network.name === "testnet") {
+ peers = [new Peer("127.0.0.1")];
+ }
+
+ if (!peers.length) {
+ throw new Error("No peers found.");
+ }
+
+ return peers;
+ }
+}
+
+export const network = new Network();
diff --git a/core-json-rpc/src/server/services/processor.ts b/core-json-rpc/src/server/services/processor.ts
new file mode 100644
index 0000000..c1854c2
--- /dev/null
+++ b/core-json-rpc/src/server/services/processor.ts
@@ -0,0 +1,101 @@
+import { Validation } from "@arkecosystem/crypto";
+import { Server } from "@hapi/hapi";
+import get from "lodash.get";
+import { IRequestParameters, IResponse, IResponseError } from "../../interfaces";
+
+export class Processor {
+ public async resource(
+ server: Server,
+ payload: IRequestParameters,
+ ): Promise | IResponseError> {
+ const { error } = Validation.validator.validate(
+ {
+ type: "object",
+ properties: {
+ jsonrpc: {
+ type: "string",
+ pattern: "2.0",
+ },
+ method: {
+ type: "string",
+ },
+ id: {
+ type: ["number", "string"],
+ },
+ params: {
+ type: "object",
+ },
+ },
+ required: ["jsonrpc", "method", "id"],
+ },
+ payload || {},
+ );
+
+ if (error) {
+ return this.createErrorResponse(payload ? payload.id : undefined, -32600, new Error(error));
+ }
+
+ const { method, params, id } = payload;
+
+ try {
+ const targetMethod = get(server.methods, method);
+
+ if (!targetMethod) {
+ return this.createErrorResponse(id, -32601, new Error("The method does not exist / is not available."));
+ }
+
+ // @ts-ignore
+ const schema = server.app.schemas[method];
+
+ if (schema) {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { error } = Validation.validator.validate(schema, params);
+
+ if (error) {
+ return this.createErrorResponse(id, -32602, error);
+ }
+ }
+
+ const result = await targetMethod(params);
+
+ return result.isBoom
+ ? this.createErrorResponse(id, result.output.statusCode, result.output.payload)
+ : this.createSuccessResponse(id, result);
+ } catch (error) {
+ return this.createErrorResponse(id, -32603, error);
+ }
+ }
+
+ public async collection(
+ server: Server,
+ payloads: IRequestParameters[],
+ ): Promise> | IResponseError[]> {
+ const results = [];
+
+ for (const payload of payloads) {
+ results.push(await this.resource(server, payload));
+ }
+
+ return results;
+ }
+
+ private createSuccessResponse(id: string | number, result: T): IResponse {
+ return {
+ jsonrpc: "2.0",
+ id,
+ result,
+ };
+ }
+
+ private createErrorResponse(id: string | number, code: number, error: Error): IResponseError {
+ return {
+ jsonrpc: "2.0",
+ id,
+ error: {
+ code,
+ message: error.message,
+ data: error.stack,
+ },
+ };
+ }
+}
diff --git a/core-json-rpc/src/server/utils.ts b/core-json-rpc/src/server/utils.ts
new file mode 100644
index 0000000..5061817
--- /dev/null
+++ b/core-json-rpc/src/server/utils.ts
@@ -0,0 +1,25 @@
+import { Crypto, Identities, Interfaces, Managers } from "@arkecosystem/crypto";
+import wif from "wif";
+import { IWallet } from "../interfaces";
+import { database } from "./services/database";
+
+export const decryptWIF = (encryptedWif, userId, bip38password): IWallet => {
+ const decrypted: Interfaces.IDecryptResult = Crypto.bip38.decrypt(
+ encryptedWif.toString("hex"),
+ bip38password + userId,
+ );
+
+ const encodedWIF: string = wif.encode(
+ Managers.configManager.get("network.wif"),
+ decrypted.privateKey,
+ decrypted.compressed,
+ );
+
+ return { keys: Identities.Keys.fromWIF(encodedWIF), wif: encodedWIF };
+};
+
+export const getBIP38Wallet = async (userId, bip38password): Promise => {
+ const encryptedWif: string = await database.get(Crypto.HashAlgorithms.sha256(Buffer.from(userId)).toString("hex"));
+
+ return encryptedWif ? decryptWIF(encryptedWif, userId, bip38password) : undefined;
+};
diff --git a/core-json-rpc/tsconfig.json b/core-json-rpc/tsconfig.json
new file mode 100644
index 0000000..0b089c5
--- /dev/null
+++ b/core-json-rpc/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src/**/**.ts"]
+}
diff --git a/core-snapshots-cli/README.md b/core-snapshots-cli/README.md
new file mode 100644
index 0000000..e3fb5d6
--- /dev/null
+++ b/core-snapshots-cli/README.md
@@ -0,0 +1,27 @@
+# ARK Core - Snapshots CLI
+
+
+
+
+
+## Deprecated
+
+Note that this plugin is deprecated and should no longer be used
+
+## Documentation
+
+You can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://docs.ark.io/guidebook/core/plugins/core-snapshots-cli.html).
+
+## Security
+
+If you discover a security vulnerability within this package, please send an e-mail to security@ark.io. All security vulnerabilities will be promptly addressed.
+
+## Credits
+
+- [Joshua Noack](https://github.com/supaiku0)
+- [Kristjan Košič](https://github.com/kristjank)
+- [All Contributors](../../../../contributors)
+
+## License
+
+[MIT](LICENSE) © [ARK Ecosystem](https://ark.io)
diff --git a/core-snapshots-cli/__tests__/.gitkeep b/core-snapshots-cli/__tests__/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/core-snapshots-cli/bin/run b/core-snapshots-cli/bin/run
new file mode 100755
index 0000000..30b14e1
--- /dev/null
+++ b/core-snapshots-cli/bin/run
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+require('@oclif/command').run()
+.then(require('@oclif/command/flush'))
+.catch(require('@oclif/errors/handle'))
diff --git a/core-snapshots-cli/bin/run.cmd b/core-snapshots-cli/bin/run.cmd
new file mode 100644
index 0000000..968fc30
--- /dev/null
+++ b/core-snapshots-cli/bin/run.cmd
@@ -0,0 +1,3 @@
+@echo off
+
+node "%~dp0\run" %*
diff --git a/core-snapshots-cli/package.json b/core-snapshots-cli/package.json
new file mode 100644
index 0000000..924b3f0
--- /dev/null
+++ b/core-snapshots-cli/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@arkecosystem/core-snapshots-cli",
+ "version": "2.2.0",
+ "description": "Provides live cli interface to the core-snapshots module for ARK Core",
+ "license": "MIT",
+ "contributors": [
+ "Kristjan Košič "
+ ],
+ "files": [
+ "/bin",
+ "/dist",
+ "/oclif.manifest.json"
+ ],
+ "main": "dist/index.js",
+ "bin": {
+ "snapshot": "./bin/run"
+ },
+ "scripts": {
+ "build": "yarn clean && yarn compile",
+ "build:watch": "yarn clean && yarn compile -w",
+ "clean": "del dist",
+ "compile": "../../node_modules/typescript/bin/tsc",
+ "debug": "node --inspect-brk ./dist/index.js",
+ "dump": "yarn snapshot dump",
+ "dump:devnet": "yarn snapshot dump --network devnet",
+ "dump:mainnet": "yarn snapshot dump --network mainnet",
+ "dump:testnet": "yarn snapshot dump --network testnet",
+ "prepack": "../../node_modules/.bin/oclif-dev manifest && npm shrinkwrap",
+ "postpack": "rm -f oclif.manifest.json",
+ "prepublishOnly": "yarn build",
+ "restore": "yarn snapshot restore",
+ "restore:devnet": "yarn snapshot restore --network devnet",
+ "restore:mainnet": "yarn snapshot restore --network mainnet",
+ "restore:testnet": "yarn snapshot restore --network testnet",
+ "rollback": "yarn snapshot rollback",
+ "rollback:devnet": "yarn snapshot rollback --network devnet",
+ "rollback:mainnet": "yarn snapshot rollback --network mainnet",
+ "rollback:testnet": "yarn snapshot rollback --network testnet",
+ "snapshot": "./bin/run",
+ "truncate": "yarn snapshot truncate",
+ "truncate:devnet": "yarn snapshot truncate --network devnet",
+ "truncate:mainnet": "yarn snapshot truncate --network mainnet",
+ "truncate:testnet": "yarn snapshot truncate --network testnet",
+ "verify": "yarn snapshot verify",
+ "verify:devnet": "yarn snapshot verify --network devnet",
+ "verify:mainnet": "yarn snapshot verify --network mainnet",
+ "verify:testnet": "yarn snapshot verify --network testnet"
+ },
+ "dependencies": {
+ "@arkecosystem/core-container": "^2.2.0",
+ "@arkecosystem/core-interfaces": "^2.2.0",
+ "@arkecosystem/core-snapshots": "^2.2.0",
+ "@oclif/command": "^1.5.10",
+ "@oclif/config": "^1.12.6",
+ "@oclif/plugin-help": "^2.1.6",
+ "@oclif/plugin-not-found": "^1.2.2",
+ "@types/boom": "^7.2.1",
+ "@types/cli-progress": "^1.8.0",
+ "@types/commander": "^2.12.2",
+ "@types/fs-extra": "^5.0.5",
+ "cli-progress": "^2.1.1",
+ "fs-extra": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.x"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "oclif": {
+ "commands": "./dist/commands",
+ "bin": "snapshot",
+ "plugins": [
+ "@oclif/plugin-help",
+ "@oclif/plugin-not-found"
+ ]
+ }
+}
diff --git a/core-snapshots-cli/src/commands/command.ts b/core-snapshots-cli/src/commands/command.ts
new file mode 100644
index 0000000..c4f02e2
--- /dev/null
+++ b/core-snapshots-cli/src/commands/command.ts
@@ -0,0 +1,25 @@
+import Command, { flags } from "@oclif/command";
+
+export abstract class BaseCommand extends Command {
+ public static flags = {
+ data: flags.string({
+ description: "data directory",
+ }),
+ config: flags.string({
+ description: "network config",
+ }),
+ token: flags.string({
+ description: "token name",
+ default: "ark",
+ }),
+ network: flags.string({
+ description: "token network",
+ }),
+ skipCompression: flags.boolean({
+ description: "skip gzip compression",
+ }),
+ trace: flags.boolean({
+ description: "dumps generated queries and settings to console",
+ }),
+ };
+}
diff --git a/core-snapshots-cli/src/commands/dump.ts b/core-snapshots-cli/src/commands/dump.ts
new file mode 100644
index 0000000..a06ac27
--- /dev/null
+++ b/core-snapshots-cli/src/commands/dump.ts
@@ -0,0 +1,38 @@
+import { app } from "@arkecosystem/core-container";
+import { Logger } from "@arkecosystem/core-interfaces";
+import { SnapshotManager } from "@arkecosystem/core-snapshots";
+import { flags } from "@oclif/command";
+import fs from "fs-extra";
+import { setUpLite } from "../utils";
+import { BaseCommand } from "./command";
+
+export class DumpCommand extends BaseCommand {
+ public static description: string = "create a full snapshot of the database";
+
+ public static flags = {
+ ...BaseCommand.flags,
+ blocks: flags.string({
+ description: "blocks to append to, correlates to folder name",
+ }),
+ start: flags.integer({
+ description: "start network height to export",
+ default: -1,
+ }),
+ end: flags.integer({
+ description: "end network height to export",
+ default: -1,
+ }),
+ codec: flags.string({
+ description: "codec name, default is msg-lite binary",
+ }),
+ };
+
+ public async run(): Promise {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { flags } = this.parse(DumpCommand);
+
+ await setUpLite(flags);
+
+ await app.resolvePlugin("snapshots").exportData(flags);
+ }
+}
diff --git a/core-snapshots-cli/src/commands/restore.ts b/core-snapshots-cli/src/commands/restore.ts
new file mode 100644
index 0000000..0f2ea79
--- /dev/null
+++ b/core-snapshots-cli/src/commands/restore.ts
@@ -0,0 +1,61 @@
+import { app } from "@arkecosystem/core-container";
+import { EventEmitter } from "@arkecosystem/core-interfaces";
+import { SnapshotManager } from "@arkecosystem/core-snapshots";
+import { flags } from "@oclif/command";
+import _cliProgress from "cli-progress";
+import { setUpLite } from "../utils";
+import { BaseCommand } from "./command";
+
+export class RestoreCommand extends BaseCommand {
+ public static description: string = "import data from specified snapshot";
+
+ public static flags = {
+ ...BaseCommand.flags,
+ blocks: flags.string({
+ description: "blocks to import, correlates to folder name",
+ required: true,
+ }),
+ codec: flags.string({
+ description: "codec name, default is msg-lite binary",
+ }),
+ truncate: flags.boolean({
+ description: "empty all tables before running import",
+ }),
+ skipRestartRound: flags.boolean({
+ description: "skip revert to current round",
+ }),
+ signatureVerify: flags.boolean({
+ description: "signature verification",
+ }),
+ };
+
+ public async run(): Promise {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { flags } = this.parse(RestoreCommand);
+
+ await setUpLite(flags);
+
+ const emitter = app.resolvePlugin("event-emitter");
+
+ const progressBar = new _cliProgress.Bar(
+ {
+ format: "{bar} {percentage}% | ETA: {eta}s | {value}/{total} | Duration: {duration}s",
+ },
+ _cliProgress.Presets.shades_classic,
+ );
+
+ emitter.on("start", data => {
+ progressBar.start(data.count, 1);
+ });
+
+ emitter.on("progress", data => {
+ progressBar.update(data.value);
+ });
+
+ emitter.on("complete", data => {
+ progressBar.stop();
+ });
+
+ await app.resolvePlugin("snapshots").importData(flags);
+ }
+}
diff --git a/core-snapshots-cli/src/commands/rollback.ts b/core-snapshots-cli/src/commands/rollback.ts
new file mode 100644
index 0000000..dfa7011
--- /dev/null
+++ b/core-snapshots-cli/src/commands/rollback.ts
@@ -0,0 +1,35 @@
+import { app } from "@arkecosystem/core-container";
+import { Logger } from "@arkecosystem/core-interfaces";
+import { SnapshotManager } from "@arkecosystem/core-snapshots";
+import { flags } from "@oclif/command";
+import { setUpLite } from "../utils";
+import { BaseCommand } from "./command";
+
+export class RollbackCommand extends BaseCommand {
+ public static description: string = "rollback chain to specified height";
+
+ public static flags = {
+ ...BaseCommand.flags,
+ height: flags.integer({
+ description: "block network height number to rollback",
+ default: -1,
+ }),
+ };
+
+ public async run(): Promise {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { flags } = this.parse(RollbackCommand);
+
+ await setUpLite(flags);
+
+ const logger = app.resolvePlugin("logger");
+
+ if (flags.height === -1) {
+ logger.warn("Rollback height is not specified. Rolling back to last completed round.");
+ }
+
+ logger.info(`Starting the process of blockchain rollback to block height of ${flags.height.toLocaleString()}`);
+
+ await app.resolvePlugin("snapshots").rollbackChain(flags.height);
+ }
+}
diff --git a/core-snapshots-cli/src/commands/truncate.ts b/core-snapshots-cli/src/commands/truncate.ts
new file mode 100644
index 0000000..1d6a8fa
--- /dev/null
+++ b/core-snapshots-cli/src/commands/truncate.ts
@@ -0,0 +1,17 @@
+import { app } from "@arkecosystem/core-container";
+import { SnapshotManager } from "@arkecosystem/core-snapshots";
+import { setUpLite } from "../utils";
+import { BaseCommand } from "./command";
+
+export class TruncateCommand extends BaseCommand {
+ public static description: string = "truncate blockchain database";
+
+ public async run(): Promise {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { flags } = this.parse(TruncateCommand);
+
+ await setUpLite(flags);
+
+ await app.resolvePlugin("snapshots").truncateChain();
+ }
+}
diff --git a/core-snapshots-cli/src/commands/verify.ts b/core-snapshots-cli/src/commands/verify.ts
new file mode 100644
index 0000000..a152cb9
--- /dev/null
+++ b/core-snapshots-cli/src/commands/verify.ts
@@ -0,0 +1,33 @@
+import { app } from "@arkecosystem/core-container";
+import { Logger } from "@arkecosystem/core-interfaces";
+import { SnapshotManager } from "@arkecosystem/core-snapshots";
+import { flags } from "@oclif/command";
+import fs from "fs-extra";
+import { setUpLite } from "../utils";
+import { BaseCommand } from "./command";
+
+export class VerifyCommand extends BaseCommand {
+ public static description: string = "check validity of specified snapshot";
+
+ public static flags = {
+ ...BaseCommand.flags,
+ blocks: flags.string({
+ description: "blocks to verify, correlates to folder name",
+ }),
+ codec: flags.string({
+ description: "codec name, default is msg-lite binary",
+ }),
+ signatureVerify: flags.boolean({
+ description: "signature verification",
+ }),
+ };
+
+ public async run(): Promise {
+ // tslint:disable-next-line:no-shadowed-variable
+ const { flags } = this.parse(VerifyCommand);
+
+ await setUpLite(flags);
+
+ await app.resolvePlugin("snapshots").verifyData(flags);
+ }
+}
diff --git a/core-snapshots-cli/src/index.ts b/core-snapshots-cli/src/index.ts
new file mode 100644
index 0000000..8bdb76f
--- /dev/null
+++ b/core-snapshots-cli/src/index.ts
@@ -0,0 +1 @@
+export { run } from "@oclif/command";
diff --git a/core-snapshots-cli/src/utils.ts b/core-snapshots-cli/src/utils.ts
new file mode 100644
index 0000000..1ef2a1b
--- /dev/null
+++ b/core-snapshots-cli/src/utils.ts
@@ -0,0 +1,18 @@
+import { app } from "@arkecosystem/core-container";
+
+export const setUpLite = async options => {
+ process.env.CORE_SKIP_BLOCKCHAIN = "true";
+
+ await app.setUp("2.0.0", options, {
+ include: [
+ "@arkecosystem/core-logger",
+ "@arkecosystem/core-logger-pino",
+ "@arkecosystem/core-event-emitter",
+ "@arkecosystem/core-snapshots",
+ ],
+ });
+
+ return app;
+};
+
+export const tearDown = async () => app.tearDown();
diff --git a/core-snapshots-cli/tsconfig.json b/core-snapshots-cli/tsconfig.json
new file mode 100644
index 0000000..0b089c5
--- /dev/null
+++ b/core-snapshots-cli/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src/**/**.ts"]
+}