pyth2wormhole-client: Run an automated attestation script in Tilt
Change-Id: Id2e6def6c246862601a206084867c5f1b26a6673
This commit is contained in:
parent
78cd4ee437
commit
f24f86adf5
|
@ -3,14 +3,17 @@ FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965b
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -yq libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang ncat
|
RUN apt-get update && apt-get install -yq libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang ncat
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
|
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
|
||||||
|
RUN curl -sSfL https://release.solana.com/v1.7.8/install | sh
|
||||||
|
|
||||||
RUN rustup default nightly-2021-08-01
|
RUN rustup default nightly-2021-08-01
|
||||||
RUN rustup component add rustfmt
|
RUN rustup component add rustfmt
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache \
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
cargo install --version =1.7.0 solana-cli && \
|
|
||||||
cargo install --version =2.0.12 spl-token-cli
|
cargo install --version =2.0.12 spl-token-cli
|
||||||
|
|
||||||
|
ENV SOLANA_BIN_PATH="/root/.local/share/solana/install/active_release/bin"
|
||||||
|
ENV PATH="$SOLANA_BIN_PATH:$PATH"
|
||||||
|
|
||||||
ADD ethereum /usr/src/ethereum
|
ADD ethereum /usr/src/ethereum
|
||||||
WORKDIR /usr/src/ethereum
|
WORKDIR /usr/src/ethereum
|
||||||
RUN --mount=type=cache,target=/root/.cache \
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
@ -26,10 +29,15 @@ RUN --mount=type=cache,target=/root/.cache \
|
||||||
npm run build-contracts && \
|
npm run build-contracts && \
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
|
|
||||||
ADD solana /usr/src/solana
|
ADD solana /usr/src/solana
|
||||||
ADD proto /usr/src/proto
|
ADD proto /usr/src/proto
|
||||||
|
|
||||||
WORKDIR /usr/src/solana
|
WORKDIR /usr/src/solana
|
||||||
|
|
||||||
|
RUN solana config set --keypair "/usr/src/solana/keys/solana-devnet.json"
|
||||||
|
RUN solana config set --url "http://solana-devnet:8899"
|
||||||
|
|
||||||
ENV EMITTER_ADDRESS="11111111111111111111111111111115"
|
ENV EMITTER_ADDRESS="11111111111111111111111111111115"
|
||||||
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||||
|
|
||||||
|
@ -39,7 +47,5 @@ RUN --mount=type=cache,target=/root/.cache \
|
||||||
set -xe && \
|
set -xe && \
|
||||||
cargo build --manifest-path ./bridge/Cargo.toml --package client --release && \
|
cargo build --manifest-path ./bridge/Cargo.toml --package client --release && \
|
||||||
cargo build --manifest-path ./modules/token_bridge/Cargo.toml --package client --release && \
|
cargo build --manifest-path ./modules/token_bridge/Cargo.toml --package client --release && \
|
||||||
cp /usr/local/cargo/bin/solana /usr/local/bin && \
|
|
||||||
cp /usr/local/cargo/bin/spl-token /usr/local/bin && \
|
|
||||||
cp bridge/target/release/client /usr/local/bin && \
|
cp bridge/target/release/client /usr/local/bin && \
|
||||||
cp modules/token_bridge/target/release/client /usr/local/bin/token-bridge-client
|
cp modules/token_bridge/target/release/client /usr/local/bin/token-bridge-client
|
||||||
|
|
28
Tiltfile
28
Tiltfile
|
@ -113,7 +113,7 @@ k8s_resource("guardian", resource_deps = ["proto-gen", "solana-devnet"], port_fo
|
||||||
docker_build(
|
docker_build(
|
||||||
ref = "pyth",
|
ref = "pyth",
|
||||||
context = ".",
|
context = ".",
|
||||||
dockerfile = "third_party/pyth/Dockerfile",
|
dockerfile = "third_party/pyth/Dockerfile.pyth",
|
||||||
)
|
)
|
||||||
k8s_yaml_with_ns("./devnet/pyth.yaml")
|
k8s_yaml_with_ns("./devnet/pyth.yaml")
|
||||||
|
|
||||||
|
@ -136,13 +136,12 @@ k8s_resource(
|
||||||
# solana client cli (used for devnet setup)
|
# solana client cli (used for devnet setup)
|
||||||
|
|
||||||
docker_build(
|
docker_build(
|
||||||
ref = "solana-client",
|
ref = "bridge-client",
|
||||||
context = ".",
|
context = ".",
|
||||||
only = ["./proto", "./solana", "./ethereum", "./clients/token_bridge"],
|
only = ["./proto", "./solana", "./ethereum", "./clients"],
|
||||||
dockerfile = "Dockerfile.client",
|
dockerfile = "Dockerfile.client",
|
||||||
|
|
||||||
# Ignore target folders from local (non-container) development.
|
# Ignore target folders from local (non-container) development.
|
||||||
ignore = ["./solana/target", "./solana/agent/target", "./solana/cli/target"],
|
ignore = ["./solana/*/target"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# solana smart contract
|
# solana smart contract
|
||||||
|
@ -167,6 +166,25 @@ k8s_resource(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pyth2wormhole client
|
||||||
|
|
||||||
|
docker_build(
|
||||||
|
ref = "p2w-client",
|
||||||
|
context = ".",
|
||||||
|
only = ["./solana", "./third_party"],
|
||||||
|
dockerfile = "./third_party/pyth/Dockerfile.p2w-client",
|
||||||
|
|
||||||
|
# Ignore target folders from local (non-container) development.
|
||||||
|
ignore = ["./solana/*/target"],
|
||||||
|
)
|
||||||
|
|
||||||
|
k8s_yaml_with_ns("devnet/p2w-client.yaml")
|
||||||
|
|
||||||
|
k8s_resource("p2w-client",
|
||||||
|
resource_deps=["solana-devnet", "pyth"],
|
||||||
|
port_forwards=[]
|
||||||
|
)
|
||||||
|
|
||||||
# eth devnet
|
# eth devnet
|
||||||
|
|
||||||
docker_build(
|
docker_build(
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: p2w-client
|
||||||
|
labels:
|
||||||
|
app: p2w-client
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 8001
|
||||||
|
name: http
|
||||||
|
protocol: TCP
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: p2w-client
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: p2w-client
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: p2w-client
|
||||||
|
serviceName: p2w-client
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: p2w-client
|
||||||
|
spec:
|
||||||
|
restartPolicy: Always
|
||||||
|
terminationGracePeriodSeconds: 0
|
||||||
|
containers:
|
||||||
|
- name: p2w-client
|
||||||
|
image: p2w-client
|
||||||
|
command:
|
||||||
|
- python3
|
||||||
|
- /usr/src/pyth/p2w_autoattest.py
|
||||||
|
tty: true
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 2000
|
||||||
|
periodSeconds: 1
|
||||||
|
failureThreshold: 300
|
|
@ -13,6 +13,9 @@ spec:
|
||||||
- port: 8898
|
- port: 8898
|
||||||
name: pyth-tx
|
name: pyth-tx
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
- port: 4242
|
||||||
|
name: pyth-accounts
|
||||||
|
protocol: TCP
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
|
@ -23,8 +26,6 @@ spec:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: pyth
|
app: pyth
|
||||||
serviceName: pyth
|
serviceName: pyth
|
||||||
updateStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
|
@ -43,13 +44,7 @@ spec:
|
||||||
port: 2000
|
port: 2000
|
||||||
periodSeconds: 1
|
periodSeconds: 1
|
||||||
failureThreshold: 300
|
failureThreshold: 300
|
||||||
- name: pyth-tx
|
|
||||||
image: pyth
|
|
||||||
command:
|
|
||||||
- ./pyth_tx
|
|
||||||
- -r
|
|
||||||
- solana-devnet
|
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8898
|
- containerPort: 4242
|
||||||
name: pyth-tx
|
name: pyth-accounts
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
|
|
@ -94,7 +94,7 @@ spec:
|
||||||
path: /health
|
path: /health
|
||||||
periodSeconds: 1
|
periodSeconds: 1
|
||||||
- name: setup
|
- name: setup
|
||||||
image: solana-client
|
image: bridge-client
|
||||||
command:
|
command:
|
||||||
- /usr/src/solana/devnet_setup.sh
|
- /usr/src/solana/devnet_setup.sh
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
|
|
|
@ -34,6 +34,7 @@ pkgs.mkShell {
|
||||||
pkgconfig
|
pkgconfig
|
||||||
protobuf
|
protobuf
|
||||||
python3
|
python3
|
||||||
|
python3Packages.autopep8
|
||||||
whcluster
|
whcluster
|
||||||
whinotify
|
whinotify
|
||||||
whkube
|
whkube
|
||||||
|
|
|
@ -43,7 +43,7 @@ RUN mkdir -p /opt/solana/deps
|
||||||
ENV EMITTER_ADDRESS="11111111111111111111111111111115"
|
ENV EMITTER_ADDRESS="11111111111111111111111111111115"
|
||||||
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||||
|
|
||||||
# Build Wormhole Solana progrms
|
# Build Wormhole Solana programs
|
||||||
RUN --mount=type=cache,target=bridge/target \
|
RUN --mount=type=cache,target=bridge/target \
|
||||||
--mount=type=cache,target=modules/token_bridge/target \
|
--mount=type=cache,target=modules/token_bridge/target \
|
||||||
--mount=type=cache,target=pyth2wormhole/target \
|
--mount=type=cache,target=pyth2wormhole/target \
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[39,20,181,104,82,27,70,145,227,136,168,14,170,24,33,88,145,152,180,229,219,142,247,114,237,79,52,97,84,65,213,172,49,165,99,116,254,135,110,132,214,114,59,200,109,253,45,43,74,172,107,84,162,223,23,15,78,167,240,137,234,123,4,231]
|
|
@ -26,9 +26,6 @@ pub struct Cli {
|
||||||
pub rpc_url: String,
|
pub rpc_url: String,
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub p2w_addr: Pubkey,
|
pub p2w_addr: Pubkey,
|
||||||
/// The bridge program account
|
|
||||||
#[clap(long = "wh-prog")]
|
|
||||||
pub wh_prog: Pubkey,
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
}
|
}
|
||||||
|
@ -37,8 +34,11 @@ pub struct Cli {
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
#[clap(about = "Initialize a pyth2wormhole program freshly deployed under <p2w_addr>")]
|
#[clap(about = "Initialize a pyth2wormhole program freshly deployed under <p2w_addr>")]
|
||||||
Init {
|
Init {
|
||||||
#[clap(long = "owner")]
|
/// The bridge program account
|
||||||
new_owner_addr: Pubkey,
|
#[clap(long = "wh-prog")]
|
||||||
|
wh_prog: Pubkey,
|
||||||
|
#[clap(long = "owner")]
|
||||||
|
owner_addr: Pubkey,
|
||||||
#[clap(long = "pyth-owner")]
|
#[clap(long = "pyth-owner")]
|
||||||
pyth_owner_addr: Pubkey,
|
pyth_owner_addr: Pubkey,
|
||||||
},
|
},
|
||||||
|
@ -53,4 +53,17 @@ pub enum Action {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
nonce: u32,
|
nonce: u32,
|
||||||
},
|
},
|
||||||
|
#[clap(about = "Update an existing pyth2wormhole program's settings (currently set owner only)")]
|
||||||
|
SetConfig {
|
||||||
|
/// Current owner keypair path
|
||||||
|
#[clap(long = "owner", default_value = "~/.config/solana/id.json")]
|
||||||
|
owner: String,
|
||||||
|
/// New owner to set
|
||||||
|
#[clap(long = "new-owner")]
|
||||||
|
new_owner_addr: Pubkey,
|
||||||
|
#[clap(long = "new-wh-prog")]
|
||||||
|
new_wh_prog: Pubkey,
|
||||||
|
#[clap(long = "new-pyth-owner")]
|
||||||
|
new_pyth_owner_addr: Pubkey,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
|
||||||
use borsh::BorshSerialize;
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
use log::LevelFilter;
|
use log::{LevelFilter, error};
|
||||||
use solana_client::rpc_client::RpcClient;
|
use solana_client::rpc_client::RpcClient;
|
||||||
use solana_program::{
|
use solana_program::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
|
@ -54,6 +54,7 @@ use bridge::{
|
||||||
use pyth2wormhole::{
|
use pyth2wormhole::{
|
||||||
config::P2WConfigAccount,
|
config::P2WConfigAccount,
|
||||||
initialize::InitializeAccounts,
|
initialize::InitializeAccounts,
|
||||||
|
set_config::SetConfigAccounts,
|
||||||
types::PriceAttestation,
|
types::PriceAttestation,
|
||||||
AttestData,
|
AttestData,
|
||||||
Pyth2WormholeConfig,
|
Pyth2WormholeConfig,
|
||||||
|
@ -74,16 +75,31 @@ fn main() -> Result<(), ErrBox> {
|
||||||
|
|
||||||
let tx = match cli.action {
|
let tx = match cli.action {
|
||||||
Action::Init {
|
Action::Init {
|
||||||
new_owner_addr,
|
owner_addr,
|
||||||
pyth_owner_addr,
|
pyth_owner_addr,
|
||||||
|
wh_prog,
|
||||||
} => handle_init(
|
} => handle_init(
|
||||||
payer,
|
payer,
|
||||||
p2w_addr,
|
p2w_addr,
|
||||||
new_owner_addr,
|
owner_addr,
|
||||||
cli.wh_prog,
|
wh_prog,
|
||||||
pyth_owner_addr,
|
pyth_owner_addr,
|
||||||
recent_blockhash,
|
recent_blockhash,
|
||||||
)?,
|
)?,
|
||||||
|
Action::SetConfig {
|
||||||
|
owner,
|
||||||
|
new_owner_addr,
|
||||||
|
new_wh_prog,
|
||||||
|
new_pyth_owner_addr,
|
||||||
|
} => handle_set_config(
|
||||||
|
payer,
|
||||||
|
p2w_addr,
|
||||||
|
read_keypair_file(&*shellexpand::tilde(&owner))?,
|
||||||
|
new_owner_addr,
|
||||||
|
new_wh_prog,
|
||||||
|
new_pyth_owner_addr,
|
||||||
|
recent_blockhash,
|
||||||
|
)?,
|
||||||
Action::Attest {
|
Action::Attest {
|
||||||
product_addr,
|
product_addr,
|
||||||
price_addr,
|
price_addr,
|
||||||
|
@ -139,6 +155,45 @@ fn handle_init(
|
||||||
Ok(tx_signed)
|
Ok(tx_signed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_set_config(
|
||||||
|
payer: Keypair,
|
||||||
|
p2w_addr: Pubkey,
|
||||||
|
owner: Keypair,
|
||||||
|
new_owner_addr: Pubkey,
|
||||||
|
new_wh_prog: Pubkey,
|
||||||
|
new_pyth_owner_addr: Pubkey,
|
||||||
|
recent_blockhash: Hash,
|
||||||
|
) -> Result<Transaction, ErrBox> {
|
||||||
|
use AccEntry::*;
|
||||||
|
|
||||||
|
let payer_pubkey = payer.pubkey();
|
||||||
|
|
||||||
|
println!("Canary!");
|
||||||
|
|
||||||
|
let accs = SetConfigAccounts {
|
||||||
|
payer: Signer(payer),
|
||||||
|
current_owner: Signer(owner),
|
||||||
|
config: Derived(p2w_addr),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Pyth2WormholeConfig {
|
||||||
|
owner: new_owner_addr,
|
||||||
|
wh_prog: new_wh_prog,
|
||||||
|
pyth_owner: new_pyth_owner_addr,
|
||||||
|
};
|
||||||
|
let ix_data = (pyth2wormhole::instruction::Instruction::SetConfig, config);
|
||||||
|
|
||||||
|
let (ix, signers) = accs.to_ix(p2w_addr, ix_data.try_to_vec()?.as_slice())?;
|
||||||
|
|
||||||
|
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
|
||||||
|
&[ix],
|
||||||
|
Some(&payer_pubkey),
|
||||||
|
signers.iter().collect::<Vec<_>>().as_ref(),
|
||||||
|
recent_blockhash,
|
||||||
|
);
|
||||||
|
Ok(tx_signed)
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_attest(
|
fn handle_attest(
|
||||||
rpc: &RpcClient, // Needed for reading Pyth account data
|
rpc: &RpcClient, // Needed for reading Pyth account data
|
||||||
payer: Keypair,
|
payer: Keypair,
|
||||||
|
@ -148,16 +203,19 @@ fn handle_attest(
|
||||||
nonce: u32,
|
nonce: u32,
|
||||||
recent_blockhash: Hash,
|
recent_blockhash: Hash,
|
||||||
) -> Result<Transaction, ErrBox> {
|
) -> Result<Transaction, ErrBox> {
|
||||||
|
|
||||||
let emitter_keypair = Keypair::new();
|
let emitter_keypair = Keypair::new();
|
||||||
let message_keypair = Keypair::new();
|
let message_keypair = Keypair::new();
|
||||||
|
|
||||||
|
let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr);
|
||||||
|
|
||||||
|
let config = Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?;
|
||||||
|
|
||||||
// Derive dynamic seeded accounts
|
// Derive dynamic seeded accounts
|
||||||
let seq_addr = Sequence::key(
|
let seq_addr = Sequence::key(
|
||||||
&SequenceDerivationData {
|
&SequenceDerivationData {
|
||||||
emitter_key: &emitter_keypair.pubkey(),
|
emitter_key: &emitter_keypair.pubkey(),
|
||||||
},
|
},
|
||||||
&wh_prog,
|
&config.wh_prog,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Arrange Attest accounts
|
// Arrange Attest accounts
|
||||||
|
@ -168,7 +226,7 @@ fn handle_attest(
|
||||||
AccountMeta::new_readonly(system_program::id(), false),
|
AccountMeta::new_readonly(system_program::id(), false),
|
||||||
// config
|
// config
|
||||||
AccountMeta::new_readonly(
|
AccountMeta::new_readonly(
|
||||||
P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr),
|
p2w_config_addr,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
// pyth_product
|
// pyth_product
|
||||||
|
@ -177,13 +235,11 @@ fn handle_attest(
|
||||||
AccountMeta::new_readonly(price_addr, false),
|
AccountMeta::new_readonly(price_addr, false),
|
||||||
// clock
|
// clock
|
||||||
AccountMeta::new_readonly(clock::id(), false),
|
AccountMeta::new_readonly(clock::id(), false),
|
||||||
|
// wh_prog
|
||||||
// wh_prog
|
AccountMeta::new_readonly(config.wh_prog, false),
|
||||||
AccountMeta::new_readonly(wh_prog, false),
|
|
||||||
|
|
||||||
// wh_bridge
|
// wh_bridge
|
||||||
AccountMeta::new(
|
AccountMeta::new(
|
||||||
Bridge::<{ AccountState::Initialized }>::key(None, &wh_prog),
|
Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
// wh_message
|
// wh_message
|
||||||
|
@ -193,7 +249,7 @@ fn handle_attest(
|
||||||
// wh_sequence
|
// wh_sequence
|
||||||
AccountMeta::new(seq_addr, false),
|
AccountMeta::new(seq_addr, false),
|
||||||
// wh_fee_collector
|
// wh_fee_collector
|
||||||
AccountMeta::new(FeeCollector::<'_>::key(None, &wh_prog), false),
|
AccountMeta::new(FeeCollector::<'_>::key(None, &config.wh_prog), false),
|
||||||
AccountMeta::new_readonly(rent::id(), false),
|
AccountMeta::new_readonly(rent::id(), false),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,35 @@
|
||||||
use solana_program::{msg, pubkey::Pubkey};
|
use solana_program::{
|
||||||
|
msg,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
use solitaire::{
|
use solitaire::{
|
||||||
AccountState, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel,
|
AccountState,
|
||||||
Result as SoliResult, Signer, SolitaireError, ToInstruction,
|
ExecutionContext,
|
||||||
|
FromAccounts,
|
||||||
|
Info,
|
||||||
|
InstructionContext,
|
||||||
|
Keyed,
|
||||||
|
Mut,
|
||||||
|
Peel,
|
||||||
|
Result as SoliResult,
|
||||||
|
Signer,
|
||||||
|
SolitaireError,
|
||||||
|
ToInstruction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::{P2WConfigAccount, Pyth2WormholeConfig};
|
use crate::config::{
|
||||||
|
P2WConfigAccount,
|
||||||
|
Pyth2WormholeConfig,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(FromAccounts, ToInstruction)]
|
#[derive(FromAccounts, ToInstruction)]
|
||||||
pub struct SetConfig<'b> {
|
pub struct SetConfig<'b> {
|
||||||
/// Current config used by the program
|
/// Current config used by the program
|
||||||
pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
|
pub config: Mut<P2WConfigAccount<'b, { AccountState::Initialized }>>,
|
||||||
/// Current owner authority of the program
|
/// Current owner authority of the program
|
||||||
pub current_owner: Signer<Info<'b>>,
|
pub current_owner: Mut<Signer<Info<'b>>>,
|
||||||
/// Payer account for updating the account data
|
/// Payer account for updating the account data
|
||||||
pub payer: Signer<Info<'b>>,
|
pub payer: Mut<Signer<Info<'b>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'b> InstructionContext<'b> for SetConfig<'b> {
|
impl<'b> InstructionContext<'b> for SetConfig<'b> {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
FROM bridge-client
|
||||||
|
|
||||||
|
RUN apt-get install -y python3
|
||||||
|
|
||||||
|
ADD third_party/pyth/pyth_utils.py /usr/src/pyth/pyth_utils.py
|
||||||
|
ADD third_party/pyth/p2w_autoattest.py /usr/src/pyth/p2w_autoattest.py
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
--mount=type=cache,target=target \
|
||||||
|
--mount=type=cache,target=pyth2wormhole/target \
|
||||||
|
cargo build --manifest-path ./pyth2wormhole/Cargo.toml --package pyth2wormhole-client && \
|
||||||
|
mv pyth2wormhole/target/debug/pyth2wormhole-client /usr/local/bin/pyth2wormhole-client && \
|
||||||
|
chmod a+rx /usr/src/pyth/*.py
|
||||||
|
|
||||||
|
ENV P2W_OWNER_KEYPAIR="/usr/src/solana/keys/p2w_owner.json"
|
||||||
|
ENV PYTH_PUBLISHER_KEYPAIR="/usr/src/solana/keys/pyth_publisher.json"
|
||||||
|
ENV PYTH_PROGRAM_KEYPAIR="/usr/src/solana/keys/pyth_program.json"
|
|
@ -26,7 +26,7 @@ RUN cp /opt/solana/keys/pyth_publisher.json publish_key_pair.json && \
|
||||||
ENV PYTH_SRC_REV=31e3188bbf52ec1a25f71e4ab969378b27415b0a
|
ENV PYTH_SRC_REV=31e3188bbf52ec1a25f71e4ab969378b27415b0a
|
||||||
ENV PYTH_SRC_ROOT=/home/pyth/pyth-client
|
ENV PYTH_SRC_ROOT=/home/pyth/pyth-client
|
||||||
|
|
||||||
ADD https://github.com/drozdziak1/pyth-client/archive/$PYTH_SRC_REV.tar.gz .
|
ADD https://github.com/pyth-network/pyth-client/archive/$PYTH_SRC_REV.tar.gz .
|
||||||
RUN tar -xvf *.tar.gz && \
|
RUN tar -xvf *.tar.gz && \
|
||||||
rm -rf $PYTH_SRC_ROOT *.tar.gz && \
|
rm -rf $PYTH_SRC_ROOT *.tar.gz && \
|
||||||
mv pyth-client-$PYTH_SRC_REV $PYTH_SRC_ROOT/
|
mv pyth-client-$PYTH_SRC_REV $PYTH_SRC_ROOT/
|
||||||
|
@ -36,7 +36,9 @@ WORKDIR $PYTH_SRC_ROOT/build
|
||||||
RUN cmake .. && make
|
RUN cmake .. && make
|
||||||
|
|
||||||
# Prepare setup script
|
# Prepare setup script
|
||||||
ADD third_party/pyth/ /opt/pyth/
|
ADD third_party/pyth/pyth_utils.py /opt/pyth/pyth_utils.py
|
||||||
|
ADD third_party/pyth/pyth_publisher.py /opt/pyth/pyth_publisher.py
|
||||||
|
|
||||||
RUN chmod a+rx /opt/pyth/*.py
|
RUN chmod a+rx /opt/pyth/*.py
|
||||||
USER pyth
|
USER pyth
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This script sets up a simple loop for periodical attestation of Pyth data
|
||||||
|
from pyth_utils import *
|
||||||
|
|
||||||
|
from http.client import HTTPConnection
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
P2W_ADDRESS = "P2WH424242424242424242424242424242424242424"
|
||||||
|
P2W_ATTEST_INTERVAL = float(os.environ.get("P2W_ATTEST_INTERVAL", 5))
|
||||||
|
P2W_OWNER_KEYPAIR = os.environ.get(
|
||||||
|
"P2W_OWNER_KEYPAIR", f"/usr/src/solana/keys/p2w_owner.json")
|
||||||
|
|
||||||
|
PYTH_ACCOUNTS_HOST = "pyth"
|
||||||
|
PYTH_ACCOUNTS_PORT = 4242
|
||||||
|
|
||||||
|
WORMHOLE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||||
|
|
||||||
|
# Get actor pubkeys
|
||||||
|
P2W_OWNER_ADDRESS = sol_run_or_die(
|
||||||
|
"address", ["--keypair", P2W_OWNER_KEYPAIR], capture_output=True).stdout.strip()
|
||||||
|
PYTH_OWNER_ADDRESS = sol_run_or_die(
|
||||||
|
"address", ["--keypair", PYTH_PROGRAM_KEYPAIR], capture_output=True).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# Top up pyth2wormhole owner
|
||||||
|
sol_run_or_die("airdrop", [
|
||||||
|
str(SOL_AIRDROP_AMT),
|
||||||
|
"--keypair", P2W_OWNER_KEYPAIR,
|
||||||
|
"--commitment", "finalized",
|
||||||
|
], capture_output=True)
|
||||||
|
|
||||||
|
# Initialize pyth2wormhole
|
||||||
|
init_result = run_or_die([
|
||||||
|
"pyth2wormhole-client",
|
||||||
|
"--log-level", "4",
|
||||||
|
"--p2w-addr", P2W_ADDRESS,
|
||||||
|
"--rpc-url", SOL_RPC_URL,
|
||||||
|
"--payer", P2W_OWNER_KEYPAIR,
|
||||||
|
"init",
|
||||||
|
"--wh-prog", WORMHOLE_ADDRESS,
|
||||||
|
"--owner", P2W_OWNER_ADDRESS,
|
||||||
|
"--pyth-owner", PYTH_OWNER_ADDRESS,
|
||||||
|
], capture_output=True, die=False)
|
||||||
|
|
||||||
|
if init_result.returncode != 0:
|
||||||
|
print("NOTE: pyth2wormhole-client init failed, retrying with set_config")
|
||||||
|
run_or_die([
|
||||||
|
"pyth2wormhole-client",
|
||||||
|
"--log-level", "4",
|
||||||
|
"--p2w-addr", P2W_ADDRESS,
|
||||||
|
"--rpc-url", SOL_RPC_URL,
|
||||||
|
"--payer", P2W_OWNER_KEYPAIR,
|
||||||
|
"set-config",
|
||||||
|
"--owner", P2W_OWNER_KEYPAIR,
|
||||||
|
"--new-owner", P2W_OWNER_ADDRESS,
|
||||||
|
"--new-wh-prog", WORMHOLE_ADDRESS,
|
||||||
|
"--new-pyth-owner", PYTH_OWNER_ADDRESS,
|
||||||
|
], capture_output=True)
|
||||||
|
|
||||||
|
# Retrieve current price/product pubkeys from the pyth publisher
|
||||||
|
conn = HTTPConnection(PYTH_ACCOUNTS_HOST, PYTH_ACCOUNTS_PORT)
|
||||||
|
|
||||||
|
conn.request("GET", "/")
|
||||||
|
|
||||||
|
res = conn.getresponse()
|
||||||
|
|
||||||
|
pyth_accounts = None
|
||||||
|
|
||||||
|
if res.getheader("Content-Type") == "application/json":
|
||||||
|
pyth_accounts = json.load(res)
|
||||||
|
else:
|
||||||
|
print(f"Bad Content type {res.getheader('Content-Type')}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
price_addr = pyth_accounts["price"]
|
||||||
|
product_addr = pyth_accounts["product"]
|
||||||
|
|
||||||
|
nonce = 0
|
||||||
|
attest_result = run_or_die([
|
||||||
|
"pyth2wormhole-client",
|
||||||
|
"--log-level", "4",
|
||||||
|
"--p2w-addr", P2W_ADDRESS,
|
||||||
|
"--rpc-url", SOL_RPC_URL,
|
||||||
|
"--payer", P2W_OWNER_KEYPAIR,
|
||||||
|
"attest",
|
||||||
|
"--price", price_addr,
|
||||||
|
"--product", product_addr,
|
||||||
|
"--nonce", str(nonce),
|
||||||
|
], capture_output=True)
|
||||||
|
|
||||||
|
print("p2w_autoattest ready to roll.")
|
||||||
|
print(f"ACCOUNTS: {pyth_accounts}")
|
||||||
|
print(f"Attest Interval: {P2W_ATTEST_INTERVAL}")
|
||||||
|
|
||||||
|
# Let k8s know the service is up
|
||||||
|
readiness_thread = threading.Thread(target=readiness, daemon=True)
|
||||||
|
readiness_thread.start()
|
||||||
|
|
||||||
|
nonce = 1
|
||||||
|
while True:
|
||||||
|
attest_result = run_or_die([
|
||||||
|
"pyth2wormhole-client",
|
||||||
|
"--log-level", "4",
|
||||||
|
"--p2w-addr", P2W_ADDRESS,
|
||||||
|
"--rpc-url", SOL_RPC_URL,
|
||||||
|
"--payer", P2W_OWNER_KEYPAIR,
|
||||||
|
"attest",
|
||||||
|
"--price", price_addr,
|
||||||
|
"--product", product_addr,
|
||||||
|
"--nonce", str(nonce),
|
||||||
|
], capture_output=True)
|
||||||
|
time.sleep(P2W_ATTEST_INTERVAL)
|
||||||
|
nonce += 1
|
||||||
|
|
||||||
|
readiness_thread.join()
|
|
@ -2,33 +2,72 @@
|
||||||
|
|
||||||
from pyth_utils import *
|
from pyth_utils import *
|
||||||
|
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Accept connections from readiness probe
|
|
||||||
def publisher_readiness():
|
|
||||||
run_or_die(["nc", "-k", "-l", "-p", READINESS_PORT])
|
|
||||||
|
|
||||||
# Update the specified price with random values
|
class P2WAccEndpoint(BaseHTTPRequestHandler):
|
||||||
|
"""
|
||||||
|
A dumb endpoint to respond with a JSON containing Pyth account addresses
|
||||||
|
"""
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
print(f"Got path {self.path}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
data = json.dumps(ACCOUNTS).encode("utf-8")
|
||||||
|
print(f"Sending:\n{data}")
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
self.wfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNTS = dict()
|
||||||
|
|
||||||
|
|
||||||
def publisher_random_update(price_pubkey):
|
def publisher_random_update(price_pubkey):
|
||||||
|
"""
|
||||||
|
Update the specified price with random values
|
||||||
|
"""
|
||||||
value = random.randrange(1024)
|
value = random.randrange(1024)
|
||||||
confidence = 1
|
confidence = 1
|
||||||
pyth_run_or_die("upd_price_val", args=[price_pubkey, str(value), str(confidence), "trading"])
|
pyth_run_or_die("upd_price_val", args=[
|
||||||
print("Price updated!")
|
price_pubkey, str(value), str(confidence), "trading"
|
||||||
|
])
|
||||||
|
print(f"Price {price_pubkey} value updated to {str(value)}!")
|
||||||
|
|
||||||
|
|
||||||
|
def accounts_endpoint():
|
||||||
|
"""
|
||||||
|
Run a barebones HTTP server to share the dynamic Pyth
|
||||||
|
mapping/product/price account addresses
|
||||||
|
"""
|
||||||
|
server_address = ('', 4242)
|
||||||
|
httpd = HTTPServer(server_address, P2WAccEndpoint)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
# Fund the publisher
|
# Fund the publisher
|
||||||
sol_run_or_die("airdrop", [str(SOL_AIRDROP_AMT),
|
sol_run_or_die("airdrop", [
|
||||||
"--keypair", PYTH_PUBLISHER_KEYPAIR,
|
str(SOL_AIRDROP_AMT),
|
||||||
"--commitment", "finalized",
|
"--keypair", PYTH_PUBLISHER_KEYPAIR,
|
||||||
])
|
"--commitment", "finalized",
|
||||||
|
])
|
||||||
|
|
||||||
# Create a mapping
|
# Create a mapping
|
||||||
pyth_run_or_die("init_mapping")
|
pyth_run_or_die("init_mapping")
|
||||||
|
|
||||||
# Add a product
|
# Add a product
|
||||||
prod_pubkey = pyth_run_or_die("add_product", capture_output=True).stdout.strip()
|
prod_pubkey = pyth_run_or_die(
|
||||||
|
"add_product", capture_output=True).stdout.strip()
|
||||||
print(f"Added product {prod_pubkey}")
|
print(f"Added product {prod_pubkey}")
|
||||||
|
|
||||||
# Add a price
|
# Add a price
|
||||||
|
@ -41,26 +80,40 @@ price_pubkey = pyth_run_or_die(
|
||||||
|
|
||||||
print(f"Added price {price_pubkey}")
|
print(f"Added price {price_pubkey}")
|
||||||
|
|
||||||
publisher_pubkey = sol_run_or_die("address", args=["--keypair", PYTH_PUBLISHER_KEYPAIR], capture_output=True).stdout.strip()
|
publisher_pubkey = sol_run_or_die("address", args=[
|
||||||
|
"--keypair", PYTH_PUBLISHER_KEYPAIR
|
||||||
|
], capture_output=True).stdout.strip()
|
||||||
|
|
||||||
# Become a publisher
|
# Become a publisher
|
||||||
pyth_run_or_die("add_publisher", args=[publisher_pubkey, price_pubkey], confirm=False, debug=True, capture_output=True)
|
pyth_run_or_die(
|
||||||
|
"add_publisher", args=[publisher_pubkey, price_pubkey],
|
||||||
|
confirm=False,
|
||||||
|
debug=True,
|
||||||
|
capture_output=True)
|
||||||
print(f"Added publisher {publisher_pubkey}")
|
print(f"Added publisher {publisher_pubkey}")
|
||||||
|
|
||||||
# Update the price as the newly added publisher
|
# Update the price as the newly added publisher
|
||||||
publisher_random_update(price_pubkey)
|
publisher_random_update(price_pubkey)
|
||||||
|
|
||||||
print(f"Updated price {price_pubkey}. Mock updates ready to roll. Updating every {str(PYTH_PUBLISHER_INTERVAL)} seconds")
|
print(
|
||||||
|
f"Mock updates ready to roll. Updating every {str(PYTH_PUBLISHER_INTERVAL)} seconds")
|
||||||
|
|
||||||
# Spin off the readiness probe endpoint into a separate thread
|
# Spin off the readiness probe endpoint into a separate thread
|
||||||
readiness_thread = threading.Thread(target=publisher_readiness)
|
readiness_thread = threading.Thread(target=readiness, daemon=True)
|
||||||
|
|
||||||
|
# Start an HTTP endpoint for looking up product/price address
|
||||||
|
http_service = threading.Thread(target=accounts_endpoint, daemon=True)
|
||||||
|
|
||||||
|
ACCOUNTS["product"] = prod_pubkey
|
||||||
|
ACCOUNTS["price"] = price_pubkey
|
||||||
|
|
||||||
readiness_thread.start()
|
readiness_thread.start()
|
||||||
|
http_service.start()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
print(f"Updating price {price_pubkey}")
|
|
||||||
publisher_random_update(price_pubkey)
|
publisher_random_update(price_pubkey)
|
||||||
time.sleep(PYTH_PUBLISHER_INTERVAL)
|
time.sleep(PYTH_PUBLISHER_INTERVAL)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
readiness_thread.join()
|
readiness_thread.join()
|
||||||
|
http_service.join()
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import os
|
import os
|
||||||
|
import socketserver
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
PYTH=os.environ.get("PYTH", "./pyth")
|
PYTH = os.environ.get("PYTH", "./pyth")
|
||||||
PYTH_KEY_STORE = os.environ.get("PYTH_KEY_STORE", "/home/pyth/.pythd")
|
PYTH_KEY_STORE = os.environ.get("PYTH_KEY_STORE", "/home/pyth/.pythd")
|
||||||
PYTH_PROGRAM_KEYPAIR = f"{PYTH_KEY_STORE}/program_key_pair.json"
|
PYTH_PROGRAM_KEYPAIR = os.environ.get(
|
||||||
PYTH_PROGRAM_SO_PATH=os.environ.get("PYTH_PROGRAM_SO", "../target/oracle.so")
|
"PYTH_PROGRAM_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json")
|
||||||
PYTH_PUBLISHER_KEYPAIR = f"{PYTH_KEY_STORE}/publish_key_pair.json"
|
PYTH_PROGRAM_SO_PATH = os.environ.get("PYTH_PROGRAM_SO", "../target/oracle.so")
|
||||||
|
PYTH_PUBLISHER_KEYPAIR = os.environ.get(
|
||||||
|
"PYTH_PUBLISHER_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json")
|
||||||
PYTH_PUBLISHER_INTERVAL = float(os.environ.get("PYTH_PUBLISHER_INTERVAL", "5"))
|
PYTH_PUBLISHER_INTERVAL = float(os.environ.get("PYTH_PUBLISHER_INTERVAL", "5"))
|
||||||
|
|
||||||
SOL_AIRDROP_AMT = 100
|
SOL_AIRDROP_AMT = 100
|
||||||
|
@ -14,10 +17,13 @@ SOL_RPC_HOST = "solana-devnet"
|
||||||
SOL_RPC_PORT = 8899
|
SOL_RPC_PORT = 8899
|
||||||
SOL_RPC_URL = f"http://{SOL_RPC_HOST}:{str(SOL_RPC_PORT)}"
|
SOL_RPC_URL = f"http://{SOL_RPC_HOST}:{str(SOL_RPC_PORT)}"
|
||||||
|
|
||||||
READINESS_PORT=os.environ.get("READINESS_PORT", "2000")
|
READINESS_PORT = int(os.environ.get("READINESS_PORT", "2000"))
|
||||||
|
|
||||||
|
|
||||||
# pretend we're set -e
|
|
||||||
def run_or_die(args, die=True, **kwargs):
|
def run_or_die(args, die=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Opinionated subprocess.run() call with fancy logging
|
||||||
|
"""
|
||||||
args_readable = ' '.join(args)
|
args_readable = ' '.join(args)
|
||||||
print(f"CMD RUN\t{args_readable}", file=sys.stderr)
|
print(f"CMD RUN\t{args_readable}", file=sys.stderr)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
@ -42,18 +48,41 @@ def run_or_die(args, die=True, **kwargs):
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
# Pyth boilerplate in front of run_or_die
|
|
||||||
def pyth_run_or_die(subcommand, args=[], debug=False, confirm=True, **kwargs):
|
|
||||||
return run_or_die([PYTH, subcommand]
|
|
||||||
+ args
|
|
||||||
+ (["-d"] if debug else [])
|
|
||||||
+ ([] if confirm else ["-n"]) # Note: not all pyth subcommands accept -n
|
|
||||||
+ ["-k", PYTH_KEY_STORE]
|
|
||||||
+ ["-r", SOL_RPC_HOST]
|
|
||||||
+ ["-c", "finalized"], **kwargs)
|
|
||||||
|
|
||||||
# Solana boilerplate in front of run_or_die
|
def pyth_run_or_die(subcommand, args=[], debug=False, confirm=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Pyth boilerplate in front of run_or_die
|
||||||
|
"""
|
||||||
|
return run_or_die(
|
||||||
|
[PYTH, subcommand]
|
||||||
|
+ args
|
||||||
|
+ (["-d"] if debug else [])
|
||||||
|
# Note: not all pyth subcommands accept -n
|
||||||
|
+ ([] if confirm else ["-n"])
|
||||||
|
+ ["-k", PYTH_KEY_STORE]
|
||||||
|
+ ["-r", SOL_RPC_HOST]
|
||||||
|
+ ["-c", "finalized"], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def sol_run_or_die(subcommand, args=[], **kwargs):
|
def sol_run_or_die(subcommand, args=[], **kwargs):
|
||||||
|
"""
|
||||||
|
Solana boilerplate in front of run_or_die
|
||||||
|
"""
|
||||||
return run_or_die(["solana", subcommand]
|
return run_or_die(["solana", subcommand]
|
||||||
+ args
|
+ args
|
||||||
+ ["--url", SOL_RPC_URL], **kwargs)
|
+ ["--url", SOL_RPC_URL], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadinessTCPHandler(socketserver.StreamRequestHandler):
|
||||||
|
def handle(self):
|
||||||
|
"""TCP black hole"""
|
||||||
|
self.rfile.read(64)
|
||||||
|
|
||||||
|
|
||||||
|
def readiness():
|
||||||
|
"""
|
||||||
|
Accept connections from readiness probe
|
||||||
|
"""
|
||||||
|
with socketserver.TCPServer(("0.0.0.0", READINESS_PORT), ReadinessTCPHandler) as srv:
|
||||||
|
srv.serve_forever()
|
||||||
|
# run_or_die(["nc", "-k", "-l", "-p", READINESS_PORT])
|
||||||
|
|
Loading…
Reference in New Issue