Skip to content

Commit

Permalink
Merge pull request #33 from dkackman/https
Browse files Browse the repository at this point in the history
add Https support to daemon library
  • Loading branch information
dkackman committed Sep 4, 2023
2 parents 7a13e49 + 3ecd399 commit 2e735b9
Show file tree
Hide file tree
Showing 12 changed files with 3,189 additions and 1,998 deletions.
4,720 changes: 2,859 additions & 1,861 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
"packages/*"
],
"version": "0.0.3"
}
}
108 changes: 61 additions & 47 deletions packages/chia-daemon-tests/daemon.live.test.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
import chai from 'chai';
import { ChiaDaemon } from 'chia-daemon';
import _utils from 'chia-utils';
import chai from "chai";
import { ChiaDaemon, createConnection } from "chia-daemon";
import _utils from "chia-utils";

const expect = chai.expect;

const bad_connection = {
host: 'localhost',
port: 44444,
key_path: '~/.chia/mainnet/config/ssl/daemon/private_daemon.key',
cert_path: '~/.chia/mainnet/config/ssl/daemon/private_daemon.crt',
timeout_seconds: 5,
};

// some tests assume that a daemon is reachable with these details
const valid_connection = {
host: 'chiapas',
port: 55400,
key_path: '~/.chia/mainnet - chiapas/config/ssl/daemon/private_daemon.key',
cert_path: '~/.chia/mainnet - chiapas/config/ssl/daemon/private_daemon.crt',
timeout_seconds: 60,
};

describe('chia-daemon', () => {
describe('connection', () => {
it('should raise socket-error event on invalid connection', async () => {
const valid_connection = createConnection(
"daemon",
"localhost",
"e:/chia/mainnet",
60
);

describe("chia-daemon", () => {
describe("connection", () => {
it("should raise socket-error event on invalid connection", async () => {
const bad_connection = {
host: "localhost",
port: 44444,
key_path:
"~/.chia/mainnet/config/ssl/daemon/private_daemon.key",
cert_path:
"~/.chia/mainnet/config/ssl/daemon/private_daemon.crt",
timeout_seconds: 5,
};
let error = false;

const chia = new ChiaDaemon(bad_connection, 'tests');
chia.on('socket-error', e => {
const chia = new ChiaDaemon(bad_connection, "tests");
chia.on("socket-error", (e) => {
error = true;
});
const connected = await chia.connect();

expect(error).to.equal(true);
expect(connected).to.equal(false);
});
it('should return true on valid connection', async function () {
const chia = new ChiaDaemon(valid_connection, 'tests');
it("should return true on valid connection", async function () {
const chia = new ChiaDaemon(valid_connection, "tests");
let error = false;
chia.on('socket-error', e => {
chia.on("socket-error", (e) => {
console.log(e);
error = true;
});
Expand All @@ -49,10 +49,11 @@ describe('chia-daemon', () => {
expect(connected).to.equal(true);
});
});
describe('invocation', () => {
it('should get all the way to the rpc endpoint _DEBUG_', async function () {
describe("invocation", () => {
it("should get all the way to the rpc endpoint", async function () {
this.timeout(valid_connection.timeout_seconds * 1000);
const chia = new ChiaDaemon(valid_connection, 'tests');

const chia = new ChiaDaemon(valid_connection, "tests");

const connected = await chia.connect();
expect(connected).to.equal(true);
Expand All @@ -63,49 +64,61 @@ describe('chia-daemon', () => {

chia.disconnect();
});
it('should decode notification message', async function () {
it("should decode notification message", async function () {
this.timeout(valid_connection.timeout_seconds * 10000);
const chia = new ChiaDaemon(valid_connection, 'tests');
const chia = new ChiaDaemon(valid_connection, "tests");

const connected = await chia.connect();
expect(connected).to.equal(true);

const notifications = await chia.services.wallet.get_notifications({ start: 0, end: 1 });
const notifications = await chia.services.wallet.get_notifications({
start: 0,
end: 1,
});
expect(notifications).to.not.equal(undefined);
expect(notifications.notifications).to.not.equal(undefined);
expect(notifications.notifications.length).to.equal(1);

const n = notifications.notifications[0];

const text = Buffer.from(n.message, "hex").toString("utf8");
expect(text).to.equal('hello');

const coinResponse = await chia.services.full_node.get_coin_record_by_name({ name: n.id });
const parentCoinRecord = await getFirstSpentParentRecord(chia.services.full_node, coinResponse.coin_record.coin);

const address = _utils.puzzle_hash_to_address(parentCoinRecord.coin.puzzle_hash, 'txch');
expect(text).to.equal("hello");

const coinResponse =
await chia.services.full_node.get_coin_record_by_name({
name: n.id,
});
const parentCoinRecord = await getFirstSpentParentRecord(
chia.services.full_node,
coinResponse.coin_record.coin
);

const address = _utils.puzzle_hash_to_address(
parentCoinRecord.coin.puzzle_hash,
"txch"
);

chia.disconnect();
});
});
describe('listen', () => {
it('should capture an event', async function () {
describe("listen", () => {
it("should capture an event", async function () {
// this test requires the node under test to be plotting or otherwise
// be triggered to emit an event
const timeout_milliseconds = 100000;
this.timeout(timeout_milliseconds + 500);
const chia = new ChiaDaemon(valid_connection, 'wallet_ui');
const chia = new ChiaDaemon(valid_connection, "wallet_ui");

const connected = await chia.connect();
expect(connected).to.equal(true);

let event_received = false;
chia.on('event-message', m => event_received = true);
chia.on("event-message", (m) => (event_received = true));

const timer = ms => new Promise(res => setTimeout(res, ms));
const timer = (ms) => new Promise((res) => setTimeout(res, ms));
const start = Date.now();

///stay here intil we receive an event or timeout
///stay here until we receive an event or timeout
while (!event_received) {
await timer(100);
const elapsed = Date.now() - start;
Expand All @@ -120,9 +133,10 @@ describe('chia-daemon', () => {
});
});


async function getFirstSpentParentRecord(full_node, coin) {
const getCoinRecordResponse = await full_node.get_coin_record_by_name({ name: coin.parent_coin_info });
const getCoinRecordResponse = await full_node.get_coin_record_by_name({
name: coin.parent_coin_info,
});
const parentCoinRecord = getCoinRecordResponse.coin_record;
if (parentCoinRecord.spent === true) {
return parentCoinRecord;
Expand Down
41 changes: 41 additions & 0 deletions packages/chia-daemon-tests/https.live.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chai from "chai";
import { createHttpsService, createConnection } from "chia-daemon";
import _utils from "chia-utils";

const expect = chai.expect;

describe("chia-https", () => {
describe("invocation", () => {
it("should get all the way to the rpc endpoint", async function () {
const connection = createConnection(
"full_node",
"localhost",
"e:/chia/mainnet",
60
);
this.timeout(connection.timeout_seconds * 1000);

const full_node = createHttpsService(connection);

const state = await full_node.get_blockchain_state();
expect(state).to.not.equal(undefined);
expect(state).to.not.equal(null);
});
it("should pass arguments _DEBUG_", async function () {
const connection = createConnection(
"wallet",
"localhost",
"e:/chia/mainnet",
60
);
this.timeout(connection.timeout_seconds * 1000);

const wallet = createHttpsService(connection);

const response = await wallet.get_wallets({ include_data: true });
expect(response).to.not.equal(undefined);
expect(response.wallets[1].data).to.not.equal(undefined);
expect(response.wallets[1].data.length).to.be.greaterThan(0);
});
});
});
44 changes: 26 additions & 18 deletions packages/chia-daemon/chia_daemon.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { randomBytes } from "crypto";
import { WebSocket } from "ws";
import { readFileSync } from "fs";
import createRpcProxy from "./rpc_proxy.js";
import { EventEmitter } from "events";
import untildify from "./untildify.js";
import _ from "lodash";
import Connection from "./connection.js";

/** This can be found in the config but here for convenience. */
//
// deprecated - moved to connection_factory.js
export let localDaemonConnection = {
host: "localhost",
port: 55400,
Expand All @@ -29,23 +30,31 @@ export default class ChiaDaemon extends EventEmitter {
* @param {string} connection.key_path - File path to the certificate key file used to secure the connection.
* @param {string} connection.cert_path - File path to the certificate crt file used to secure the connection.
* @param {number} connection.timeout_seconds - Timeout, in seconds, for each call to the daemon.
* @param {string} service_name - the name of the client application or service talking to the daemon.
* @param {string} originServiceName - the name of the client application or service talking to the daemon.
*/
constructor(connection, service_name = "my_chia_app") {
constructor(connection, originServiceName = "my_chia_app") {
super();
if (connection === undefined) {
throw new Error("Connection meta data must be provided");
}

this.connection = connection;
this._service_name = service_name;
this.connection = new Connection(
"daemon",
connection.host,
connection.port,
connection.key_path,
connection.cert_path,
connection.timeout_seconds
);

this._originServiceName = originServiceName;
this.outgoing = new Map(); // outgoing messages awaiting a response
this.incoming = new Map(); // incoming responses not yet consumed
}

/** The service_name passed to the daemon as the message origin */
get serviceName() {
return this._service_name;
get originServiceName() {
return this._originServiceName;
}

/**
Expand Down Expand Up @@ -87,22 +96,20 @@ export default class ChiaDaemon extends EventEmitter {
throw new Error("Already connected");
}

const address = `wss://${this.connection.host}:${this.connection.port}`;
this.emit("connecting", address);
this.emit("connecting", this.connection.daemonAddress);

// the lifetime of the websocket is between connect and disconnect
const ws = new WebSocket(address, {
rejectUnauthorized: false,
key: readFileSync(untildify(this.connection.key_path)),
cert: readFileSync(untildify(this.connection.cert_path)),
});
const ws = new WebSocket(
this.connection.daemonAddress,
this.connection.createClientOptions()
);

ws.once("open", () => {
const msg = formatMessage(
"daemon",
"register_service",
this._service_name,
{ service: this._service_name }
this._originServiceName,
{ service: this._originServiceName }
);
ws.send(JSON.stringify(msg));
});
Expand All @@ -126,6 +133,7 @@ export default class ChiaDaemon extends EventEmitter {
}
});

// this is used below to wait for the socket to connect
let error = false;
ws.on("error", (e) => {
error = true;
Expand Down Expand Up @@ -181,7 +189,7 @@ export default class ChiaDaemon extends EventEmitter {
const outgoingMsg = formatMessage(
destination,
command,
this._service_name,
this._originServiceName,
data
);

Expand Down
63 changes: 63 additions & 0 deletions packages/chia-daemon/chia_https.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import _ from "lodash";
import Connection from "./connection.js";
import axios from "axios";
import https from "https";
import createRpcProxy from "./rpc_proxy.js";

export function createHttpsService(connection) {
if (connection === undefined) {
throw new Error("Connection meta data must be provided");
}

return createRpcProxy(new ChiaHttps(connection), connection.service);
}

export class ChiaHttps {
constructor(connection) {
if (connection === undefined) {
throw new Error("Connection meta data must be provided");
}

this.connection = new Connection(
connection.service,
connection.host,
connection.port,
connection.key_path,
connection.cert_path,
connection.timeout_seconds
);
}

get chiaServiceName() {
return this.connection.service;
}

async sendCommand(destination, command, data = {}) {
if (destination !== this.connection.service) {
// if this happens something is seriously wrong
throw new Error(
`Invalid destination ${destination} for service ${this.connection.service}`
);
}

// lazily create an axios instance
if (this.axios === undefined) {
const clientOptions = this.connection.createClientOptions();
this.axios = axios.create({
baseURL: this.connection.serviceAddress,
timeout: this.connection.timeout_seconds * 1000,
headers: {
accepts: "application/json",
"content-type": "application/json",
},
httpsAgent: new https.Agent(clientOptions),
});
}

const response = await this.axios.post(`/${command}`, data);
if (!response.data.success) {
throw new Error(response.data.error);
}
return response.data;
}
}
Loading

0 comments on commit 2e735b9

Please sign in to comment.