Skip to content

Commit

Permalink
Added GetRuntimeStatus Endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
nhenin committed Jan 3, 2024
1 parent 2de6c0d commit 8bc1e12
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 12 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20240103_085440_nicolas.henin_runtimeStatus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### @marlowe.io/runtime-rest-client

- Added a new endpoint `GetRuntimeStatus` that retrieves informations about the Runtime (version deployed, Network Id of the Node and tips)
8 changes: 8 additions & 0 deletions packages/adapter/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ const getWithDataAndHeaders = TE.bimap(
(v: AxiosResponse): any => [v.headers, v.data]
);

const getWithHeaders = TE.bimap(
(e: unknown) => (e instanceof Error ? e : new Error(String(e))),
(v: AxiosResponse): any => v.headers
);

export const Get = (request: AxiosInstance) =>
flow(TE.tryCatchK(request.get, identity), getOnlyData);

export const GetWithDataAndHeaders = (request: AxiosInstance) =>
flow(TE.tryCatchK(request.get, identity), getWithDataAndHeaders);

export const GetWithHeaders = (request: AxiosInstance) =>
flow(TE.tryCatchK(request.get, identity), getWithHeaders);

export const Post = (request: AxiosInstance) =>
flow(TE.tryCatchK(request.post, identity), getOnlyData);

Expand Down
2 changes: 1 addition & 1 deletion packages/adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * as MarloweJSON from "./codec.js";
export * as Codec from "./codec.js";
export * as Time from "./time.js";
5 changes: 5 additions & 0 deletions packages/runtime/client/rest/src/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ export {
} from "./contract/rolesConfigurations.js";

export { ContractDetailsGuard as ContractDetails } from "./contract/details.js";

export {
CompatibleRuntimeVersionGuard as CompatibleRuntimeVersion,
TipGuard as Tip,
} from "./runtime/status.js";
9 changes: 9 additions & 0 deletions packages/runtime/client/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { submitContractViaAxios } from "./contract/endpoints/singleton.js";
import { ContractDetails } from "./contract/details.js";
import { TransactionDetails } from "./contract/transaction/details.js";
import { ItemRange } from "./pagination.js";
import { RuntimeStatus, getRuntimeStatus } from "./runtime/status.js";
// import curlirize from 'axios-curlirize';

export {
Expand All @@ -63,6 +64,11 @@ export {
* This version of the RestClient targets version `0.0.5` of the Marlowe Runtime.
*/
export interface RestClient {
/**
* Get a Set of Runtime Environment information (Tips, NetworkId and Runtime Version Deployed)
*/
getRuntimeStatus(): Promise<RuntimeStatus>;

/**
* Gets a paginated list of contracts {@link contract.ContractHeader }
* @param request Optional filtering and pagination options.
Expand Down Expand Up @@ -270,6 +276,9 @@ export function mkRestClient(baseURL: string): RestClient {
});

return {
getRuntimeStatus() {
return getRuntimeStatus(axiosInstance);
},
getContracts(request) {
const range = request?.range;
const tags = request?.tags ?? [];
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/client/rest/src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./status.js";
103 changes: 103 additions & 0 deletions packages/runtime/client/rest/src/runtime/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { AxiosInstance } from "axios";
import * as HTTP from "@marlowe.io/adapter/http";
import { unsafeEither, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts";
import { ISO8601 } from "@marlowe.io/adapter/time";
import {
BlockHeader,
BlockHeaderGuard,
NetworkId,
bigintGuard,
} from "@marlowe.io/runtime-core";
import { formatValidationErrors } from "jsonbigint-io-ts-reporters";
import * as E from "fp-ts/lib/Either.js";
import * as t from "io-ts/lib/index.js";
import { MarloweJSON, MarloweJSONCodec } from "@marlowe.io/adapter/codec";
export type BlockHash = string;

export type RuntimeVersion = string;

export type CompatibleRuntimeVersion = "0.0.6" | "0.0.5";

export const CompatibleRuntimeVersionGuard: t.Type<
CompatibleRuntimeVersion,
string
> = t.union([t.literal("0.0.6"), t.literal("0.0.5")]);

/**
* A **Tip** represents the last block read in a "projection" process.
* In the context of Cardano and Blockchain in general, a Projection is about deriving a state from a ledger by streaming the block events.
* These States (e.g : contract details) are eventually consistent, and the Tip gives you an approximate notion on how freshly updated they are.
*/
export type Tip = {
/**
* Last Block Header Read
*/
blockHeader: BlockHeader;
/**
* Last Slot Read in UTC time
*/
slotTimeUTC: ISO8601;
};
/**
* @hidden
*/
export const TipGuard = t.type({
slotTimeUTC: ISO8601,
blockHeader: BlockHeaderGuard,
});

/**
* Set of information about the runtime hosted
*/
export type RuntimeStatus = {
/**
* Network ID of the node connected to the runtime
*/
networkId: NetworkId;
/**
* Runtime Version Deployed
*/
version: RuntimeVersion;
/**
* Set of Tips providing information on how healthy is the flow of Projections : Node > Runtime Chain > Runtime
* The Runtime Tip indicates if the information Queried is up to date. The Node and the Runtime Chain Tips are
* here to help the diagnostic of a Runtime Tip that would be too long in the past or not being updated anymore.
*/
tips: {
node: Tip;
runtimeChain: Tip;
runtime: Tip;
};
};

export const getRuntimeStatus = async (
axiosInstance: AxiosInstance
): Promise<RuntimeStatus> => {
const headers = await unsafeTaskEither(
HTTP.GetWithHeaders(axiosInstance)("/healthcheck")
);

return {
networkId: headers["x-network-id"],
version: headers["x-runtime-version"],
tips: {
node: unsafeEither(
E.mapLeft(formatValidationErrors)(
TipGuard.decode(MarloweJSONCodec.decode(headers["x-node-tip"]))
)
),
runtimeChain: unsafeEither(
E.mapLeft(formatValidationErrors)(
TipGuard.decode(
MarloweJSONCodec.decode(headers["x-runtime-chain-tip"])
)
)
),
runtime: unsafeEither(
E.mapLeft(formatValidationErrors)(
TipGuard.decode(MarloweJSONCodec.decode(headers["x-runtime-tip"]))
)
),
},
};
};
18 changes: 18 additions & 0 deletions packages/runtime/client/rest/test/endpoints/runtime.spec.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mkRestClient } from "@marlowe.io/runtime-rest-client";

import { getMarloweRuntimeUrl } from "../context.js";

import console from "console";
import * as G from "@marlowe.io/runtime-rest-client/guards";
import { MarloweJSON } from "@marlowe.io/adapter/codec";

global.console = console;

describe("Runtime", () => {
const restClient = mkRestClient(getMarloweRuntimeUrl());
it("is deployed with a version compatible with @marlowe.io/runtime-rest-client.", async () => {
const status = await restClient.getRuntimeStatus();
console.log("status", MarloweJSON.stringify(status));
expect(G.CompatibleRuntimeVersion.is(status.version)).toBe(true);
}, 100_000);
});
1 change: 1 addition & 0 deletions packages/runtime/client/rest/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"entryPointStrategy": "expand",
"entryPoints": [
"./src/index.ts",
"./src/runtime/index.ts",
"./src/contract/index.ts",
"./src/payout/index.ts",
"./src/withdrawal/index.ts"
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from "./contract/id.js";
export * from "./sourceId.js";
export * from "./asset/index.js";
export * from "./payout/index.js";
export * from "./network.js";
24 changes: 24 additions & 0 deletions packages/runtime/core/src/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as t from "io-ts/lib/index.js";

export type NetworkId = bigint;
export type Network = "preview" | "preprod" | "mainnet" | "private";

export const NetworkGuard: t.Type<Network, string> = t.union([
t.literal("preview"),
t.literal("preprod"),
t.literal("mainnet"),
t.literal("private"),
]);

export const getNetwork = (networkId: NetworkId): Network => {
switch (networkId) {
case 0n:
return "mainnet";
case 1n:
return "preprod";
case 2n:
return "preview";
default:
return "private";
}
};
25 changes: 18 additions & 7 deletions packages/runtime/lifecycle/test/context.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { Network } from "lucid-cardano";
import { Network, NetworkGuard, getNetwork } from "@marlowe.io/runtime-core";
import { Context, getPrivateKeyFromHexString } from "@marlowe.io/wallet/nodejs";
import { formatValidationErrors } from "jsonbigint-io-ts-reporters";
import { unsafeEither, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts";
import * as E from "fp-ts/lib/Either.js";

export function getBlockfrostContext(): Context {
const { BLOCKFROST_URL, BLOCKFROST_PROJECT_ID, NETWORK_ID } = process.env;
const { BLOCKFROST_URL, BLOCKFROST_PROJECT_ID } = process.env;
if (BLOCKFROST_URL == undefined)
throw "environment configurations not available (BLOCKFROST_URL)";
throw "Test environment variable not defined (BLOCKFROST_URL)";
if (BLOCKFROST_PROJECT_ID == undefined)
throw "environment configurations not available (BLOCKFROST_PROJECT_ID)";
if (NETWORK_ID == undefined)
throw "environment configurations not available (NETWORK_ID)";
throw "Test environment variable not defined (BLOCKFROST_PROJECT_ID)";

return new Context(
BLOCKFROST_PROJECT_ID as string,
BLOCKFROST_URL as string,
NETWORK_ID as Network
getNetworkTestConfiguration()
);
}

export const getNetworkTestConfiguration = (): Network => {
const { NETWORK_NAME } = process.env;
if (NETWORK_NAME == undefined)
throw "Test environment variable not defined (NETWORK_NAME) ";
return unsafeEither(
E.mapLeft(formatValidationErrors)(NetworkGuard.decode(NETWORK_NAME))
);
};

export function getBankPrivateKey(): string {
const { BANK_PK_HEX } = process.env;
if (BANK_PK_HEX == undefined)
Expand Down
22 changes: 18 additions & 4 deletions packages/wallet/src/nodejs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as API from "@blockfrost/blockfrost-js";
import {
Blockfrost,
Lucid,
C,
Network,
C,
PrivateKey,
PolicyId,
getAddressDetails,
Expand Down Expand Up @@ -38,6 +38,7 @@ import {
policyId,
AssetId,
} from "@marlowe.io/runtime-core";
import * as RuntimeCore from "@marlowe.io/runtime-core";
import { WalletAPI } from "../api.js";
import * as Codec from "@47ng/codec";
import { MarloweJSON } from "@marlowe.io/adapter/codec";
Expand All @@ -50,18 +51,31 @@ export type Address = string;
// TODO: This is a pure datatype, convert to type alias or interface
export class Context {
projectId: string;
network: Network;
network: RuntimeCore.Network;
blockfrostUrl: string;

public constructor(
projectId: string,
blockfrostUrl: string,
network: Network
network: RuntimeCore.Network
) {
this.projectId = projectId;
this.network = network;
this.blockfrostUrl = blockfrostUrl;
}

public toLucidNetwork(): Network {
switch (this.network) {
case "private":
return "Custom";
case "preview":
return "Preview";
case "preprod":
return "Preprod";
case "mainnet":
return "Mainnet";
}
}
}

// [[testing-wallet-discussion]]
Expand Down Expand Up @@ -119,7 +133,7 @@ export class SingleAddressWallet implements WalletAPI {
private async initialise() {
this.lucid = await Lucid.new(
new Blockfrost(this.context.blockfrostUrl, this.context.projectId),
this.context.network
this.context.toLucidNetwork()
);
this.lucid.selectWalletFromPrivateKey(this.privateKeyBech32);
this.address = addressBech32(await this.lucid.wallet.address());
Expand Down

0 comments on commit 8bc1e12

Please sign in to comment.