diff --git a/__tests__/integration/core-json-rpc/__support__/request.ts b/__tests__/integration/core-json-rpc/__support__/request.ts index cce749b2c0..dbe7d6d30b 100644 --- a/__tests__/integration/core-json-rpc/__support__/request.ts +++ b/__tests__/integration/core-json-rpc/__support__/request.ts @@ -2,7 +2,7 @@ import { httpie } from "@arkecosystem/core-utils"; import uuid from "uuid/v4"; export async function sendRequest(method, params: any = {}) { - const id = uuid(); + const id: string = uuid(); const response = await httpie.post("http://localhost:8080/", { body: { jsonrpc: "2.0", diff --git a/__tests__/integration/core-json-rpc/__support__/setup.ts b/__tests__/integration/core-json-rpc/__support__/setup.ts index 2f06ff3af0..d26ccd7d22 100644 --- a/__tests__/integration/core-json-rpc/__support__/setup.ts +++ b/__tests__/integration/core-json-rpc/__support__/setup.ts @@ -1,17 +1,9 @@ import { app } from "@arkecosystem/core-container"; import { tmpdir } from "os"; -import { registerWithContainer, setUpContainer } from "../../../utils/helpers/container"; +import { setUpContainer } from "../../../utils/helpers/container"; jest.setTimeout(60000); -const options = { - enabled: true, - host: "0.0.0.0", - port: 8080, - allowRemote: false, - whitelist: ["127.0.0.1", "::ffff:127.0.0.1"], -}; - export async function setUp() { // @ts-ignore process.env.CORE_JSON_RPC_ENABLED = true; @@ -19,18 +11,13 @@ export async function setUp() { process.env.CORE_PATH_CACHE = tmpdir(); await setUpContainer({ - exclude: ["@arkecosystem/core-webhooks", "@arkecosystem/core-forger", "@arkecosystem/core-json-rpc"], + exclude: ["@arkecosystem/core-webhooks", "@arkecosystem/core-forger"], + exit: "@arkecosystem/core-json-rpc", }); - const { plugin } = require("../../../../packages/core-json-rpc/src"); - await registerWithContainer(plugin, options); - return app; } export async function tearDown() { await app.tearDown(); - - const { plugin } = require("../../../../packages/core-json-rpc/src"); - await plugin.deregister(app, options); } diff --git a/__tests__/integration/core-json-rpc/blocks.test.ts b/__tests__/integration/core-json-rpc/blocks.test.ts index 5919b9d5fb..47a6ff09f4 100644 --- a/__tests__/integration/core-json-rpc/blocks.test.ts +++ b/__tests__/integration/core-json-rpc/blocks.test.ts @@ -1,6 +1,7 @@ +import "jest-extended"; + import { app } from "@arkecosystem/core-container"; import { Peer } from "@arkecosystem/core-p2p/src/peer"; -import "jest-extended"; import nock from "nock"; import { sendRequest } from "./__support__/request"; import { setUp, tearDown } from "./__support__/setup"; @@ -13,8 +14,7 @@ let mockHost; beforeAll(async () => { await setUp(); - peerMock = new Peer("1.0.0.99", 4000); - Object.assign(peerMock, peerMock.headers, { status: "OK" }); + peerMock = new Peer("1.0.0.99", 4003); // @NOTE: we use the Public API port app.resolvePlugin("p2p") .getStorage() @@ -22,23 +22,12 @@ beforeAll(async () => { nock("http://localhost", { allowUnmocked: true }); - mockHost = nock("http://localhost:4003"); + mockHost = nock(peerMock.url); }); -afterAll(async () => { - nock.cleanAll(); - await tearDown(); -}); +afterAll(async () => await tearDown()); -beforeEach(async () => { - nock(peerMock.url) - .get("/peer/status") - .reply(200, { success: true, height: 1 }, peerMock.headers); -}); - -afterEach(async () => { - nock.cleanAll(); -}); +afterEach(async () => nock.cleanAll()); describe("Blocks", () => { describe("POST blocks.latest", () => { diff --git a/__tests__/integration/core-json-rpc/transactions.test.ts b/__tests__/integration/core-json-rpc/transactions.test.ts index 2c7e69862a..5be1073c1a 100644 --- a/__tests__/integration/core-json-rpc/transactions.test.ts +++ b/__tests__/integration/core-json-rpc/transactions.test.ts @@ -1,7 +1,8 @@ +import "jest-extended"; + import { app } from "@arkecosystem/core-container"; import { Peer } from "@arkecosystem/core-p2p"; import { Crypto } from "@arkecosystem/crypto"; -import "jest-extended"; import nock from "nock"; import { sendRequest } from "./__support__/request"; import { setUp, tearDown } from "./__support__/setup"; @@ -16,8 +17,7 @@ let mockHost; beforeAll(async () => { await setUp(); - peerMock = new Peer("1.0.0.99", 4000); - Object.assign(peerMock, peerMock.headers, { status: "OK" }); + peerMock = new Peer("1.0.0.99", 4003); // @NOTE: we use the Public API port app.resolvePlugin("p2p") .getStorage() @@ -25,22 +25,12 @@ beforeAll(async () => { nock("http://localhost", { allowUnmocked: true }); - mockHost = nock("http://localhost:4003"); + mockHost = nock(peerMock.url); }); -afterAll(async () => { - nock.cleanAll(); - await tearDown(); -}); -beforeEach(async () => { - nock(peerMock.url) - .get("/peer/status") - .reply(200, { success: true, height: 1 }, peerMock.headers); -}); +afterAll(async () => await tearDown()); -afterEach(async () => { - nock.cleanAll(); -}); +afterEach(async () => nock.cleanAll()); describe("Transactions", () => { describe("POST transactions.info", () => { @@ -118,9 +108,10 @@ describe("Transactions", () => { describe("POST transactions.bip38.create", () => { it("should create a new transaction", async () => { - const userId = require("crypto") + const userId: string = require("crypto") .randomBytes(32) .toString("hex"); + await sendRequest("wallets.bip38.create", { bip38: "this is a top secret passphrase", userId, diff --git a/__tests__/integration/core-json-rpc/wallets.test.ts b/__tests__/integration/core-json-rpc/wallets.test.ts index f041018905..94d0ec9675 100644 --- a/__tests__/integration/core-json-rpc/wallets.test.ts +++ b/__tests__/integration/core-json-rpc/wallets.test.ts @@ -14,7 +14,7 @@ let mockHost; beforeAll(async () => { await setUp(); - peerMock = new Peer("1.0.0.99", 4000); + peerMock = new Peer("1.0.0.99", 4003); // @NOTE: we use the Public API port app.resolvePlugin("p2p") .getStorage() @@ -22,46 +22,18 @@ beforeAll(async () => { nock("http://localhost", { allowUnmocked: true }); - mockHost = nock("http://localhost:4003"); + mockHost = nock(peerMock.url); }); -afterAll(async () => { - nock.cleanAll(); - await tearDown(); -}); +afterAll(async () => await tearDown()); beforeEach(async () => { nock(peerMock.url) .get("/api/loader/autoconfigure") .reply(200, { network: {} }, peerMock.headers); - - nock(peerMock.url) - .get("/peer/status") - .reply(200, { success: true, height: 5 }, peerMock.headers); - - nock(peerMock.url) - .get("/peer/list") - .reply( - 200, - { - success: true, - peers: [ - { - status: "OK", - ip: peerMock.ip, - port: 4002, - height: 5, - latency: 8, - }, - ], - }, - peerMock.headers, - ); }); -afterEach(async () => { - nock.cleanAll(); -}); +afterEach(async () => nock.cleanAll()); describe("Wallets", () => { describe("POST wallets.info", () => { @@ -118,6 +90,15 @@ describe("Wallets", () => { }); it("should fail to get transactions for the given wallet", async () => { + mockHost + .get("/api/transactions") + .query({ + offset: 0, + orderBy: "timestamp:desc", + ownerId: "AUDud8tvyVZa67p3QY7XPRUTjRGnWQQ9Xv", + }) + .reply(200, { meta: { totalCount: 0 }, data: [] }, peerMock.headers); + const response = await sendRequest("wallets.transactions", { address: "AUDud8tvyVZa67p3QY7XPRUTjRGnWQQ9Xv", }); diff --git a/packages/core-json-rpc/src/interfaces.ts b/packages/core-json-rpc/src/interfaces.ts new file mode 100644 index 0000000000..6ffbc2602c --- /dev/null +++ b/packages/core-json-rpc/src/interfaces.ts @@ -0,0 +1,22 @@ +import { Interfaces } from "@arkecosystem/crypto"; + +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/packages/core-json-rpc/src/server/index.ts b/packages/core-json-rpc/src/server/index.ts index ef5b906cae..08c1e9d074 100755 --- a/packages/core-json-rpc/src/server/index.ts +++ b/packages/core-json-rpc/src/server/index.ts @@ -1,7 +1,7 @@ import { app } from "@arkecosystem/core-container"; import { createServer, mountServer, plugins } from "@arkecosystem/core-http-utils"; import { Logger } from "@arkecosystem/core-interfaces"; -import { registerMethods } from "./methods"; +import * as modules from "./modules"; import { Processor } from "./services/processor"; export async function startServer(options) { @@ -29,13 +29,21 @@ export async function startServer(options) { }); } - // @ts-ignore - registerMethods(server); + 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, h) { + async handler(request) { const processor = new Processor(); return Array.isArray(request.payload) diff --git a/packages/core-json-rpc/src/server/methods/blocks/info.ts b/packages/core-json-rpc/src/server/methods/blocks/info.ts deleted file mode 100644 index 87ff9e53ac..0000000000 --- a/packages/core-json-rpc/src/server/methods/blocks/info.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Boom from "boom"; -import Joi from "joi"; -import { network } from "../../services/network"; - -export const blockInfo = { - name: "blocks.info", - async method(params) { - const response = await network.sendRequest(`blocks/${params.id}`); - - return response ? response.data : Boom.notFound(`Block ${params.id} could not be found.`); - }, - schema: { - id: Joi.number() - // @ts-ignore - .unsafe() - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/blocks/latest.ts b/packages/core-json-rpc/src/server/methods/blocks/latest.ts deleted file mode 100644 index b13b4b6e13..0000000000 --- a/packages/core-json-rpc/src/server/methods/blocks/latest.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Boom from "boom"; -import { network } from "../../services/network"; - -export const blockLatest = { - name: "blocks.latest", - async method() { - const response = await network.sendRequest("blocks", { orderBy: "height:desc", limit: 1 }); - - return response ? response.data[0] : Boom.notFound(`Latest block could not be found.`); - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/blocks/transactions.ts b/packages/core-json-rpc/src/server/methods/blocks/transactions.ts deleted file mode 100644 index 3bd0aae721..0000000000 --- a/packages/core-json-rpc/src/server/methods/blocks/transactions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Boom from "boom"; -import Joi from "joi"; -import { network } from "../../services/network"; - -export const blockTransactions = { - name: "blocks.transactions", - async method(params) { - const response = await network.sendRequest(`blocks/${params.id}/transactions`, { - offset: params.offset, - orderBy: "timestamp:desc", - }); - - return response - ? { - count: response.meta.totalCount, - data: response.data, - } - : Boom.notFound(`Block ${params.id} could not be found.`); - }, - schema: { - id: Joi.number() - // @ts-ignore - .unsafe() - .required(), - offset: Joi.number().default(0), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/index.ts b/packages/core-json-rpc/src/server/methods/index.ts deleted file mode 100644 index 536380626e..0000000000 --- a/packages/core-json-rpc/src/server/methods/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Hapi from "hapi"; - -import { blockInfo } from "./blocks/info"; -import { blockLatest } from "./blocks/latest"; -import { blockTransactions } from "./blocks/transactions"; - -import { walletBIP38Create } from "./wallets/bip38/create"; -import { walletBIP38 } from "./wallets/bip38/show"; -import { walletCreate } from "./wallets/create"; -import { walletInfo } from "./wallets/info"; -import { walletTransactions } from "./wallets/transactions"; - -import { transactionBIP38Create } from "./transactions/bip38/create"; -import { transactionBroadcast } from "./transactions/broadcast"; -import { transactionCreate } from "./transactions/create"; -import { transactionInfo } from "./transactions/info"; - -export function registerMethods(server: Hapi.Server) { - const registerMethod = method => { - // @ts-ignore - server.app.schemas[method.name] = method.schema; - - delete method.schema; - - server.method(method); - }; - - registerMethod(blockLatest); - registerMethod(blockInfo); - registerMethod(blockTransactions); - registerMethod(walletBIP38Create); - registerMethod(walletBIP38); - registerMethod(walletCreate); - registerMethod(walletInfo); - registerMethod(walletTransactions); - registerMethod(transactionBIP38Create); - registerMethod(transactionBroadcast); - registerMethod(transactionCreate); - registerMethod(transactionInfo); -} diff --git a/packages/core-json-rpc/src/server/methods/transactions/bip38/create.ts b/packages/core-json-rpc/src/server/methods/transactions/bip38/create.ts deleted file mode 100644 index ee3e767036..0000000000 --- a/packages/core-json-rpc/src/server/methods/transactions/bip38/create.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Transactions } from "@arkecosystem/crypto"; -import Boom from "boom"; -import Joi from "joi"; -import { database } from "../../../services/database"; -import { getBIP38Wallet } from "../../../utils/bip38-keys"; - -export const transactionBIP38Create = { - name: "transactions.bip38.create", - async method(params) { - const wallet = await getBIP38Wallet(params.userId, params.bip38); - - if (!wallet) { - return Boom.notFound(`User ${params.userId} could not be found.`); - } - - const transaction = Transactions.BuilderFactory.transfer() - .recipientId(params.recipientId) - .amount(params.amount) - .signWithWif(wallet.wif) - .getStruct(); - - await database.set(transaction.id, transaction); - - return transaction; - }, - schema: { - amount: Joi.number().required(), - recipientId: Joi.string() - .length(34) - .required(), - bip38: Joi.string().required(), - userId: Joi.string() - .hex() - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/transactions/broadcast.ts b/packages/core-json-rpc/src/server/methods/transactions/broadcast.ts deleted file mode 100644 index 7338947e3b..0000000000 --- a/packages/core-json-rpc/src/server/methods/transactions/broadcast.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Crypto } from "@arkecosystem/crypto"; -import Boom from "boom"; -import Joi from "joi"; -import { database } from "../../services/database"; -import { network } from "../../services/network"; - -export const transactionBroadcast = { - name: "transactions.broadcast", - async method(params) { - const transaction = await database.get(params.id); - - if (!transaction) { - return Boom.notFound(`Transaction ${params.id} could not be found.`); - } - - if (!Crypto.crypto.verify(transaction)) { - return Boom.badData(); - } - - await network.broadcast(transaction); - - return transaction; - }, - schema: { - id: Joi.string().length(64), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/transactions/create.ts b/packages/core-json-rpc/src/server/methods/transactions/create.ts deleted file mode 100644 index 9146c4cb8a..0000000000 --- a/packages/core-json-rpc/src/server/methods/transactions/create.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Transactions } from "@arkecosystem/crypto"; -import Joi from "joi"; -import { database } from "../../services/database"; - -export const transactionCreate = { - name: "transactions.create", - async method(params) { - const transaction = Transactions.BuilderFactory.transfer() - .recipientId(params.recipientId) - .amount(params.amount) - .sign(params.passphrase) - .getStruct(); - - await database.set(transaction.id, transaction); - - return transaction; - }, - schema: { - amount: Joi.number().required(), - recipientId: Joi.string().required(), - passphrase: Joi.string().required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/transactions/info.ts b/packages/core-json-rpc/src/server/methods/transactions/info.ts deleted file mode 100644 index 3a4cbe4446..0000000000 --- a/packages/core-json-rpc/src/server/methods/transactions/info.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Boom from "boom"; -import Joi from "joi"; -import { network } from "../../services/network"; - -export const transactionInfo = { - name: "transactions.info", - async method(params) { - const response = await network.sendRequest(`transactions/${params.id}`); - - return response ? response.data : Boom.notFound(`Transaction ${params.id} could not be found.`); - }, - schema: { - id: Joi.string() - .length(64) - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/wallets/bip38/create.ts b/packages/core-json-rpc/src/server/methods/wallets/bip38/create.ts deleted file mode 100644 index 8419415d74..0000000000 --- a/packages/core-json-rpc/src/server/methods/wallets/bip38/create.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Crypto } from "@arkecosystem/crypto"; -import { generateMnemonic } from "bip39"; -import Joi from "joi"; -import { database } from "../../../services/database"; -import { getBIP38Wallet } from "../../../utils/bip38-keys"; -import { decryptWIF } from "../../../utils/decrypt-wif"; - -export const walletBIP38Create = { - name: "wallets.bip38.create", - async method(params) { - try { - const { keys, wif } = await getBIP38Wallet(params.userId, params.bip38); - - return { - publicKey: keys.publicKey, - address: Crypto.crypto.getAddress(keys.publicKey), - wif, - }; - } catch (error) { - const { publicKey, privateKey } = Crypto.crypto.getKeys(generateMnemonic()); - - const encryptedWIF = 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); - - const { wif } = decryptWIF(encryptedWIF, params.userId, params.bip38); - - return { - publicKey, - address: Crypto.crypto.getAddress(publicKey), - wif, - }; - } - }, - schema: { - bip38: Joi.string().required(), - userId: Joi.string() - .hex() - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/wallets/bip38/show.ts b/packages/core-json-rpc/src/server/methods/wallets/bip38/show.ts deleted file mode 100644 index 8afd8cdfd6..0000000000 --- a/packages/core-json-rpc/src/server/methods/wallets/bip38/show.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Crypto } from "@arkecosystem/crypto"; -import Boom from "boom"; -import Joi from "joi"; -import { database } from "../../../services/database"; -import { decryptWIF } from "../../../utils/decrypt-wif"; - -export const walletBIP38 = { - name: "wallets.bip38.info", - async method(params) { - const encryptedWIF = 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 } = decryptWIF(encryptedWIF, params.userId, params.bip38); - - return { - publicKey: keys.publicKey, - address: Crypto.crypto.getAddress(keys.publicKey), - wif, - }; - }, - schema: { - bip38: Joi.string().required(), - userId: Joi.string() - .hex() - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/wallets/create.ts b/packages/core-json-rpc/src/server/methods/wallets/create.ts deleted file mode 100644 index 07c3466c6c..0000000000 --- a/packages/core-json-rpc/src/server/methods/wallets/create.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Crypto } from "@arkecosystem/crypto"; -import Joi from "joi"; - -export const walletCreate = { - name: "wallets.create", - async method(params) { - const { publicKey } = Crypto.crypto.getKeys(params.passphrase); - - return { - publicKey, - address: Crypto.crypto.getAddress(publicKey), - }; - }, - schema: { - passphrase: Joi.string().required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/wallets/info.ts b/packages/core-json-rpc/src/server/methods/wallets/info.ts deleted file mode 100644 index 96dd4ca712..0000000000 --- a/packages/core-json-rpc/src/server/methods/wallets/info.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Boom from "boom"; -import Joi from "joi"; -import { network } from "../../services/network"; - -export const walletInfo = { - name: "wallets.info", - async method(params) { - const response = await network.sendRequest(`wallets/${params.address}`); - - return response ? response.data : Boom.notFound(`Wallet ${params.address} could not be found.`); - }, - schema: { - address: Joi.string() - .length(34) - .required(), - }, -}; diff --git a/packages/core-json-rpc/src/server/methods/wallets/transactions.ts b/packages/core-json-rpc/src/server/methods/wallets/transactions.ts deleted file mode 100644 index 645efec677..0000000000 --- a/packages/core-json-rpc/src/server/methods/wallets/transactions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Boom from "boom"; -import Joi from "joi"; -import { network } from "../../services/network"; - -export const walletTransactions = { - name: "wallets.transactions", - async method(params) { - const response = await network.sendRequest("transactions", { - 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: { - address: Joi.string() - .length(34) - .required(), - offset: Joi.number().default(0), - }, -}; diff --git a/packages/core-json-rpc/src/server/modules/blocks.ts b/packages/core-json-rpc/src/server/modules/blocks.ts new file mode 100644 index 0000000000..75b50fc800 --- /dev/null +++ b/packages/core-json-rpc/src/server/modules/blocks.ts @@ -0,0 +1,63 @@ +import Boom from "boom"; +import Joi from "joi"; +import { network } from "../services/network"; + +export const blockInfo = { + name: "blocks.info", + async method(params) { + const response = await network.sendRequest({ url: `blocks/${params.id}` }); + + if (!response) { + return Boom.notFound(`Block ${params.id} could not be found.`); + } + + return response.data; + }, + schema: { + id: Joi.number() + // @ts-ignore + .unsafe() + .required(), + }, +}; + +export const blockLatest = { + name: "blocks.latest", + async method() { + const response = await network.sendRequest({ + url: "blocks", + query: { orderBy: "height:desc", limit: 1 }, + }); + + return response ? response.data[0] : Boom.notFound(`Latest block could not be found.`); + }, +}; + +export const blockTransactions = { + name: "blocks.transactions", + async method(params) { + const response = await network.sendRequest({ + url: `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: { + id: Joi.number() + // @ts-ignore + .unsafe() + .required(), + offset: Joi.number().default(0), + }, +}; diff --git a/packages/core-json-rpc/src/server/modules/index.ts b/packages/core-json-rpc/src/server/modules/index.ts new file mode 100644 index 0000000000..d0ad34373f --- /dev/null +++ b/packages/core-json-rpc/src/server/modules/index.ts @@ -0,0 +1,5 @@ +import * as blocks from "./blocks"; +import * as transactions from "./transactions"; +import * as wallets from "./wallets"; + +export { blocks, transactions, wallets }; diff --git a/packages/core-json-rpc/src/server/modules/transactions.ts b/packages/core-json-rpc/src/server/modules/transactions.ts new file mode 100644 index 0000000000..d625ba32e9 --- /dev/null +++ b/packages/core-json-rpc/src/server/modules/transactions.ts @@ -0,0 +1,99 @@ +import { Crypto, Interfaces, Transactions } from "@arkecosystem/crypto"; +import { ITransactionData } from "@arkecosystem/crypto/dist/interfaces"; +import Boom from "boom"; +import Joi from "joi"; +import { IWallet } from "../../interfaces"; +import { database } from "../services/database"; +import { network } from "../services/network"; +import { getBIP38Wallet } from "../utils"; + +export const transactionBroadcast = { + name: "transactions.broadcast", + async method(params) { + const transaction: ITransactionData = await database.get(params.id); + + if (!transaction) { + return Boom.notFound(`Transaction ${params.id} could not be found.`); + } + + if (!Crypto.crypto.verify(transaction)) { + return Boom.badData(); + } + + await network.broadcast(transaction); + + return transaction; + }, + schema: { + id: Joi.string().length(64), + }, +}; + +export const transactionCreate = { + name: "transactions.create", + async method(params) { + const transaction: Interfaces.ITransactionData = Transactions.BuilderFactory.transfer() + .recipientId(params.recipientId) + .amount(params.amount) + .sign(params.passphrase) + .getStruct(); + + await database.set(transaction.id, transaction); + + return transaction; + }, + schema: { + amount: Joi.number().required(), + recipientId: Joi.string().required(), + passphrase: Joi.string().required(), + }, +}; + +export const transactionInfo = { + name: "transactions.info", + async method(params) { + const response = await network.sendRequest({ url: `transactions/${params.id}` }); + + if (!response) { + return Boom.notFound(`Transaction ${params.id} could not be found.`); + } + + return response.data; + }, + schema: { + id: Joi.string() + .length(64) + .required(), + }, +}; + +export const transactionBIP38Create = { + name: "transactions.bip38.create", + async method(params) { + const wallet: IWallet = await getBIP38Wallet(params.userId, params.bip38); + + if (!wallet) { + return Boom.notFound(`User ${params.userId} could not be found.`); + } + + const transaction: Interfaces.ITransactionData = Transactions.BuilderFactory.transfer() + .recipientId(params.recipientId) + .amount(params.amount) + .signWithWif(wallet.wif) + .getStruct(); + + await database.set(transaction.id, transaction); + + return transaction; + }, + schema: { + amount: Joi.number().required(), + recipientId: Joi.string() + .length(34) + .required(), + bip38: Joi.string().required(), + userId: Joi.string() + .hex() + .required(), + }, +}; diff --git a/packages/core-json-rpc/src/server/modules/wallets.ts b/packages/core-json-rpc/src/server/modules/wallets.ts new file mode 100644 index 0000000000..3e079e0306 --- /dev/null +++ b/packages/core-json-rpc/src/server/modules/wallets.ts @@ -0,0 +1,134 @@ +import { Crypto, Interfaces } from "@arkecosystem/crypto"; +import { generateMnemonic } from "bip39"; +import Boom from "boom"; +import Joi from "joi"; +import { IWallet } from "../../interfaces"; +import { database } from "../services/database"; +import { network } from "../services/network"; +import { decryptWIF, getBIP38Wallet } from "../utils"; + +export const walletCreate = { + name: "wallets.create", + async method(params) { + const { publicKey }: Interfaces.IKeyPair = Crypto.crypto.getKeys(params.passphrase); + + return { + publicKey, + address: Crypto.crypto.getAddress(publicKey), + }; + }, + schema: { + passphrase: Joi.string().required(), + }, +}; + +export const walletInfo = { + name: "wallets.info", + async method(params) { + const response = await network.sendRequest({ url: `wallets/${params.address}` }); + + if (!response) { + return Boom.notFound(`Wallet ${params.address} could not be found.`); + } + + return response.data; + }, + schema: { + address: Joi.string() + .length(34) + .required(), + }, +}; + +export const walletTransactions = { + name: "wallets.transactions", + async method(params) { + const response = await network.sendRequest({ + url: "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: { + address: Joi.string() + .length(34) + .required(), + offset: Joi.number().default(0), + }, +}; + +export const walletBIP38Create = { + name: "wallets.bip38.create", + async method(params) { + try { + const { keys, wif }: IWallet = await getBIP38Wallet(params.userId, params.bip38); + + return { + publicKey: keys.publicKey, + address: Crypto.crypto.getAddress(keys.publicKey), + wif, + }; + } catch (error) { + const { publicKey, privateKey }: Interfaces.IKeyPair = Crypto.crypto.getKeys(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: Crypto.crypto.getAddress(publicKey), + wif: decryptWIF(encryptedWIF, params.userId, params.bip38).wif, + }; + } + }, + schema: { + bip38: Joi.string().required(), + userId: Joi.string() + .hex() + .required(), + }, +}; + +export const walletBIP38 = { + name: "wallets.bip38.info", + async method(params) { + const encryptedWIF = 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 } = decryptWIF(encryptedWIF, params.userId, params.bip38); + + return { + publicKey: keys.publicKey, + address: Crypto.crypto.getAddress(keys.publicKey), + wif, + }; + }, + schema: { + bip38: Joi.string().required(), + userId: Joi.string() + .hex() + .required(), + }, +}; diff --git a/packages/core-json-rpc/src/server/services/database.ts b/packages/core-json-rpc/src/server/services/database.ts index e734dbd5cc..5cb2f58cb6 100644 --- a/packages/core-json-rpc/src/server/services/database.ts +++ b/packages/core-json-rpc/src/server/services/database.ts @@ -7,20 +7,20 @@ class Database { this.database = new Keyv(options); } - public async get(id) { + public async get(id: string): Promise { return this.database.get(id); } - public async set(id, value) { - return this.database.set(id, value); + public async set(id: string, value: T): Promise { + this.database.set(id, value); } - public async delete(id) { - return this.database.delete(id); + public async delete(id: string): Promise { + this.database.delete(id); } - public async clear() { - return this.database.clear(); + public async clear(): Promise { + this.database.clear(); } } diff --git a/packages/core-json-rpc/src/server/services/network.ts b/packages/core-json-rpc/src/server/services/network.ts index fafd7bc5a3..7cdefc0d98 100644 --- a/packages/core-json-rpc/src/server/services/network.ts +++ b/packages/core-json-rpc/src/server/services/network.ts @@ -1,14 +1,15 @@ 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 { Managers } from "@arkecosystem/crypto"; +import { Interfaces, Managers } from "@arkecosystem/crypto"; import isReachable from "is-reachable"; import sample from "lodash.sample"; class Network { private peers: P2P.IPeer[]; private server: P2P.IPeer; - private readonly network: any = Managers.configManager.all(); + 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"); private readonly requestOpts: Record = { @@ -19,35 +20,35 @@ class Network { timeout: 3000, }; - public async init() { + public async init(): Promise { this.loadRemotePeers(); } - public setServer() { + public setServer(): void { this.server = this.getRandomPeer(); } - public async sendRequest(url, query = {}) { + public async sendRequest({ url, query = {} }: { url: string; query?: Record }): Promise { if (!this.server) { this.setServer(); } - const peer = await this.selectResponsivePeer(this.server); - const uri = `http://${peer.ip}:${peer.port}/api/${url}`; - try { - this.logger.info(`Sending request on "${this.network.name}" to "${uri}"`); + const peer: P2P.IPeer = await this.selectResponsivePeer(this.server); + const uri: string = `http://${peer.ip}:${peer.port}/api/${url}`; - const response = await httpie.get(uri, { query, ...this.requestOpts }); + this.logger.info(`Sending request on "${this.network.name}" to "${uri}"`); - return response.body; + return (await httpie.get(uri, { query, ...this.requestOpts })).body; } catch (error) { this.logger.error(error.message); } + + return undefined; } - public async broadcast(transaction) { - return httpie.post(`http://${this.server.ip}:${this.server.port}/api/transactions`, { + public async broadcast(transaction): Promise { + await httpie.post(`http://${this.server.ip}:${this.server.port}/api/transactions`, { body: { transactions: [transaction], }, @@ -55,58 +56,48 @@ class Network { }); } - public async connect(): Promise { + public async connect(): Promise { if (this.server) { - // this.logger.info(`Server is already configured as "${this.server.ip}:${this.server.port}"`) - return true; + return; } this.setServer(); try { - const peerPort = app.resolveOptions("p2p").port; - const response = await httpie.get(`http://${this.server.ip}:${peerPort}/config`); - - const plugin = response.body.data.plugins["@arkecosystem/core-api"]; - - if (!plugin.enabled) { - const index = this.peers.findIndex(peer => peer.ip === this.server.ip); - this.peers.splice(index, 1); - - if (!this.peers.length) { - this.loadRemotePeers(); - } + await httpie.get(`http://${this.server.ip}:${this.server.port}/api/loader/autoconfigure`); + } catch (error) { + this.peers.splice(this.peers.findIndex(peer => peer.ip === this.server.ip), 1); - return this.connect(); + if (!this.peers.length) { + this.loadRemotePeers(); } - this.server.port = plugin.port; - } catch (error) { return this.connect(); } } - private getRandomPeer() { + private getRandomPeer(): P2P.IPeer { this.loadRemotePeers(); - return sample(this.peers); + const peer: P2P.IPeer = sample(this.peers); + peer.port = app.resolveOptions("api").port; + + return peer; } private loadRemotePeers(): void { - if (this.network.name === "testnet") { - // @ts-ignore - @TODO: make this a peer instance - this.peers = [{ ip: "localhost", port: app.resolveOptions("api").port }]; - } else { - this.peers = this.p2p.getStorage().getPeers(); + this.peers = this.p2p.getStorage().getPeers(); + + if (!this.peers.length && this.network.name === "testnet") { + this.peers = [new Peer("127.0.0.1", app.resolveOptions("api").port)]; } if (!this.peers.length) { - this.logger.error("No peers found. Shutting down..."); - process.exit(); + app.forceExit("No peers found. Shutting down..."); } } - private async selectResponsivePeer(peer) { + private async selectResponsivePeer(peer: P2P.IPeer): Promise { if (!(await isReachable(`${peer.ip}:${peer.port}`))) { this.logger.warn(`${peer} is unresponsive. Choosing new peer.`); diff --git a/packages/core-json-rpc/src/server/services/processor.ts b/packages/core-json-rpc/src/server/services/processor.ts index 3ccae294da..1c4e261cb2 100644 --- a/packages/core-json-rpc/src/server/services/processor.ts +++ b/packages/core-json-rpc/src/server/services/processor.ts @@ -1,9 +1,12 @@ +import { Server } from "hapi"; import Joi from "joi"; import get from "lodash.get"; +import { IResponse, IResponseError } from "../../interfaces"; import { network } from "./network"; export class Processor { - public async resource(server, payload) { + public async resource(server: Server, payload) { + // @TODO: replace Joi with AJV const { error } = Joi.validate(payload || {}, { jsonrpc: Joi.string() .valid("2.0") @@ -23,9 +26,10 @@ export class Processor { const targetMethod = get(server.methods, method); if (!targetMethod) { - return this.createErrorResponse(id, -32601, "The method does not exist / is not available."); + 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) { @@ -61,7 +65,7 @@ export class Processor { return results; } - private createSuccessResponse(id, result) { + private createSuccessResponse(id: string | number, result: T): IResponse { return { jsonrpc: "2.0", id, @@ -69,7 +73,7 @@ export class Processor { }; } - private createErrorResponse(id, code, error) { + private createErrorResponse(id: string | number, code: number, error: Error): IResponseError { return { jsonrpc: "2.0", id, diff --git a/packages/core-json-rpc/src/server/utils.ts b/packages/core-json-rpc/src/server/utils.ts new file mode 100644 index 0000000000..9be9f4437e --- /dev/null +++ b/packages/core-json-rpc/src/server/utils.ts @@ -0,0 +1,31 @@ +import { Crypto, Interfaces, Managers } from "@arkecosystem/crypto"; +import wif from "wif"; +import { IWallet } from "../interfaces"; +import { database } from "./services/database"; + +export async function getBIP38Wallet(userId, bip38password): Promise { + try { + const encryptedWif: string = await database.get( + Crypto.HashAlgorithms.sha256(Buffer.from(userId)).toString("hex"), + ); + + return encryptedWif ? decryptWIF(encryptedWif, userId, bip38password) : undefined; + } catch (error) { + throw new Error("Could not find a matching WIF"); + } +} + +export function 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: Crypto.crypto.getKeysFromWIF(encodedWIF), wif: encodedWIF }; +} diff --git a/packages/core-json-rpc/src/server/utils/bip38-keys.ts b/packages/core-json-rpc/src/server/utils/bip38-keys.ts deleted file mode 100644 index 0fcecccd37..0000000000 --- a/packages/core-json-rpc/src/server/utils/bip38-keys.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Crypto } from "@arkecosystem/crypto"; -import { database } from "../services/database"; -import { decryptWIF } from "./decrypt-wif"; - -export async function getBIP38Wallet(userId, bip38password): Promise { - try { - const encryptedWif = await database.get(Crypto.HashAlgorithms.sha256(Buffer.from(userId)).toString("hex")); - - if (encryptedWif) { - return decryptWIF(encryptedWif, userId, bip38password); - } - } catch (error) { - throw Error("Could not find a matching WIF"); - // TODO: Unreachable code. What was the intention here? To have it return a boolean or throw an Error? - return false; - } -} diff --git a/packages/core-json-rpc/src/server/utils/decrypt-wif.ts b/packages/core-json-rpc/src/server/utils/decrypt-wif.ts deleted file mode 100644 index 2fa69e827a..0000000000 --- a/packages/core-json-rpc/src/server/utils/decrypt-wif.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Crypto, Managers } from "@arkecosystem/crypto"; -import wif from "wif"; - -export const decryptWIF = (encryptedWif, userId, bip38password) => { - const decrypted = Crypto.bip38.decrypt(encryptedWif.toString("hex"), bip38password + userId); - - const encodedWIF = wif.encode( - Managers.configManager.get("network.wif"), - decrypted.privateKey, - decrypted.compressed, - ); - - return { keys: Crypto.crypto.getKeysFromWIF(encodedWIF), wif: encodedWIF }; -};