Skip to content

Commit

Permalink
feat(connector-chainlink): add skeleton implementation - oracle streams
Browse files Browse the repository at this point in the history
WORK IN PROGRESS

Fixes hyperledger-cacti#3530

Signed-off-by: Peter Somogyvari <[email protected]>
  • Loading branch information
petermetz committed Sep 8, 2024
1 parent ef63371 commit 8600f13
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/cactus-test-tooling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"socket.io-client-fixed-types": "4.5.4",
"tar-stream": "2.2.0",
"temp": "0.9.4",
"ts-results": "3.3.0",
"typescript-optional": "2.0.1",
"uuid": "10.0.0",
"web3": "1.6.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { EventEmitter } from "node:events";

import Docker, { Container, ContainerInfo } from "dockerode";
import Joi from "joi";
import { None, Option, Some } from "ts-results";

import { ITestLedger } from "../i-test-ledger";
import { Containers } from "../common/containers";

import {
LogLevelDesc,
Logger,
LoggerProvider,
Checks,
Bools,
} from "@hyperledger/cactus-common";

export interface IChainlinkTestLedgerConstructorOptions {
imageVersion?: string;
imageName?: string;
rpcPortNotary?: number;
sshPort?: number;
rpcPortA?: number;
rpcPortB?: number;
httpPort?: number;
logLevel?: LogLevelDesc;
envVars?: string[];
emitContainerLogs?: boolean;
}

// const imageTag = "smartcontract/chainlink:2.15.0";

const DEFAULTS = Object.freeze({
imageVersion: "2.15.0",
imageName: "smartcontract/chainlink",
httpPort: 8080,
envVars: [],
});
export const CHAINLINK_TEST_LEDGER_DEFAULT_OPTIONS = DEFAULTS;

/*
* Provides validations for the Chainlink AIO ledger container's options
*/
const JOI_SCHEMA: Joi.Schema = Joi.object().keys({
imageVersion: Joi.string().min(5).required(),
imageName: Joi.string().min(1).required(),
httpPort: Joi.number().min(1).max(65535).required(),
});
export const CHAINLINK_TEST_LEDGER_OPTIONS_JOI_SCHEMA = JOI_SCHEMA;

/**
* @see https://github.com/smartcontractkit/chainlink/blob/develop/core/chainlink.Dockerfile
*/
export class ChainlinkTestLedger implements ITestLedger {
public static readonly CLASS_NAME = "ChainlinkTestLedger";

private readonly log: Logger;
private readonly envVars: string[];

public get className(): string {
return ChainlinkTestLedger.CLASS_NAME;
}

public readonly imageVersion: string;
public readonly imageName: string;
public readonly httpPort: number;
public readonly emitContainerLogs: boolean;

private container: Container | undefined;
private containerId: string | undefined;

constructor(
public readonly opts: IChainlinkTestLedgerConstructorOptions = {},
) {
const fnTag = `${this.className}#constructor()`;
Checks.truthy(opts, `${fnTag} options`);

this.imageVersion = opts.imageVersion || DEFAULTS.imageVersion;
this.imageName = opts.imageName || DEFAULTS.imageName;

this.httpPort = opts.httpPort || DEFAULTS.httpPort;

this.emitContainerLogs = Bools.isBooleanStrict(opts.emitContainerLogs)
? (opts.emitContainerLogs as boolean)
: true;

this.envVars = opts.envVars ? opts.envVars : DEFAULTS.envVars;
Checks.truthy(Array.isArray(this.envVars), `${fnTag}:envVars not an array`);

this.validateConstructorOptions();
const label = "chainlink-test-ledger";
const level = opts.logLevel || "INFO";
this.log = LoggerProvider.getOrCreate({ level, label });
}

public getContainerId(): string {
const fnTag = `${this.className}.getContainerId()`;
Checks.nonBlankString(this.containerId, `${fnTag}::containerId`);
return this.containerId as string;
}

public async start(skipPull = false): Promise<Container> {
const imageFqn = this.getContainerImageName();

if (this.container) {
await this.container.stop();
await this.container.remove();
}
const docker = new Docker();

if (!skipPull) {
await Containers.pullImage(imageFqn, {}, this.opts.logLevel);
}

return new Promise<Container>((resolve, reject) => {
const eventEmitter: EventEmitter = docker.run(
imageFqn,
[],
[],
{
ExposedPorts: {
[`${this.httpPort}/tcp`]: {},
},
HostConfig: {
PublishAllPorts: true,
},
// TODO: this can be removed once the new docker image is published and
// specified as the default one to be used by the tests.
// Healthcheck: {
// Test: [
// "CMD-SHELL",
// `curl -v 'http://127.0.0.1:7005/jolokia/exec/org.apache.activemq.artemis:address=%22rpc.server%22,broker=%22RPC%22,component=addresses,queue=%22rpc.server%22,routing-type=%22multicast%22,subcomponent=queues/countMessages()/'`,
// ],
// Interval: 1000000000, // 1 second
// Timeout: 3000000000, // 3 seconds
// Retries: 99,
// StartPeriod: 1000000000, // 1 second
// },
Env: this.envVars,
},
{},
(err: unknown) => {
if (err) {
reject(err);
}
},
);

eventEmitter.once("start", async (container: Container) => {
this.container = container;
this.containerId = container.id;

if (this.emitContainerLogs) {
const fnTag = `[${this.getContainerImageName()}]`;
await Containers.streamLogs({
container: this.getContainer().expect("stream logs: no container"),
tag: fnTag,
log: this.log,
});
}

try {
let isHealthy = false;
do {
const containerInfo = await this.getContainerInfo();
this.log.debug(`ContainerInfo.Status=%o`, containerInfo.Status);
this.log.debug(`ContainerInfo.State=%o`, containerInfo.State);
isHealthy = containerInfo.Status.endsWith("(healthy)");
if (!isHealthy) {
await new Promise((resolve2) => setTimeout(resolve2, 1000));
}
} while (!isHealthy);
resolve(container);
} catch (ex) {
reject(ex);
}
});
});
}

public async logDebugPorts(): Promise<void> {
const httpPort = await this.getHttpPortPublic();
this.log.info(`HTTP Port: ${httpPort}`);
}

public async stop(): Promise<unknown> {
const container = this.getContainer();
if (container.none) {
return "Container was not present. Skipped stopping it.";
}
return Containers.stop(container.val);
}

public async destroy(): Promise<unknown> {
const fnTag = `${this.className}.destroy()`;
if (this.container) {
return this.container.remove();
} else {
return Promise.reject(
new Error(`${fnTag} Container was never created, nothing to destroy.`),
);
}
}

protected async getContainerInfo(): Promise<ContainerInfo> {
const fnTag = `${this.className}.getContainerInfo()`;
const docker = new Docker();
const containerInfos = await docker.listContainers({});
const id = this.getContainerId();

const aContainerInfo = containerInfos.find((ci) => ci.Id === id);

if (aContainerInfo) {
return aContainerInfo;
} else {
throw new Error(`${fnTag} no container with ID "${id}"`);
}
}

/**
* @returns The port mapped to the host machine's network interface.
*/
public async getHttpPortPublic(): Promise<number> {
const aContainerInfo = await this.getContainerInfo();
return Containers.getPublicPort(this.httpPort, aContainerInfo);
}

public getContainer(): Option<Container> {
return this.container instanceof Container ? Some(this.container) : None;
}

public getContainerImageName(): string {
return `${this.imageName}:${this.imageVersion}`;
}

private validateConstructorOptions(): void {
const fnTag = `${this.className}#validateConstructorOptions()`;
const validationResult = JOI_SCHEMA.validate({
imageVersion: this.imageVersion,
imageName: this.imageName,
httpPort: this.httpPort,
});

if (validationResult.error) {
throw new Error(`${fnTag} ${validationResult.error.annotate()}`);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ export {
SubstrateTestLedger,
} from "./substrate-test-ledger/substrate-test-ledger";

export {
CHAINLINK_TEST_LEDGER_DEFAULT_OPTIONS,
CHAINLINK_TEST_LEDGER_OPTIONS_JOI_SCHEMA,
ChainlinkTestLedger,
IChainlinkTestLedgerConstructorOptions,
} from "./chainlink/chainlink-test-ledger";

export { Streams } from "./common/streams";

export { isRunningInGithubAction } from "./github-actions/is-running-in-github-action";
Expand Down
20 changes: 20 additions & 0 deletions tools/docker/chainlink-all-in-one/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM ubuntu:22.04

# Install dependencies
RUN apt-get update && apt-get install -y wget curl gnupg2

# Add Chainlink GPG key
RUN wget -O - https://raw.githubusercontent.com/chainlink/docker-chainlink/master/chainlink.gpg | gpg --dearmor -o /usr/share/keyrings/chainlink.gpg
RUN echo "deb [signed-by=/usr/share/keyrings/chainlink.gpg] https://chainlink.github.io/chainlink/stable/ubuntu/22.04 amd64 main" | tee /etc/apt/sources.list.d/chainlink.list

RUN apt-get update && apt-get install -y chainlink

# Create a Chainlink configuration file
RUN mkdir -p /etc/chainlink
COPY chainlink.config.yaml /etc/chainlink/chainlink.config.yaml

# Expose the Chainlink port (8080)
EXPOSE 8080

# Command to run the Chainlink node
CMD chainlink node run --config /etc/chainlink/chainlink.config.yaml
29 changes: 29 additions & 0 deletions tools/docker/chainlink-all-in-one/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ChainLink All-In-One Ledger Image

This is a container image which is designed to accurately simulate a ChainLInk node for testing purposes on localhost.

Because of it having been designed for test automation use-cases, we try
to make the image as light as possible.

## Usage

### Build and Run Image Locally

```sh
DOCKER_BUILDKIT=1 docker build \
--progress=plain \
--file ./tools/docker/chainlink-all-in-one/Dockerfile \
./tools/docker/chainlink-all-in-one/ \
--tag claio \
--tag chainlink-all-in-one \
--tag ghcr.io/hyperledger/cacti-chainlink-all-in-one:$(date +"%Y-%m-%dT%H-%M-%S" --utc)-dev-$(git rev-parse --short HEAD)
```

```sh
docker run --rm -it claio
```


## References

$ cd ~/.chainlink && docker run -p 6688:6688 -v ~/.chainlink:/chainlink -it --env-file=.env smartcontract/chainlink local n
19 changes: 19 additions & 0 deletions tools/docker/chainlink-all-in-one/chainlink.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Chainlink configuration file
ethereum:
url: https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>
accounts:
- private_key: <YOUR_PRIVATE_KEY>

# Job definitions
jobs:
- type: web
name: google_price
tasks:
- type: http
url: https://api.priceofbitcoin.com/v1/current
- type: jsonparse
path: $.price
target:
- type: eth
address: <YOUR_SMART_CONTRACT_ADDRESS>
data: 0x6080604052361561000f576020357f1656d78fa275ee266d0e7f380a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10932,6 +10932,7 @@ __metadata:
socket.io-client-fixed-types: "npm:4.5.4"
tar-stream: "npm:2.2.0"
temp: "npm:0.9.4"
ts-results: "npm:3.3.0"
typescript-optional: "npm:2.0.1"
uuid: "npm:10.0.0"
web3: "npm:1.6.1"
Expand Down Expand Up @@ -51410,6 +51411,13 @@ __metadata:
languageName: node
linkType: hard

"ts-results@npm:3.3.0":
version: 3.3.0
resolution: "ts-results@npm:3.3.0"
checksum: 10/127371a096d0ec0d31ee813ae7e160fb7c4e2535c063a154e969389f746a319cf9612f21402aaa2a563cd0d8b6aee8604e955c0d2971b1803979e6f5300e98ea
languageName: node
linkType: hard

"tsconfig-paths@npm:^3.14.2":
version: 3.14.2
resolution: "tsconfig-paths@npm:3.14.2"
Expand Down

0 comments on commit 8600f13

Please sign in to comment.