forked from hyperledger-cacti/cacti
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(connector-chainlink): add skeleton implementation - oracle streams
WORK IN PROGRESS Fixes hyperledger-cacti#3530 Signed-off-by: Peter Somogyvari <[email protected]>
- Loading branch information
Showing
7 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
248 changes: 248 additions & 0 deletions
248
packages/cactus-test-tooling/src/main/typescript/chainlink/chainlink-test-ledger.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters