[WIP] Pr/drozdziak1/p2w batching/5e704f8b (#877)
* ethereum: p2w contract -> p2w emitter, fill in essential envs Change-Id: I6fa9364a96738d2cc02ec829a31fedba0586d8e8 commit-id:0a56f1f8 * Add p2w-relay, a p2w-sdk integration test commit-id:6bfab639 * p2w-sdk: Expand README Change-Id: I17cb547d6aaddc240588923561c26d11a787df2e commit-id:6ebd6a22 * p2w-sdk: don't build ETH contracts, only the types Change-Id: I7cbd18328227700635d7688aa24a9671e8919fcd commit-id:adf079f7 * p2w: configurability and sane envs commit-id:f10fd90e * Solitaire: Implement Option<T> support in structs commit-id:31aa12d6 * bridge/governance.rs: Stop pestering about EMITTER_ADDRESS commit-id:d5bd7234 * p2w-attest: price batching This commit introduces support for multiple Pyth product/price pairs per call. The initial maximum batch size is 5 and is enforced using a `P2W_MAX_BATCH_SIZE` constant. solana/pyth2wormhole/program: * On-chain batching logic * Batch message parsing logic solana/pyth2wormhole/client: * Off-chain batching logic - divides any number of symbols into largest possible batches * Use a multi-symbol config file instead of CLI arguments third_party/pyth/p2w-sdk: * Expose batch parsing logic third_party/pyth/p2w-relay: * Comment out target chain calls until ETH contract supports batching * Test the batch parsing function third_party/pyth/p2w_autoattest.py: * Generate and use the symbol config file with pyth2wormhole-client third_party/pyth/pyth_publisher.py: * Add a configurable number of mock Pyth symbols * Adjust HTTP endpoint for multiple symbols commit-id:73787a61 * p2w-attest: mention attestation size in batch This commit ensures that no matter the attestation format, a batch will never contain attestations of different sizes. This guarantee enables forward compatibility by adding new constant-size fields at the end of a batch at all times. An older implementation will simply not consume the remaining newer values while respecting the stated batch member alignment. commit-id:210da230 * pyth2wormhole-client: use fresh blockhashes, harden batch errors This commit makes sure we don't have to deal with expired transactions due to stale blockhashes. The problem existed with larger symbol configs as well as on Solana mainnet. Additionally, the attestation logic now treats transaction errors as non-critical - a failure for a batch does not prevent attestation attempts for batches farther in the queue commit-id:5e704f8b
This commit is contained in:
parent
3b10f124a1
commit
2ea41b8176
16
Tiltfile
16
Tiltfile
|
@ -89,7 +89,7 @@ local_resource(
|
|||
|
||||
local_resource(
|
||||
name = "proto-gen-web",
|
||||
deps = proto_deps,
|
||||
deps = proto_deps + ["buf.gen.web.yaml"],
|
||||
resource_deps = ["proto-gen"],
|
||||
cmd = "tilt docker build -- --target node-export -f Dockerfile.proto -o type=local,dest=. .",
|
||||
env = {"DOCKER_BUILDKIT": "1"},
|
||||
|
@ -273,6 +273,13 @@ if pyth:
|
|||
ignore = ["./solana/*/target"],
|
||||
)
|
||||
|
||||
# Automatic pyth2wormhole relay, showcasing p2w-sdk
|
||||
docker_build(
|
||||
ref = "p2w-relay",
|
||||
context = ".",
|
||||
dockerfile = "./third_party/pyth/p2w-relay/Dockerfile",
|
||||
)
|
||||
|
||||
k8s_yaml_with_ns("devnet/p2w-attest.yaml")
|
||||
k8s_resource(
|
||||
"p2w-attest",
|
||||
|
@ -282,6 +289,13 @@ if pyth:
|
|||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
||||
k8s_yaml_with_ns("devnet/p2w-relay.yaml")
|
||||
k8s_resource(
|
||||
"p2w-relay",
|
||||
resource_deps = ["solana-devnet", "eth-devnet", "pyth", "guardian", "p2w-attest", "proto-gen-web", "wasm-gen"],
|
||||
port_forwards = [],
|
||||
)
|
||||
|
||||
k8s_yaml_with_ns("devnet/eth-devnet.yaml")
|
||||
|
||||
k8s_resource(
|
||||
|
|
|
@ -37,6 +37,9 @@ spec:
|
|||
command:
|
||||
- python3
|
||||
- /usr/src/pyth/p2w_autoattest.py
|
||||
env:
|
||||
- name: P2W_INITIALIZE_SOL_CONTRACT
|
||||
value: "1"
|
||||
tty: true
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
|
|
|
@ -31,8 +31,9 @@ spec:
|
|||
- name: p2w-relay
|
||||
image: p2w-relay
|
||||
command:
|
||||
- node
|
||||
- /usr/src/third_party/pyth/p2w-sdk/lib/autorelayer.js
|
||||
- npm
|
||||
- start
|
||||
workingDir: /usr/src/third_party/pyth/p2w-relay/
|
||||
tty: true
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
|
|
|
@ -21,5 +21,5 @@ BRIDGE_INIT_WETH= # 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
|
|||
PYTH_INIT_CHAIN_ID= # 0x2
|
||||
PYTH_INIT_GOV_CHAIN_ID= # 0x3
|
||||
PYTH_INIT_GOV_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000004
|
||||
PYTH_TO_WORMHOLE_CHAIN_ID= # 0x5
|
||||
PYTH_TO_WORMHOLE_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000006
|
||||
PYTH_TO_WORMHOLE_CHAIN_ID= # 0x1
|
||||
PYTH_TO_WORMHOLE_EMITTER= # 8fuAZUxHecYLMC76ZNjYzwRybUiDv9LhkRQsAccEykLr
|
||||
|
|
|
@ -14,5 +14,5 @@ BRIDGE_INIT_WETH=0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E
|
|||
PYTH_INIT_CHAIN_ID=0x2
|
||||
PYTH_INIT_GOV_CHAIN_ID=0x3
|
||||
PYTH_INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004
|
||||
PYTH_TO_WORMHOLE_CHAIN_ID=0x5
|
||||
PYTH_TO_WORMHOLE_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000006
|
||||
PYTH_TO_WORMHOLE_CHAIN_ID=0x1
|
||||
PYTH_TO_WORMHOLE_EMITTER=8fuAZUxHecYLMC76ZNjYzwRybUiDv9LhkRQsAccEykLr
|
|
@ -24,6 +24,6 @@ RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
|
|||
# Amusingly, Debian's coreutils version has a bug where mv believes that
|
||||
# the target is on a different fs and does a full recursive copy for what
|
||||
# could be a renameat syscall. Alpine does not have this bug.
|
||||
RUN rmdir node_modules && mv node_modules_cache node_modules
|
||||
RUN rm -rf node_modules && mv node_modules_cache node_modules
|
||||
|
||||
ADD --chown=node:node . .
|
||||
|
|
|
@ -34,7 +34,7 @@ contract Pyth is PythGovernance {
|
|||
if (vm.emitterChainId != pyth2WormholeChainId()) {
|
||||
return false;
|
||||
}
|
||||
if (vm.emitterAddress != pyth2WormholeContract()) {
|
||||
if (vm.emitterAddress != pyth2WormholeEmitter()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -36,8 +36,8 @@ contract PythGetters is PythState {
|
|||
return _state.provider.pyth2WormholeChainId;
|
||||
}
|
||||
|
||||
function pyth2WormholeContract() public view returns (bytes32){
|
||||
return _state.provider.pyth2WormholeContract;
|
||||
function pyth2WormholeEmitter() public view returns (bytes32){
|
||||
return _state.provider.pyth2WormholeEmitter;
|
||||
}
|
||||
|
||||
function latestAttestation(bytes32 product, uint8 priceType) public view returns (PythStructs.PriceAttestation memory attestation){
|
||||
|
|
|
@ -30,8 +30,8 @@ contract PythSetters is PythState {
|
|||
_state.provider.pyth2WormholeChainId = chainId;
|
||||
}
|
||||
|
||||
function setPyth2WormholeContract(bytes32 contractAddr) internal {
|
||||
_state.provider.pyth2WormholeContract = contractAddr;
|
||||
function setPyth2WormholeEmitter(bytes32 emitterAddr) internal {
|
||||
_state.provider.pyth2WormholeEmitter = emitterAddr;
|
||||
}
|
||||
|
||||
function setWormhole(address wh) internal {
|
||||
|
|
|
@ -19,7 +19,7 @@ contract PythSetup is PythSetters, ERC1967Upgrade {
|
|||
bytes32 governanceContract,
|
||||
|
||||
uint16 pyth2WormholeChainId,
|
||||
bytes32 pyth2WormholeContract
|
||||
bytes32 pyth2WormholeEmitter
|
||||
) public {
|
||||
setChainId(chainId);
|
||||
|
||||
|
@ -29,7 +29,7 @@ contract PythSetup is PythSetters, ERC1967Upgrade {
|
|||
setGovernanceContract(governanceContract);
|
||||
|
||||
setPyth2WormholeChainId(pyth2WormholeChainId);
|
||||
setPyth2WormholeContract(pyth2WormholeContract);
|
||||
setPyth2WormholeEmitter(pyth2WormholeEmitter);
|
||||
|
||||
_upgradeTo(implementation);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ contract PythStorage {
|
|||
bytes32 governanceContract;
|
||||
|
||||
uint16 pyth2WormholeChainId;
|
||||
bytes32 pyth2WormholeContract;
|
||||
bytes32 pyth2WormholeEmitter;
|
||||
}
|
||||
|
||||
struct State {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
myth like bonus scare over problem client lizard pioneer submit female collect
|
|
@ -1,4 +1,5 @@
|
|||
require('dotenv').config({ path: "../.env" });
|
||||
const bs58 = require("bs58");
|
||||
|
||||
const PythDataBridge = artifacts.require("PythDataBridge");
|
||||
const PythImplementation = artifacts.require("PythImplementation");
|
||||
|
@ -9,7 +10,9 @@ const chainId = process.env.PYTH_INIT_CHAIN_ID;
|
|||
const governanceChainId = process.env.PYTH_INIT_GOV_CHAIN_ID;
|
||||
const governanceContract = process.env.PYTH_INIT_GOV_CONTRACT; // bytes32
|
||||
const pyth2WormholeChainId = process.env.PYTH_TO_WORMHOLE_CHAIN_ID;
|
||||
const pyth2WormholeContract = process.env.PYTH_TO_WORMHOLE_CONTRACT; // bytes32
|
||||
const pyth2WormholeEmitter = bs58.decode(process.env.PYTH_TO_WORMHOLE_EMITTER); // base58, must fit into bytes32
|
||||
|
||||
console.log("Deploying Pyth with emitter", pyth2WormholeEmitter.toString("hex"))
|
||||
|
||||
module.exports = async function (deployer) {
|
||||
// deploy implementation
|
||||
|
@ -29,7 +32,7 @@ module.exports = async function (deployer) {
|
|||
governanceContract,
|
||||
|
||||
pyth2WormholeChainId,
|
||||
pyth2WormholeContract,
|
||||
"0x" + pyth2WormholeEmitter.toString("hex"),
|
||||
).encodeABI();
|
||||
|
||||
// deploy proxy
|
||||
|
|
|
@ -20,7 +20,7 @@ contract("Pyth", function () {
|
|||
const testGovernanceChainId = "3";
|
||||
const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004";
|
||||
const testPyth2WormholeChainId = "5";
|
||||
const testPyth2WormholeContract = "0x0000000000000000000000000000000000000000000000000000000000000006";
|
||||
const testPyth2WormholeEmitter = "0x0000000000000000000000000000000000000000000000000000000000000006";
|
||||
|
||||
|
||||
it("should be initialized with the correct signers and values", async function(){
|
||||
|
@ -39,8 +39,8 @@ contract("Pyth", function () {
|
|||
// pyth2wormhole
|
||||
const pyth2wormChain = await initialized.methods.pyth2WormholeChainId().call();
|
||||
assert.equal(pyth2wormChain, testPyth2WormholeChainId);
|
||||
const pyth2wormContract = await initialized.methods.pyth2WormholeContract().call();
|
||||
assert.equal(pyth2wormContract, testPyth2WormholeContract);
|
||||
const pyth2wormEmitter = await initialized.methods.pyth2WormholeEmitter().call();
|
||||
assert.equal(pyth2wormEmitter, testPyth2WormholeEmitter);
|
||||
})
|
||||
|
||||
it("should accept a valid upgrade", async function() {
|
||||
|
@ -132,7 +132,7 @@ contract("Pyth", function () {
|
|||
1,
|
||||
1,
|
||||
testPyth2WormholeChainId,
|
||||
testPyth2WormholeContract,
|
||||
testPyth2WormholeEmitter,
|
||||
0,
|
||||
testUpdate,
|
||||
[
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use solitaire::*;
|
||||
|
||||
use solana_program::{
|
||||
log::sol_log,
|
||||
program::invoke_signed,
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
sysvar::{
|
||||
clock::Clock,
|
||||
|
@ -40,7 +42,10 @@ fn verify_governance<'a, T>(vaa: &ClaimableVAA<'a, T>) -> Result<()>
|
|||
where
|
||||
T: DeserializePayload,
|
||||
{
|
||||
let expected_emitter = std::env!("EMITTER_ADDRESS");
|
||||
let expected_emitter = std::option_env!("EMITTER_ADDRESS").ok_or_else(|| {
|
||||
sol_log("EMITTER_ADDRESS not set at compile-time");
|
||||
ProgramError::UninitializedAccount
|
||||
})?;
|
||||
let current_emitter = format!(
|
||||
"{}",
|
||||
Pubkey::new_from_array(vaa.message.meta().emitter_address)
|
||||
|
|
|
@ -1785,6 +1785,8 @@ dependencies = [
|
|||
"env_logger 0.8.4",
|
||||
"log",
|
||||
"pyth2wormhole",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"shellexpand",
|
||||
"solana-client",
|
||||
"solana-program",
|
||||
|
|
|
@ -15,6 +15,8 @@ env_logger = "0.8.4"
|
|||
log = "0.4.14"
|
||||
wormhole-bridge-solana = {path = "../../bridge/program"}
|
||||
pyth2wormhole = {path = "../program"}
|
||||
serde = "1"
|
||||
serde_yaml = "0.8"
|
||||
shellexpand = "2.1.0"
|
||||
solana-client = "=1.9.4"
|
||||
solana-program = "=1.9.4"
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use serde::{
|
||||
de::Error,
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
Serialize,
|
||||
Serializer,
|
||||
};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use solitaire::ErrBox;
|
||||
|
||||
/// Pyth2wormhole config specific to attestation requests
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct AttestationConfig {
|
||||
pub symbols: Vec<P2WSymbol>,
|
||||
}
|
||||
|
||||
/// Config entry for a Pyth product + price pair
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct P2WSymbol {
|
||||
/// User-defined human-readable name
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(
|
||||
deserialize_with = "pubkey_string_de",
|
||||
serialize_with = "pubkey_string_ser"
|
||||
)]
|
||||
pub product_addr: Pubkey,
|
||||
#[serde(
|
||||
deserialize_with = "pubkey_string_de",
|
||||
serialize_with = "pubkey_string_ser"
|
||||
)]
|
||||
pub price_addr: Pubkey,
|
||||
}
|
||||
|
||||
// Helper methods for strinigified SOL addresses
|
||||
|
||||
fn pubkey_string_ser<S>(k: &Pubkey, ser: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
ser.serialize_str(&k.to_string())
|
||||
}
|
||||
|
||||
fn pubkey_string_de<'de, D>(de: D) -> Result<Pubkey, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let pubkey_string = String::deserialize(de)?;
|
||||
let pubkey = Pubkey::from_str(&pubkey_string).map_err(D::Error::custom)?;
|
||||
Ok(pubkey)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sanity() -> Result<(), ErrBox> {
|
||||
let initial = AttestationConfig {
|
||||
symbols: vec![
|
||||
P2WSymbol {
|
||||
name: Some("ETH/USD".to_owned()),
|
||||
product_addr: Default::default(),
|
||||
price_addr: Default::default(),
|
||||
},
|
||||
P2WSymbol {
|
||||
name: None,
|
||||
product_addr: Pubkey::new(&[42u8; 32]),
|
||||
price_addr: Default::default(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let serialized = serde_yaml::to_string(&initial)?;
|
||||
eprintln!("Serialized:\n{}", serialized);
|
||||
|
||||
let deserialized: AttestationConfig = serde_yaml::from_str(&serialized)?;
|
||||
|
||||
assert_eq!(initial, deserialized);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
//! CLI options
|
||||
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Clap;
|
||||
#[derive(Clap)]
|
||||
|
@ -22,7 +23,7 @@ pub struct Cli {
|
|||
default_value = "~/.config/solana/id.json"
|
||||
)]
|
||||
pub payer: String,
|
||||
#[clap(long, default_value = "http://localhost:8899")]
|
||||
#[clap(short, long, default_value = "http://localhost:8899")]
|
||||
pub rpc_url: String,
|
||||
#[clap(long)]
|
||||
pub p2w_addr: Pubkey,
|
||||
|
@ -35,23 +36,19 @@ pub enum Action {
|
|||
#[clap(about = "Initialize a pyth2wormhole program freshly deployed under <p2w_addr>")]
|
||||
Init {
|
||||
/// The bridge program account
|
||||
#[clap(long = "wh-prog")]
|
||||
#[clap(short = 'w', long = "wh-prog")]
|
||||
wh_prog: Pubkey,
|
||||
#[clap(long = "owner")]
|
||||
#[clap(short = 'o', long = "owner")]
|
||||
owner_addr: Pubkey,
|
||||
#[clap(long = "pyth-owner")]
|
||||
#[clap(short = 'p', long = "pyth-owner")]
|
||||
pyth_owner_addr: Pubkey,
|
||||
},
|
||||
#[clap(
|
||||
about = "Use an existing pyth2wormhole program to attest product price information to another chain"
|
||||
)]
|
||||
Attest {
|
||||
#[clap(long = "product")]
|
||||
product_addr: Pubkey,
|
||||
#[clap(long = "price")]
|
||||
price_addr: Pubkey,
|
||||
#[clap(long)]
|
||||
nonce: u32,
|
||||
#[clap(short = 'f', long = "--config", about = "Attestation YAML config")]
|
||||
attestation_cfg: PathBuf,
|
||||
},
|
||||
#[clap(about = "Update an existing pyth2wormhole program's settings (currently set owner only)")]
|
||||
SetConfig {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
symbols: Vec<P2WSymbol>,
|
||||
}
|
||||
|
||||
/// Config entry for a Pyth2Wormhole product + price pair
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct P2WSymbol {
|
||||
/// Optional human-readable name, never used on-chain; makes
|
||||
/// attester logs and the config easier to understand
|
||||
name: Option<String>,
|
||||
product: Pubkey,
|
||||
price: Pubkey,
|
||||
}
|
||||
|
||||
#[testmod]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_sanity() -> Result<(), ErrBox> {
|
||||
let serialized = r#"
|
||||
symbols:
|
||||
- name: ETH/USD
|
||||
product_addr: 11111111111111111111111111111111
|
||||
price_addr: 11111111111111111111111111111111
|
||||
- name: SOL/EUR
|
||||
product_addr: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
|
||||
price_addr: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
|
||||
- name: BTC/CNY
|
||||
product_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
|
||||
price_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
|
||||
- # no name
|
||||
product_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
|
||||
price_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
|
||||
"#;
|
||||
let deserialized = serde_yaml::from_str(serialized)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,11 +1,23 @@
|
|||
pub mod attestation_cfg;
|
||||
pub mod cli;
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{
|
||||
Path,
|
||||
PathBuf,
|
||||
},
|
||||
};
|
||||
|
||||
use borsh::{
|
||||
BorshDeserialize,
|
||||
BorshSerialize,
|
||||
};
|
||||
use clap::Clap;
|
||||
use log::{
|
||||
debug,
|
||||
error,
|
||||
info,
|
||||
warn,
|
||||
LevelFilter,
|
||||
};
|
||||
|
@ -59,7 +71,10 @@ use bridge::{
|
|||
};
|
||||
|
||||
use pyth2wormhole::{
|
||||
attest::P2WEmitter,
|
||||
attest::{
|
||||
P2WEmitter,
|
||||
P2W_MAX_BATCH_SIZE,
|
||||
},
|
||||
config::P2WConfigAccount,
|
||||
initialize::InitializeAccounts,
|
||||
set_config::SetConfigAccounts,
|
||||
|
@ -68,6 +83,8 @@ use pyth2wormhole::{
|
|||
Pyth2WormholeConfig,
|
||||
};
|
||||
|
||||
use crate::attestation_cfg::AttestationConfig;
|
||||
|
||||
pub type ErrBox = Box<dyn std::error::Error>;
|
||||
|
||||
pub const SEQNO_PREFIX: &'static str = "Program log: Sequence: ";
|
||||
|
@ -81,65 +98,49 @@ fn main() -> Result<(), ErrBox> {
|
|||
|
||||
let p2w_addr = cli.p2w_addr;
|
||||
|
||||
let (recent_blockhash, _) = rpc_client.get_recent_blockhash()?;
|
||||
let latest_blockhash = rpc_client.get_latest_blockhash()?;
|
||||
|
||||
let tx = match cli.action {
|
||||
match cli.action {
|
||||
Action::Init {
|
||||
owner_addr,
|
||||
pyth_owner_addr,
|
||||
wh_prog,
|
||||
} => handle_init(
|
||||
payer,
|
||||
p2w_addr,
|
||||
owner_addr,
|
||||
wh_prog,
|
||||
pyth_owner_addr,
|
||||
recent_blockhash,
|
||||
)?,
|
||||
} => {
|
||||
let tx = handle_init(
|
||||
payer,
|
||||
p2w_addr,
|
||||
owner_addr,
|
||||
wh_prog,
|
||||
pyth_owner_addr,
|
||||
latest_blockhash,
|
||||
)?;
|
||||
rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
|
||||
}
|
||||
Action::SetConfig {
|
||||
ref 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,
|
||||
)?,
|
||||
} => {
|
||||
let tx = handle_set_config(
|
||||
payer,
|
||||
p2w_addr,
|
||||
read_keypair_file(&*shellexpand::tilde(&owner))?,
|
||||
new_owner_addr,
|
||||
new_wh_prog,
|
||||
new_pyth_owner_addr,
|
||||
latest_blockhash,
|
||||
)?;
|
||||
rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
|
||||
}
|
||||
Action::Attest {
|
||||
product_addr,
|
||||
price_addr,
|
||||
nonce,
|
||||
} => handle_attest(
|
||||
&rpc_client,
|
||||
payer,
|
||||
p2w_addr,
|
||||
product_addr,
|
||||
price_addr,
|
||||
nonce,
|
||||
recent_blockhash,
|
||||
)?,
|
||||
};
|
||||
ref attestation_cfg,
|
||||
} => {
|
||||
// Load the attestation config yaml
|
||||
let attestation_cfg: AttestationConfig =
|
||||
serde_yaml::from_reader(File::open(attestation_cfg)?)?;
|
||||
|
||||
let sig = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
|
||||
|
||||
// To complete attestation, retrieve sequence number from transaction logs
|
||||
if let Action::Attest { .. } = cli.action {
|
||||
let this_tx = rpc_client.get_transaction(&sig, UiTransactionEncoding::Json)?;
|
||||
|
||||
if let Some(logs) = this_tx.transaction.meta.and_then(|meta| meta.log_messages) {
|
||||
for log in logs {
|
||||
if log.starts_with(SEQNO_PREFIX) {
|
||||
let seqno = log.replace(SEQNO_PREFIX, "");
|
||||
println!("Sequence number: {}", seqno);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Could not get program logs for attestation");
|
||||
handle_attest(&rpc_client, payer, p2w_addr, &attestation_cfg)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +153,7 @@ fn handle_init(
|
|||
new_owner_addr: Pubkey,
|
||||
wh_prog: Pubkey,
|
||||
pyth_owner_addr: Pubkey,
|
||||
recent_blockhash: Hash,
|
||||
latest_blockhash: Hash,
|
||||
) -> Result<Transaction, ErrBox> {
|
||||
use AccEntry::*;
|
||||
|
||||
|
@ -164,6 +165,7 @@ fn handle_init(
|
|||
};
|
||||
|
||||
let config = Pyth2WormholeConfig {
|
||||
max_batch_size: P2W_MAX_BATCH_SIZE,
|
||||
owner: new_owner_addr,
|
||||
wh_prog: wh_prog,
|
||||
pyth_owner: pyth_owner_addr,
|
||||
|
@ -176,7 +178,7 @@ fn handle_init(
|
|||
&[ix],
|
||||
Some(&payer_pubkey),
|
||||
signers.iter().collect::<Vec<_>>().as_ref(),
|
||||
recent_blockhash,
|
||||
latest_blockhash,
|
||||
);
|
||||
Ok(tx_signed)
|
||||
}
|
||||
|
@ -188,14 +190,12 @@ fn handle_set_config(
|
|||
new_owner_addr: Pubkey,
|
||||
new_wh_prog: Pubkey,
|
||||
new_pyth_owner_addr: Pubkey,
|
||||
recent_blockhash: Hash,
|
||||
latest_blockhash: Hash,
|
||||
) -> Result<Transaction, ErrBox> {
|
||||
use AccEntry::*;
|
||||
|
||||
let payer_pubkey = payer.pubkey();
|
||||
|
||||
println!("Canary!");
|
||||
|
||||
let accs = SetConfigAccounts {
|
||||
payer: Signer(payer),
|
||||
current_owner: Signer(owner),
|
||||
|
@ -203,6 +203,7 @@ fn handle_set_config(
|
|||
};
|
||||
|
||||
let config = Pyth2WormholeConfig {
|
||||
max_batch_size: P2W_MAX_BATCH_SIZE,
|
||||
owner: new_owner_addr,
|
||||
wh_prog: new_wh_prog,
|
||||
pyth_owner: new_pyth_owner_addr,
|
||||
|
@ -215,30 +216,28 @@ fn handle_set_config(
|
|||
&[ix],
|
||||
Some(&payer_pubkey),
|
||||
signers.iter().collect::<Vec<_>>().as_ref(),
|
||||
recent_blockhash,
|
||||
latest_blockhash,
|
||||
);
|
||||
Ok(tx_signed)
|
||||
}
|
||||
|
||||
fn handle_attest(
|
||||
rpc: &RpcClient, // Needed for reading Pyth account data
|
||||
rpc_client: &RpcClient, // Needed for reading Pyth account data
|
||||
payer: Keypair,
|
||||
p2w_addr: Pubkey,
|
||||
product_addr: Pubkey,
|
||||
price_addr: Pubkey,
|
||||
nonce: u32,
|
||||
recent_blockhash: Hash,
|
||||
) -> Result<Transaction, ErrBox> {
|
||||
let message_keypair = Keypair::new();
|
||||
|
||||
attestation_cfg: &AttestationConfig,
|
||||
) -> Result<(), ErrBox> {
|
||||
// Derive seeded accounts
|
||||
let emitter_addr = P2WEmitter::key(None, &p2w_addr);
|
||||
|
||||
info!("Using emitter addr {}", emitter_addr);
|
||||
|
||||
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())?;
|
||||
let config = Pyth2WormholeConfig::try_from_slice(
|
||||
rpc_client.get_account_data(&p2w_config_addr)?.as_slice(),
|
||||
)?;
|
||||
|
||||
// Derive dynamic seeded accounts
|
||||
let seq_addr = Sequence::key(
|
||||
&SequenceDerivationData {
|
||||
emitter_key: &emitter_addr,
|
||||
|
@ -246,58 +245,167 @@ fn handle_attest(
|
|||
&config.wh_prog,
|
||||
);
|
||||
|
||||
// Arrange Attest accounts
|
||||
let acc_metas = vec![
|
||||
// payer
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
// system_program
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
// config
|
||||
AccountMeta::new_readonly(p2w_config_addr, false),
|
||||
// pyth_product
|
||||
AccountMeta::new_readonly(product_addr, false),
|
||||
// pyth_price
|
||||
AccountMeta::new_readonly(price_addr, false),
|
||||
// clock
|
||||
AccountMeta::new_readonly(clock::id(), false),
|
||||
// wh_prog
|
||||
AccountMeta::new_readonly(config.wh_prog, false),
|
||||
// wh_bridge
|
||||
AccountMeta::new(
|
||||
Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog),
|
||||
false,
|
||||
),
|
||||
// wh_message
|
||||
AccountMeta::new(message_keypair.pubkey(), true),
|
||||
// wh_emitter
|
||||
AccountMeta::new_readonly(emitter_addr, false),
|
||||
// wh_sequence
|
||||
AccountMeta::new(seq_addr, false),
|
||||
// wh_fee_collector
|
||||
AccountMeta::new(FeeCollector::<'_>::key(None, &config.wh_prog), false),
|
||||
AccountMeta::new_readonly(rent::id(), false),
|
||||
];
|
||||
// Read the current max batch size from the contract's settings
|
||||
let max_batch_size = config.max_batch_size;
|
||||
|
||||
let ix_data = (
|
||||
pyth2wormhole::instruction::Instruction::Attest,
|
||||
AttestData {
|
||||
nonce,
|
||||
consistency_level: ConsistencyLevel::Finalized,
|
||||
},
|
||||
let batch_count = {
|
||||
let whole_batches = attestation_cfg.symbols.len() / config.max_batch_size as usize;
|
||||
|
||||
// Include partial batch if there is a remainder
|
||||
if attestation_cfg.symbols.len() % config.max_batch_size as usize > 0 {
|
||||
whole_batches + 1
|
||||
} else {
|
||||
whole_batches
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Symbol config:\n{:#?}", attestation_cfg);
|
||||
|
||||
info!(
|
||||
"{} symbols read, max batch size {}, dividing into {} batches",
|
||||
attestation_cfg.symbols.len(),
|
||||
max_batch_size,
|
||||
batch_count
|
||||
);
|
||||
|
||||
let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
|
||||
for (idx, symbols) in attestation_cfg
|
||||
.symbols
|
||||
.as_slice()
|
||||
.chunks(max_batch_size as usize)
|
||||
.enumerate()
|
||||
{
|
||||
let batch_no = idx + 1;
|
||||
let sym_msg_keypair = Keypair::new();
|
||||
info!(
|
||||
"Batch {}/{} contents: {:?}",
|
||||
batch_no,
|
||||
batch_count,
|
||||
symbols
|
||||
.iter()
|
||||
.map(|s| s
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or(format!("unnamed product {:?}", s.product_addr)))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// Signers that use off-chain keypairs
|
||||
let signer_keypairs = vec![&payer, &message_keypair];
|
||||
let mut sym_metas_vec: Vec<_> = symbols
|
||||
.iter()
|
||||
.map(|s| {
|
||||
vec![
|
||||
AccountMeta::new_readonly(s.product_addr, false),
|
||||
AccountMeta::new_readonly(s.price_addr, false),
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
|
||||
&[ix],
|
||||
Some(&payer.pubkey()),
|
||||
&signer_keypairs,
|
||||
recent_blockhash,
|
||||
);
|
||||
Ok(tx_signed)
|
||||
// Align to max batch size with null accounts
|
||||
let mut blank_accounts =
|
||||
vec![
|
||||
AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false);
|
||||
2 * (max_batch_size as usize - symbols.len())
|
||||
];
|
||||
sym_metas_vec.append(&mut blank_accounts);
|
||||
|
||||
// Arrange Attest accounts
|
||||
let mut acc_metas = vec![
|
||||
// payer
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
// system_program
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
// config
|
||||
AccountMeta::new_readonly(p2w_config_addr, false),
|
||||
];
|
||||
|
||||
// Insert max_batch_size metas
|
||||
acc_metas.append(&mut sym_metas_vec);
|
||||
|
||||
// Continue with other pyth2wormhole accounts
|
||||
let mut acc_metas_remainder = vec![
|
||||
// clock
|
||||
AccountMeta::new_readonly(clock::id(), false),
|
||||
// wh_prog
|
||||
AccountMeta::new_readonly(config.wh_prog, false),
|
||||
// wh_bridge
|
||||
AccountMeta::new(
|
||||
Bridge::<{ AccountState::Initialized }>::key(None, &config.wh_prog),
|
||||
false,
|
||||
),
|
||||
// wh_message
|
||||
AccountMeta::new(sym_msg_keypair.pubkey(), true),
|
||||
// wh_emitter
|
||||
AccountMeta::new_readonly(emitter_addr, false),
|
||||
// wh_sequence
|
||||
AccountMeta::new(seq_addr, false),
|
||||
// wh_fee_collector
|
||||
AccountMeta::new(FeeCollector::<'_>::key(None, &config.wh_prog), false),
|
||||
AccountMeta::new_readonly(rent::id(), false),
|
||||
];
|
||||
|
||||
acc_metas.append(&mut acc_metas_remainder);
|
||||
|
||||
let ix_data = (
|
||||
pyth2wormhole::instruction::Instruction::Attest,
|
||||
AttestData {
|
||||
consistency_level: ConsistencyLevel::Finalized,
|
||||
},
|
||||
);
|
||||
|
||||
let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
|
||||
|
||||
// Execute the transaction, obtain the resulting sequence
|
||||
// number. The and_then() calls enforce error handling
|
||||
// location near loop end.
|
||||
let res = rpc_client
|
||||
.get_latest_blockhash()
|
||||
.and_then(|latest_blockhash| {
|
||||
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
|
||||
&[ix],
|
||||
Some(&payer.pubkey()),
|
||||
&vec![&payer, &sym_msg_keypair],
|
||||
latest_blockhash,
|
||||
);
|
||||
rpc_client.send_and_confirm_transaction_with_spinner(&tx_signed)
|
||||
})
|
||||
.and_then(|sig| rpc_client.get_transaction(&sig, UiTransactionEncoding::Json))
|
||||
.map_err(|e| -> ErrBox { e.into() })
|
||||
.and_then(|this_tx| {
|
||||
this_tx
|
||||
.transaction
|
||||
.meta
|
||||
.and_then(|meta| meta.log_messages)
|
||||
.and_then(|logs| {
|
||||
let mut seqno = None;
|
||||
for log in logs {
|
||||
if log.starts_with(SEQNO_PREFIX) {
|
||||
seqno = Some(log.replace(SEQNO_PREFIX, ""));
|
||||
break;
|
||||
}
|
||||
}
|
||||
seqno
|
||||
})
|
||||
.ok_or_else(|| format!("No seqno in program logs").into())
|
||||
});
|
||||
|
||||
// Individual batch errors mustn't prevent other batches from being sent.
|
||||
match res {
|
||||
Ok(seqno) => {
|
||||
println!("Sequence number: {}", seqno);
|
||||
info!("Batch {}/{}: OK, seqno {}", batch_no, batch_count, seqno);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Batch {}/{} tx error: {}",
|
||||
batch_no,
|
||||
batch_count,
|
||||
e.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_logging(verbosity: u32) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
config::P2WConfigAccount,
|
||||
types::PriceAttestation,
|
||||
types::{PriceAttestation, batch_serialize},
|
||||
};
|
||||
use borsh::{
|
||||
BorshDeserialize,
|
||||
|
@ -28,6 +28,7 @@ use bridge::{
|
|||
};
|
||||
|
||||
use solitaire::{
|
||||
invoke_seeded,
|
||||
trace,
|
||||
AccountState,
|
||||
Derive,
|
||||
|
@ -40,7 +41,6 @@ use solitaire::{
|
|||
Peel,
|
||||
Result as SoliResult,
|
||||
Seeded,
|
||||
invoke_seeded,
|
||||
Signer,
|
||||
SolitaireError,
|
||||
Sysvar,
|
||||
|
@ -49,20 +49,66 @@ use solitaire::{
|
|||
|
||||
pub type P2WEmitter<'b> = Derive<Info<'b>, "p2w-emitter">;
|
||||
|
||||
/// Important: must be manually maintained until native Solitaire
|
||||
/// variable len vector support.
|
||||
///
|
||||
/// The number must reflect how many pyth product/price pairs are
|
||||
/// expected in the Attest struct below. The constant itself is only
|
||||
/// used in the on-chain config in order for attesters to learn the
|
||||
/// correct value dynamically.
|
||||
pub const P2W_MAX_BATCH_SIZE: u16 = 5;
|
||||
|
||||
#[derive(FromAccounts, ToInstruction)]
|
||||
pub struct Attest<'b> {
|
||||
// Payer also used for wormhole
|
||||
pub payer: Mut<Signer<Info<'b>>>,
|
||||
pub system_program: Info<'b>,
|
||||
pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
|
||||
|
||||
// Hardcoded product/price pairs, bypassing Solitaire's variable-length limitations
|
||||
// Any change to the number of accounts must include an appropriate change to P2W_MAX_BATCH_SIZE
|
||||
pub pyth_product: Info<'b>,
|
||||
pub pyth_price: Info<'b>,
|
||||
|
||||
pub pyth_product2: Option<Info<'b>>,
|
||||
pub pyth_price2: Option<Info<'b>>,
|
||||
|
||||
pub pyth_product3: Option<Info<'b>>,
|
||||
pub pyth_price3: Option<Info<'b>>,
|
||||
|
||||
pub pyth_product4: Option<Info<'b>>,
|
||||
pub pyth_price4: Option<Info<'b>>,
|
||||
|
||||
pub pyth_product5: Option<Info<'b>>,
|
||||
pub pyth_price5: Option<Info<'b>>,
|
||||
|
||||
// Did you read the comment near `pyth_product`?
|
||||
// pub pyth_product6: Option<Info<'b>>,
|
||||
// pub pyth_price6: Option<Info<'b>>,
|
||||
|
||||
// pub pyth_product7: Option<Info<'b>>,
|
||||
// pub pyth_price7: Option<Info<'b>>,
|
||||
|
||||
// pub pyth_product8: Option<Info<'b>>,
|
||||
// pub pyth_price8: Option<Info<'b>>,
|
||||
|
||||
// pub pyth_product9: Option<Info<'b>>,
|
||||
// pub pyth_price9: Option<Info<'b>>,
|
||||
|
||||
// pub pyth_product10: Option<Info<'b>>,
|
||||
// pub pyth_price10: Option<Info<'b>>,
|
||||
|
||||
pub clock: Sysvar<'b, Clock>,
|
||||
|
||||
// post_message accounts
|
||||
/// Wormhole program address
|
||||
/// Wormhole program address - must match the config value
|
||||
pub wh_prog: Info<'b>,
|
||||
|
||||
// wormhole's post_message accounts
|
||||
//
|
||||
// This contract makes no attempt to exhaustively validate
|
||||
// Wormhole's account inputs. Only the wormhole contract address
|
||||
// is validated (see above).
|
||||
|
||||
/// Bridge config needed for fee calculation
|
||||
pub wh_bridge: Mut<Info<'b>>,
|
||||
|
||||
|
@ -85,7 +131,6 @@ pub struct Attest<'b> {
|
|||
|
||||
#[derive(BorshDeserialize, BorshSerialize)]
|
||||
pub struct AttestData {
|
||||
pub nonce: u32,
|
||||
pub consistency_level: ConsistencyLevel,
|
||||
}
|
||||
|
||||
|
@ -98,16 +143,6 @@ impl<'b> InstructionContext<'b> for Attest<'b> {
|
|||
pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> {
|
||||
accs.config.verify_derivation(ctx.program_id, None)?;
|
||||
|
||||
if accs.config.pyth_owner != *accs.pyth_price.owner
|
||||
|| accs.config.pyth_owner != *accs.pyth_product.owner
|
||||
{
|
||||
trace!(&format!(
|
||||
"pyth_owner pubkey mismatch (expected {:?}, got price owner {:?} and product owner {:?}",
|
||||
accs.config.pyth_owner, accs.pyth_price.owner, accs.pyth_product.owner
|
||||
));
|
||||
return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into());
|
||||
}
|
||||
|
||||
if accs.config.wh_prog != *accs.wh_prog.key {
|
||||
trace!(&format!(
|
||||
"Wormhole program account mismatch (expected {:?}, got {:?})",
|
||||
|
@ -115,20 +150,91 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
|
|||
));
|
||||
}
|
||||
|
||||
let price_attestation = PriceAttestation::from_pyth_price_bytes(
|
||||
accs.pyth_price.key.clone(),
|
||||
accs.clock.unix_timestamp,
|
||||
&*accs.pyth_price.try_borrow_data()?,
|
||||
)?;
|
||||
// Make the specified prices iterable
|
||||
let price_pair_opts = [
|
||||
Some(&accs.pyth_product),
|
||||
Some(&accs.pyth_price),
|
||||
accs.pyth_product2.as_ref(),
|
||||
accs.pyth_price2.as_ref(),
|
||||
accs.pyth_product3.as_ref(),
|
||||
accs.pyth_price3.as_ref(),
|
||||
accs.pyth_product4.as_ref(),
|
||||
accs.pyth_price4.as_ref(),
|
||||
accs.pyth_product5.as_ref(),
|
||||
accs.pyth_price5.as_ref(),
|
||||
|
||||
if &price_attestation.product_id != accs.pyth_product.key {
|
||||
// Did you read the comment near `pyth_product`?
|
||||
// accs.pyth_product6.as_ref(),
|
||||
// accs.pyth_price6.as_ref(),
|
||||
// accs.pyth_product7.as_ref(),
|
||||
// accs.pyth_price7.as_ref(),
|
||||
// accs.pyth_product8.as_ref(),
|
||||
// accs.pyth_price8.as_ref(),
|
||||
// accs.pyth_product9.as_ref(),
|
||||
// accs.pyth_price9.as_ref(),
|
||||
// accs.pyth_product10.as_ref(),
|
||||
// accs.pyth_price10.as_ref(),
|
||||
];
|
||||
|
||||
let price_pairs: Vec<_> = price_pair_opts.into_iter().filter_map(|acc| *acc).collect();
|
||||
|
||||
if price_pairs.len() % 2 != 0 {
|
||||
trace!(&format!(
|
||||
"Price's product_id does not match the pased account (points at {:?} instead)",
|
||||
price_attestation.product_id
|
||||
"Uneven product/price count detected: {}",
|
||||
price_pairs.len()
|
||||
));
|
||||
return Err(ProgramError::InvalidAccountData.into());
|
||||
}
|
||||
|
||||
trace!("{} Pyth symbols received", price_pairs.len() / 2);
|
||||
|
||||
// Collect the validated symbols for batch serialization
|
||||
let mut attestations = Vec::with_capacity(price_pairs.len() / 2);
|
||||
|
||||
for pair in price_pairs.as_slice().chunks_exact(2) {
|
||||
|
||||
let product = pair[0];
|
||||
let price = pair[1];
|
||||
|
||||
if accs.config.pyth_owner != *price.owner
|
||||
|| accs.config.pyth_owner != *product.owner
|
||||
{
|
||||
trace!(&format!(
|
||||
"Pair {:?} - {:?}: pyth_owner pubkey mismatch (expected {:?}, got product owner {:?} and price owner {:?}",
|
||||
|
||||
product, price,
|
||||
accs.config.pyth_owner, product.owner, price.owner
|
||||
));
|
||||
return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into());
|
||||
}
|
||||
|
||||
let attestation = PriceAttestation::from_pyth_price_bytes(
|
||||
price.key.clone(),
|
||||
accs.clock.unix_timestamp,
|
||||
&*price.try_borrow_data()?,
|
||||
)?;
|
||||
|
||||
// The following check is crucial against poorly ordered
|
||||
// account inputs, e.g. [Some(prod1), Some(price1),
|
||||
// Some(prod2), None, None, Some(price)], interpreted by
|
||||
// earlier logic as [(prod1, price1), (prod2, price3)].
|
||||
//
|
||||
// Failing to verify the product/price relationship could lead
|
||||
// to mismatched product/price metadata, which would result in
|
||||
// a false attestation.
|
||||
if &attestation.product_id != product.key {
|
||||
trace!(&format!(
|
||||
"Price's product_id does not match the pased account (points at {:?} instead)",
|
||||
attestation.product_id
|
||||
));
|
||||
return Err(ProgramError::InvalidAccountData.into());
|
||||
}
|
||||
|
||||
attestations.push(attestation);
|
||||
}
|
||||
|
||||
trace!("Attestations successfully created");
|
||||
|
||||
let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config;
|
||||
|
||||
// Pay wormhole fee
|
||||
|
@ -143,9 +249,12 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
|
|||
let post_message_data = (
|
||||
bridge::instruction::Instruction::PostMessage,
|
||||
PostMessageData {
|
||||
nonce: data.nonce,
|
||||
payload: price_attestation.serialize(),
|
||||
consistency_level: data.consistency_level,
|
||||
nonce: 0, // Superseded by the sequence number
|
||||
payload: batch_serialize(attestations.as_slice().iter()).map_err(|e| {
|
||||
trace!(e.to_string());
|
||||
ProgramError::InvalidAccountData
|
||||
})?,
|
||||
consistency_level: data.consistency_level,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
//! On-chain state for the pyth2wormhole SOL contract.
|
||||
//!
|
||||
//! Important: A config init/update should be performed on every
|
||||
//! deployment/upgrade of this Solana program. Doing so prevents
|
||||
//! problems related to max batch size mismatches between config and
|
||||
//! contract logic. See attest.rs for details.
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use solitaire::{processors::seeded::AccountOwner, AccountState, Data, Derive, Owned};
|
||||
|
@ -10,6 +17,12 @@ pub struct Pyth2WormholeConfig {
|
|||
pub wh_prog: Pubkey,
|
||||
/// Authority owning Pyth price data
|
||||
pub pyth_owner: Pubkey,
|
||||
/// How many product/price pairs can be sent and attested at once
|
||||
///
|
||||
/// Important: Whenever the corresponding logic in attest.rs
|
||||
/// changes its expected number of symbols per batch, this config
|
||||
/// must be updated accordingly on-chain.
|
||||
pub max_batch_size: u16,
|
||||
}
|
||||
|
||||
impl Owned for Pyth2WormholeConfig {
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
//! Constants and values common to every p2w custom-serialized message.
|
||||
//!
|
||||
//! The format makes no attempt to provide human-readable symbol names
|
||||
//! in favor of explicit product/price Solana account addresses
|
||||
//! (IDs). This choice was made to disambiguate any symbols with
|
||||
//! similar human-readable names and provide a failsafe for some of
|
||||
//! the probable adversarial scenarios.
|
||||
|
||||
pub mod pyth_extensions;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
convert::{
|
||||
TryFrom,
|
||||
TryInto,
|
||||
},
|
||||
io::Read,
|
||||
iter::Iterator,
|
||||
mem,
|
||||
};
|
||||
|
||||
|
@ -37,26 +47,34 @@ use self::pyth_extensions::{
|
|||
P2WPriceType,
|
||||
};
|
||||
|
||||
// Constants and values common to every p2w custom-serialized message
|
||||
|
||||
/// Precedes every message implementing the p2w serialization format
|
||||
pub const P2W_MAGIC: &'static [u8] = b"P2WH";
|
||||
|
||||
/// Format version used and understood by this codebase
|
||||
pub const P2W_FORMAT_VERSION: u16 = 1;
|
||||
pub const P2W_FORMAT_VERSION: u16 = 2;
|
||||
|
||||
pub const PUBKEY_LEN: usize = 32;
|
||||
|
||||
/// Decides the format of following bytes
|
||||
#[repr(u8)]
|
||||
pub enum PayloadId {
|
||||
PriceAttestation = 1,
|
||||
PriceAttestation = 1, // Not in use, currently batch attestations imply PriceAttestation messages inside
|
||||
PriceBatchAttestation,
|
||||
}
|
||||
|
||||
// On-chain data types
|
||||
|
||||
/// The main attestation data type.
|
||||
///
|
||||
/// Important: For maximum security, *both* product_id and price_id
|
||||
/// should be used as storage keys for known attestations in target
|
||||
/// chain logic.
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))]
|
||||
#[cfg_attr(
|
||||
feature = "wasm",
|
||||
derive(serde_derive::Serialize, serde_derive::Deserialize)
|
||||
)]
|
||||
pub struct PriceAttestation {
|
||||
pub product_id: Pubkey,
|
||||
pub price_id: Pubkey,
|
||||
|
@ -71,6 +89,123 @@ pub struct PriceAttestation {
|
|||
pub timestamp: UnixTimestamp,
|
||||
}
|
||||
|
||||
/// Turn a bunch of attestations into a combined payload.
|
||||
///
|
||||
/// Batches assume constant-size attestations within a single batch.
|
||||
pub fn batch_serialize(
|
||||
attestations: impl Iterator<Item = impl Borrow<PriceAttestation>>,
|
||||
) -> Result<Vec<u8>, ErrBox> {
|
||||
// magic
|
||||
let mut buf = P2W_MAGIC.to_vec();
|
||||
|
||||
// version
|
||||
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
|
||||
|
||||
// payload_id
|
||||
buf.push(PayloadId::PriceBatchAttestation as u8);
|
||||
|
||||
let collected: Vec<_> = attestations.collect();
|
||||
|
||||
// n_attestations
|
||||
buf.extend_from_slice(&(collected.len() as u16).to_be_bytes()[..]);
|
||||
|
||||
let mut attestation_size = 0; // Will be determined as we serialize attestations
|
||||
let mut serialized_attestations = Vec::with_capacity(collected.len());
|
||||
for (idx, a) in collected.iter().enumerate() {
|
||||
// Learn the current attestation's size
|
||||
let serialized = PriceAttestation::serialize(a.borrow());
|
||||
let a_len = serialized.len();
|
||||
|
||||
// Verify it's the same as the first one we saw for the batch, assign if we're first.
|
||||
if attestation_size > 0 {
|
||||
if a_len != attestation_size {
|
||||
return Err(format!(
|
||||
"attestation {} serializes to {} bytes, {} expected",
|
||||
idx + 1,
|
||||
a_len,
|
||||
attestation_size
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
attestation_size = a_len;
|
||||
}
|
||||
|
||||
serialized_attestations.push(serialized);
|
||||
}
|
||||
|
||||
// attestation_size
|
||||
buf.extend_from_slice(&(attestation_size as u16).to_be_bytes()[..]);
|
||||
|
||||
for mut s in serialized_attestations.into_iter() {
|
||||
buf.append(&mut s)
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Undo `batch_serialize`
|
||||
pub fn batch_deserialize(mut bytes: impl Read) -> Result<Vec<PriceAttestation>, ErrBox> {
|
||||
let mut magic_vec = vec![0u8; P2W_MAGIC.len()];
|
||||
bytes.read_exact(magic_vec.as_mut_slice())?;
|
||||
|
||||
if magic_vec.as_slice() != P2W_MAGIC {
|
||||
return Err(format!(
|
||||
"Invalid magic {:02X?}, expected {:02X?}",
|
||||
magic_vec, P2W_MAGIC,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
|
||||
bytes.read_exact(version_vec.as_mut_slice())?;
|
||||
let version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
|
||||
|
||||
if version != P2W_FORMAT_VERSION {
|
||||
return Err(format!(
|
||||
"Unsupported format version {}, expected {}",
|
||||
version, P2W_FORMAT_VERSION
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
|
||||
bytes.read_exact(payload_id_vec.as_mut_slice())?;
|
||||
|
||||
if payload_id_vec[0] != PayloadId::PriceBatchAttestation as u8 {
|
||||
return Err(format!(
|
||||
"Invalid Payload ID {}, expected {}",
|
||||
payload_id_vec[0],
|
||||
PayloadId::PriceBatchAttestation as u8,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut batch_len_vec = vec![0u8; 2];
|
||||
bytes.read_exact(batch_len_vec.as_mut_slice())?;
|
||||
let batch_len = u16::from_be_bytes(batch_len_vec.as_slice().try_into()?);
|
||||
|
||||
let mut attestation_size_vec = vec![0u8; 2];
|
||||
bytes.read_exact(attestation_size_vec.as_mut_slice())?;
|
||||
let attestation_size = u16::from_be_bytes(attestation_size_vec.as_slice().try_into()?);
|
||||
|
||||
let mut ret = Vec::with_capacity(batch_len as usize);
|
||||
|
||||
for i in 0..batch_len {
|
||||
let mut attestation_buf = vec![0u8; attestation_size as usize];
|
||||
bytes.read_exact(attestation_buf.as_mut_slice())?;
|
||||
|
||||
dbg!(&attestation_buf.len());
|
||||
|
||||
match PriceAttestation::deserialize(attestation_buf.as_slice()) {
|
||||
Ok(attestation) => ret.push(attestation),
|
||||
Err(e) => return Err(format!("PriceAttestation {}/{}: {}", i + 1, batch_len, e).into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl PriceAttestation {
|
||||
pub fn from_pyth_price_bytes(
|
||||
price_id: Pubkey,
|
||||
|
@ -161,7 +296,6 @@ impl PriceAttestation {
|
|||
use P2WPriceStatus::*;
|
||||
use P2WPriceType::*;
|
||||
|
||||
println!("Using {} bytes for magic", P2W_MAGIC.len());
|
||||
let mut magic_vec = vec![0u8; P2W_MAGIC.len()];
|
||||
|
||||
bytes.read_exact(magic_vec.as_mut_slice())?;
|
||||
|
@ -176,7 +310,7 @@ impl PriceAttestation {
|
|||
|
||||
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
|
||||
bytes.read_exact(version_vec.as_mut_slice())?;
|
||||
let mut version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
|
||||
let version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
|
||||
|
||||
if version != P2W_FORMAT_VERSION {
|
||||
return Err(format!(
|
||||
|
@ -217,20 +351,21 @@ impl PriceAttestation {
|
|||
};
|
||||
|
||||
let mut price_vec = vec![0u8; mem::size_of::<i64>()];
|
||||
bytes.read_exact(price_vec.as_mut_slice())?;
|
||||
let price = i64::from_be_bytes(price_vec.as_slice().try_into()?);
|
||||
bytes.read_exact(price_vec.as_mut_slice())?;
|
||||
let price = i64::from_be_bytes(price_vec.as_slice().try_into()?);
|
||||
|
||||
let mut expo_vec = vec![0u8; mem::size_of::<i32>()];
|
||||
bytes.read_exact(expo_vec.as_mut_slice())?;
|
||||
let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?);
|
||||
bytes.read_exact(expo_vec.as_mut_slice())?;
|
||||
let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?);
|
||||
|
||||
let twap = P2WEma::deserialize(&mut bytes)?;
|
||||
let twac = P2WEma::deserialize(&mut bytes)?;
|
||||
let twap = P2WEma::deserialize(&mut bytes)?;
|
||||
let twac = P2WEma::deserialize(&mut bytes)?;
|
||||
|
||||
println!("twac OK");
|
||||
println!("twac OK");
|
||||
let mut confidence_interval_vec = vec![0u8; mem::size_of::<u64>()];
|
||||
bytes.read_exact(confidence_interval_vec.as_mut_slice())?;
|
||||
let confidence_interval = u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?);
|
||||
bytes.read_exact(confidence_interval_vec.as_mut_slice())?;
|
||||
let confidence_interval =
|
||||
u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?);
|
||||
|
||||
let mut status_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
|
||||
bytes.read_exact(status_vec.as_mut_slice())?;
|
||||
|
@ -244,7 +379,6 @@ impl PriceAttestation {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
let mut corp_act_vec = vec![0u8; mem::size_of::<P2WPriceType>()];
|
||||
bytes.read_exact(corp_act_vec.as_mut_slice())?;
|
||||
let corp_act = match corp_act_vec[0] {
|
||||
|
@ -254,23 +388,23 @@ impl PriceAttestation {
|
|||
}
|
||||
};
|
||||
|
||||
let mut timestamp_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
|
||||
bytes.read_exact(timestamp_vec.as_mut_slice())?;
|
||||
let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?);
|
||||
let mut timestamp_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
|
||||
bytes.read_exact(timestamp_vec.as_mut_slice())?;
|
||||
let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?);
|
||||
|
||||
Ok( Self {
|
||||
product_id,
|
||||
price_id,
|
||||
price_type,
|
||||
price,
|
||||
expo,
|
||||
twap,
|
||||
twac,
|
||||
confidence_interval,
|
||||
status,
|
||||
corp_act,
|
||||
timestamp
|
||||
})
|
||||
Ok(Self {
|
||||
product_id,
|
||||
price_id,
|
||||
price_type,
|
||||
price,
|
||||
expo,
|
||||
twap,
|
||||
twac,
|
||||
confidence_interval,
|
||||
status,
|
||||
corp_act,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,38 +539,10 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pyth_price_wrong_size_slices() {
|
||||
assert!(parse_pyth_price(&[]).is_err());
|
||||
assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_values() -> SoliResult<()> {
|
||||
let price = Price {
|
||||
expo: 5,
|
||||
agg: PriceInfo {
|
||||
price: 42,
|
||||
..empty_priceinfo!()
|
||||
},
|
||||
..empty_price!()
|
||||
};
|
||||
let price_vec = vec![price];
|
||||
|
||||
// use the C repr to mock pyth's format
|
||||
let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::<u8>() };
|
||||
|
||||
parse_pyth_price(bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize() -> Result<(), ErrBox> {
|
||||
let product_id_bytes = [21u8; 32];
|
||||
let price_id_bytes = [222u8; 32];
|
||||
println!("Hex product_id: {:02X?}", &product_id_bytes);
|
||||
println!("Hex price_id: {:02X?}", &price_id_bytes);
|
||||
let attestation: PriceAttestation = PriceAttestation {
|
||||
fn mock_attestation(prod: Option<[u8; 32]>, price: Option<[u8; 32]>) -> PriceAttestation {
|
||||
let product_id_bytes = prod.unwrap_or([21u8; 32]);
|
||||
let price_id_bytes = prod.unwrap_or([222u8; 32]);
|
||||
PriceAttestation {
|
||||
product_id: Pubkey::new_from_array(product_id_bytes),
|
||||
price_id: Pubkey::new_from_array(price_id_bytes),
|
||||
price: (0xdeadbeefdeadbabe as u64) as i64,
|
||||
|
@ -456,14 +562,93 @@ mod tests {
|
|||
confidence_interval: 101,
|
||||
corp_act: P2WCorpAction::NoCorpAct,
|
||||
timestamp: 123456789i64,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pyth_price_wrong_size_slices() {
|
||||
assert!(parse_pyth_price(&[]).is_err());
|
||||
assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pyth_price() -> SoliResult<()> {
|
||||
let price = Price {
|
||||
expo: 5,
|
||||
agg: PriceInfo {
|
||||
price: 42,
|
||||
..empty_priceinfo!()
|
||||
},
|
||||
..empty_price!()
|
||||
};
|
||||
let price_vec = vec![price];
|
||||
|
||||
// use the C repr to mock pyth's format
|
||||
let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::<u8>() };
|
||||
|
||||
parse_pyth_price(bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_serde() -> Result<(), ErrBox> {
|
||||
let product_id_bytes = [21u8; 32];
|
||||
let price_id_bytes = [222u8; 32];
|
||||
let attestation: PriceAttestation =
|
||||
mock_attestation(Some(product_id_bytes), Some(price_id_bytes));
|
||||
|
||||
println!("Hex product_id: {:02X?}", &product_id_bytes);
|
||||
println!("Hex price_id: {:02X?}", &price_id_bytes);
|
||||
|
||||
println!("Regular: {:#?}", &attestation);
|
||||
println!("Hex: {:#02X?}", &attestation);
|
||||
let bytes = attestation.serialize();
|
||||
let bytes = attestation.serialize();
|
||||
println!("Hex Bytes: {:02X?}", bytes);
|
||||
|
||||
assert_eq!(PriceAttestation::deserialize(bytes.as_slice())?, attestation);
|
||||
assert_eq!(
|
||||
PriceAttestation::deserialize(bytes.as_slice())?,
|
||||
attestation
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_serde_wrong_size() -> Result<(), ErrBox> {
|
||||
assert!(PriceAttestation::deserialize(&[][..]).is_err());
|
||||
assert!(PriceAttestation::deserialize(vec![0u8; 1].as_slice()).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_serde() -> Result<(), ErrBox> {
|
||||
let attestations: Vec<_> = (0..65535)
|
||||
.map(|i| mock_attestation(Some([(i % 256) as u8; 32]), None))
|
||||
.collect();
|
||||
|
||||
let serialized = batch_serialize(attestations.iter())?;
|
||||
|
||||
let deserialized = batch_deserialize(serialized.as_slice())?;
|
||||
|
||||
assert_eq!(attestations, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_serde_wrong_size() -> Result<(), ErrBox> {
|
||||
assert!(batch_deserialize(&[][..]).is_err());
|
||||
assert!(batch_deserialize(vec![0u8; 1].as_slice()).is_err());
|
||||
|
||||
let attestations: Vec<_> = (0..20)
|
||||
.map(|i| mock_attestation(Some([(i % 256) as u8; 32]), None))
|
||||
.collect();
|
||||
|
||||
let serialized = batch_serialize(attestations.iter())?;
|
||||
|
||||
// Missing last byte in last attestation must be an error
|
||||
let len = serialized.len();
|
||||
assert!(batch_deserialize(&serialized.as_slice()[..len - 1]).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,7 @@ use wasm_bindgen::prelude::*;
|
|||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{attest::P2WEmitter, types::PriceAttestation};
|
||||
|
||||
/// sanity check for wasm compilation, TODO(sdrozd): remove after
|
||||
/// meaningful endpoints are added
|
||||
#[wasm_bindgen]
|
||||
pub fn hello_p2w() -> String {
|
||||
"Ciao mondo!".to_owned()
|
||||
}
|
||||
use crate::{attest::P2WEmitter, types};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_emitter_address(program_id: String) -> Vec<u8> {
|
||||
|
@ -23,7 +16,15 @@ pub fn get_emitter_address(program_id: String) -> Vec<u8> {
|
|||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_attestation(bytes: Vec<u8>) -> JsValue {
|
||||
let a = PriceAttestation::deserialize(bytes.as_slice()).unwrap();
|
||||
let a = types::PriceAttestation::deserialize(bytes.as_slice()).unwrap();
|
||||
|
||||
JsValue::from_serde(&a).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_batch_attestation(bytes: Vec<u8>) -> JsValue {
|
||||
let a = types::batch_deserialize(bytes.as_slice()).unwrap();
|
||||
|
||||
|
||||
JsValue::from_serde(&a).unwrap()
|
||||
}
|
||||
|
|
|
@ -66,6 +66,9 @@ pub enum AccEntry {
|
|||
Derived(Pubkey),
|
||||
/// Key derived from constants and/or program address, read-only.
|
||||
DerivedRO(Pubkey),
|
||||
|
||||
/// Empty value for nullables
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// Types implementing Wrap are those that can be turned into a
|
||||
|
@ -85,6 +88,15 @@ pub trait Wrap {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Wrap> Wrap for Option<T> {
|
||||
fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
|
||||
match a {
|
||||
AccEntry::Empty => Ok(vec![AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false)]),
|
||||
other => T::wrap(other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a, T> Wrap for Signer<T>
|
||||
where
|
||||
T: Keyed<'a, 'b>,
|
||||
|
|
|
@ -17,6 +17,7 @@ use solana_program::{
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{
|
||||
trace,
|
||||
processors::seeded::{
|
||||
AccountOwner,
|
||||
Owned,
|
||||
|
@ -41,6 +42,32 @@ pub trait Peel<'a, 'b: 'a, 'c> {
|
|||
fn persist(&self, program_id: &Pubkey) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Peel a nullable value (0-account means None)
|
||||
impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for Option<T> {
|
||||
fn peel<I>(ctx: &'c mut Context<'a, 'b, 'c, I>) -> Result<Self> {
|
||||
// Check for 0-account
|
||||
if ctx.info().key == &Pubkey::new_from_array([0u8; 32]) {
|
||||
trace!(&format!("Peeled {} is None, returning", std::any::type_name::<Option<T>>()));
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(T::peel(ctx)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn deps() -> Vec<Pubkey> {
|
||||
T::deps()
|
||||
}
|
||||
|
||||
fn persist(&self, program_id: &Pubkey) -> Result<()> {
|
||||
if let Some(s) = self.as_ref() {
|
||||
T::persist(s, program_id)
|
||||
} else {
|
||||
trace!(&format!("Peeled {} is None, not persisting", std::any::type_name::<Option<T>>()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Peel a Derived Key
|
||||
impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>, const Seed: &'static str> Peel<'a, 'b, 'c>
|
||||
for Derive<T, Seed>
|
||||
|
|
|
@ -16,3 +16,4 @@ ENV P2W_OWNER_KEYPAIR="/usr/src/solana/keys/p2w_owner.json"
|
|||
ENV P2W_ATTESTATIONS_PORT="4343"
|
||||
ENV PYTH_PUBLISHER_KEYPAIR="/usr/src/solana/keys/pyth_publisher.json"
|
||||
ENV PYTH_PROGRAM_KEYPAIR="/usr/src/solana/keys/pyth_program.json"
|
||||
ENV SOL_AIRDROP_AMT="100"
|
||||
|
|
|
@ -44,3 +44,4 @@ USER pyth
|
|||
|
||||
ENV PYTH=$PYTH_SRC_ROOT/build/pyth
|
||||
ENV READINESS_PORT=2000
|
||||
ENV SOL_AIRDROP_AMT=100
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# ethereum contracts
|
||||
/contracts
|
||||
/src/*-contracts/
|
||||
|
||||
# tsproto output
|
||||
/src/proto
|
||||
|
||||
# build
|
||||
/lib
|
|
@ -0,0 +1,40 @@
|
|||
FROM node:16-alpine@sha256:004dbac84fed48e20f9888a23e32fa7cf83c2995e174a78d41d9a9dd1e051a20
|
||||
|
||||
# npm needs a Python for some of the deps
|
||||
RUN apk add git python3 make build-base
|
||||
|
||||
# Build ETH
|
||||
WORKDIR /usr/src/ethereum
|
||||
ADD ethereum .
|
||||
RUN --mount=type=cache,target=/home/node/.npm \
|
||||
npm ci
|
||||
|
||||
# Build Wormhole SDK
|
||||
WORKDIR /usr/src/sdk/js
|
||||
ADD sdk/js/ .
|
||||
RUN --mount=type=cache,target=/home/node/.npm \
|
||||
npm ci && npm run build
|
||||
|
||||
# Build p2w-sdk in dir preserving directory structure
|
||||
WORKDIR /usr/src/third_party/pyth/p2w-sdk
|
||||
COPY third_party/pyth/p2w-sdk/package.json third_party/pyth/p2w-sdk/package-lock.json .
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
COPY third_party/pyth/p2w-sdk .
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm run build
|
||||
|
||||
# Build p2w-relay
|
||||
WORKDIR /usr/src/third_party/pyth/p2w-relay
|
||||
COPY third_party/pyth/p2w-relay/package.json third_party/pyth/p2w-relay/package-lock.json .
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
COPY third_party/pyth/p2w-relay .
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm run build
|
|
@ -0,0 +1,24 @@
|
|||
# Pyth2wormhole relay example
|
||||
IMPORTANT: This is not ready for production.
|
||||
|
||||
This package is an example Pyth2wormhole relayer implementation. The
|
||||
main focus is to provide an automated integration test that will
|
||||
perform last-mile delivery of Pyth2wormhole price attestations.
|
||||
|
||||
# How it works
|
||||
## Relayer recap
|
||||
When attesting with Wormhole, the final step consists of a query for
|
||||
the guardian-signed attestation data on the guardian public RPC,
|
||||
followed by posting the data to each desired target chain
|
||||
contract. Each target chain contract lets callers verify the payload's
|
||||
signatures, thus proving its validity. This activity means being
|
||||
a Wormhole **relayer**.
|
||||
|
||||
## How this package relays attestations
|
||||
`p2w-relay` is a Node.js relayer script targeting ETH that will
|
||||
periodically query its source-chain counterpart for new sequence
|
||||
numbers to query from the guardians. Any pending sequence numbers will
|
||||
stick around in a global state until their corresponding messages are
|
||||
successfully retrieved from the guardians. Later, target chain calls
|
||||
are made and a given seqno is deleted from the pool. Failed target
|
||||
chain calls will not be retried.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "@certusone/p2w-relay",
|
||||
"version": "0.1.0",
|
||||
"description": "p2w-sdk integration test; not intended for production use",
|
||||
"private": true,
|
||||
"types": "lib/index.d.ts",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node -r esm lib/index.js",
|
||||
"build": "npm run build-eth-types && npm run build-lib",
|
||||
"build-lib": "npm run copy-artifacts && tsc",
|
||||
"build-watch": "npm run build-eth-types && npm run copy-artifacts && tsc --watch",
|
||||
"build-eth-types": "node scripts/copyEthContracts.cjs && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
||||
"copy-artifacts": "node scripts/copyWasm.cjs && node scripts/copyEthersTypes.cjs",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"postversion": "git push && git push --tags",
|
||||
"preversion": "npm run lint",
|
||||
"version": "npm run format && git add -A src"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/certusone/wormhole.git"
|
||||
},
|
||||
"author": "https://certus.one",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@openzeppelin/contracts": "^4.2.0",
|
||||
"@typechain/ethers-v5": "^7.1.2",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^16.6.1",
|
||||
"copy-dir": "^1.3.0",
|
||||
"esm": "^3.2.25",
|
||||
"ethers": "^5.4.7",
|
||||
"find": "^0.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certusone/p2w-sdk": "file:../p2w-sdk",
|
||||
"@certusone/wormhole-sdk": "file:../../../sdk/js",
|
||||
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/certusone/wormhole/issues"
|
||||
},
|
||||
"homepage": "https://github.com/certusone/wormhole#readme"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
const copydir = require("copy-dir");
|
||||
copydir.sync("../../../ethereum/build/contracts", "./contracts");
|
|
@ -0,0 +1,17 @@
|
|||
const find = require("find");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const SOURCE_ROOT = "src";
|
||||
const TARGET_ROOT = "lib";
|
||||
|
||||
find.eachfile(/\.d\.ts(\..*)?/, SOURCE_ROOT, fname => {
|
||||
|
||||
fname_copy = fname.replace(SOURCE_ROOT, TARGET_ROOT);
|
||||
|
||||
console.log("copying types:", fname, "to", fname_copy);
|
||||
|
||||
fs.mkdirSync(path.dirname(fname_copy), {recursive: true});
|
||||
|
||||
fs.copyFileSync(fname, fname_copy);
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
const find = require("find");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const SOURCE_ROOT = "src";
|
||||
const TARGET_ROOT = "lib";
|
||||
|
||||
find.eachfile(/\.wasm(\..*)?/, SOURCE_ROOT, fname => {
|
||||
|
||||
fname_copy = fname.replace(SOURCE_ROOT, TARGET_ROOT);
|
||||
|
||||
console.log("copyWasm:", fname, "to", fname_copy);
|
||||
|
||||
fs.mkdirSync(path.dirname(fname_copy), {recursive: true});
|
||||
|
||||
fs.copyFileSync(fname, fname_copy);
|
||||
});
|
|
@ -0,0 +1,198 @@
|
|||
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
|
||||
import {PythImplementation__factory} from "./ethers-contracts";
|
||||
|
||||
import * as http from "http";
|
||||
import * as net from "net";
|
||||
import fs from "fs";
|
||||
|
||||
|
||||
import {ethers} from "ethers";
|
||||
|
||||
import {getSignedAttestation, parseBatchAttestation, p2w_core, sol_addr2buf} from "@certusone/p2w-sdk";
|
||||
|
||||
import {setDefaultWasm, importCoreWasm} from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
|
||||
|
||||
interface NewAttestationsResponse {
|
||||
pendingSeqnos: Array<number>,
|
||||
}
|
||||
|
||||
|
||||
async function readinessProbeRoutine(port: number) {
|
||||
let srv = net.createServer();
|
||||
|
||||
return await srv.listen(port);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
||||
// p2w-attest exposes an HTTP endpoint that shares the currently pending sequence numbers
|
||||
const P2W_ATTESTATIONS_HOST = process.env.P2W_ATTESTATIONS_HOST || "p2w-attest";
|
||||
const P2W_ATTESTATIONS_PORT = Number(process.env.P2W_ATTESTATIONS_PORT || "4343");
|
||||
const P2W_ATTESTATIONS_POLL_INTERVAL_MS = Number(process.env.P2W_ATTESTATIONS_POLL_INTERVAL_MS || "5000");
|
||||
|
||||
const P2W_SOL_ADDRESS = process.env.P2W_SOL_ADDRESS || "P2WH424242424242424242424242424242424242424";
|
||||
|
||||
const READINESS_PROBE_PORT = Number(process.env.READINESS_PROBE_PORT || "2000");
|
||||
|
||||
const P2W_RELAY_RETRY_COUNT = Number(process.env.P2W_RELAY_RETRY_COUNT || "3");
|
||||
|
||||
// ETH node connection details; Currently, we expect to read BIP44
|
||||
// wallet recovery mnemonics from a text file.
|
||||
const ETH_NODE_URL = process.env.ETH_NODE_URL || "ws://eth-devnet:8545";
|
||||
const ETH_P2W_CONTRACT = process.env.ETH_P2W_CONTRACT || "0xA94B7f0465E98609391C623d0560C5720a3f2D33";
|
||||
const ETH_MNEMONIC_FILE = process.env.ETH_MNEMONIC_FILE || "../../../ethereum/devnet_mnemonic.txt";
|
||||
const ETH_HD_WALLET_PATH = process.env.ETH_HD_WALLET_PATH || "m/44'/60'/0'/0/0";
|
||||
|
||||
// Public RPC address for use with signed attestation queries
|
||||
const GUARDIAN_RPC_HOST_PORT = process.env.GUARDIAN_RPC_HOST_PORT || "http://guardian:7071";
|
||||
|
||||
let readinessProbe = null;
|
||||
|
||||
let seqnoPool: Map<number, number> = new Map();
|
||||
|
||||
console.log(`Polling attestations endpoint every ${P2W_ATTESTATIONS_POLL_INTERVAL_MS / 1000} seconds`);
|
||||
|
||||
setDefaultWasm("node");
|
||||
const {parse_vaa} = await importCoreWasm();
|
||||
|
||||
let p2w_eth: any;
|
||||
|
||||
// Connect to ETH
|
||||
try {
|
||||
let provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
|
||||
let mnemonic: string = fs.readFileSync(ETH_MNEMONIC_FILE).toString("utf-8").trim();
|
||||
let wallet = ethers.Wallet.fromMnemonic(mnemonic, ETH_HD_WALLET_PATH);
|
||||
console.log(`Using ETH wallet pubkey: ${wallet.publicKey}`);
|
||||
let signer = new ethers.Wallet(wallet.privateKey, provider);
|
||||
let balance = await signer.getBalance();
|
||||
console.log(`Account balance is ${balance}`);
|
||||
let factory = new PythImplementation__factory(signer);
|
||||
p2w_eth = factory.attach(ETH_P2W_CONTRACT);
|
||||
}
|
||||
catch(e) {
|
||||
console.error(`Error: Could not instantiate ETH contract:`, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
http.get({
|
||||
hostname: P2W_ATTESTATIONS_HOST,
|
||||
port: P2W_ATTESTATIONS_PORT,
|
||||
path: "/",
|
||||
agent: false
|
||||
}, (res) => {
|
||||
if (res.statusCode != 200) {
|
||||
console.error("Could not reach attestations endpoint", res);
|
||||
} else {
|
||||
let chunks: string[] = [];
|
||||
res.setEncoding("utf-8");
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
let body = chunks.join('');
|
||||
|
||||
let response: NewAttestationsResponse = JSON.parse(body);
|
||||
|
||||
console.log(`Got ${response.pendingSeqnos.length} new seqnos: ${response.pendingSeqnos}`);
|
||||
|
||||
for (let seqno of response.pendingSeqnos) {
|
||||
seqnoPool.set(seqno, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('error', (e) => {
|
||||
console.error(`Got error: ${e.message}`);
|
||||
});
|
||||
|
||||
console.log("Processing seqnos:", seqnoPool);
|
||||
for (let poolEntry of seqnoPool) {
|
||||
|
||||
let seqno = poolEntry[0];
|
||||
let attempts = poolEntry[1];
|
||||
|
||||
if (attempts >= P2W_RELAY_RETRY_COUNT) {
|
||||
console.warn(`[seqno ${poolEntry}] Exceeded retry count, removing from list`);
|
||||
seqnoPool.delete(seqno);
|
||||
continue;
|
||||
}
|
||||
|
||||
let vaaResponse: any;
|
||||
try {
|
||||
vaaResponse = await getSignedAttestation(
|
||||
GUARDIAN_RPC_HOST_PORT,
|
||||
P2W_SOL_ADDRESS,
|
||||
seqno,
|
||||
{
|
||||
transport: NodeHttpTransport()
|
||||
}
|
||||
);
|
||||
}
|
||||
catch(e) {
|
||||
console.error(`[seqno ${poolEntry}] Error: Could not call getSignedAttestation:`, e);
|
||||
|
||||
seqnoPool.set(seqno, attempts + 1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[seqno ${poolEntry}] Price attestation VAA bytes:\n`, vaaResponse.vaaBytes);
|
||||
|
||||
let parsedVaa = parse_vaa(vaaResponse.vaaBytes);
|
||||
|
||||
console.log(`[seqno ${poolEntry}] Parsed VAA:\n`, parsedVaa);
|
||||
|
||||
let parsedAttestations = await parseBatchAttestation(parsedVaa.payload);
|
||||
|
||||
console.log(`[seqno ${poolEntry}] Parsed ${parsedAttestations.length} price attestations:\n`, parsedAttestations);
|
||||
|
||||
// try {
|
||||
// let tx = await p2w_eth.attestPrice(vaaResponse.vaaBytes, {gasLimit: 1000000});
|
||||
// let retval = await tx.wait();
|
||||
// console.log(`[seqno ${poolEntry}] attestPrice() output:\n`, retval);
|
||||
// } catch(e) {
|
||||
// console.error(`[seqno ${poolEntry}, {parsedAttestations.length} symbols] Error: Could not call attestPrice() on ETH:`, e);
|
||||
|
||||
// seqnoPool.set(seqno, attempts + 1);
|
||||
|
||||
// continue;
|
||||
// }
|
||||
|
||||
console.warn("TODO: implement relayer ETH call");
|
||||
|
||||
// for (let att of parsedAttestations) {
|
||||
|
||||
// let product_id = att.product_id;
|
||||
// let price_type = att.price_type == "Price" ? 1 : 0;
|
||||
// let latest_attestation: any;
|
||||
// try {
|
||||
// let p2w = await p2w_core();
|
||||
|
||||
// console.log(`Looking up latestAttestation for `, product_id, price_type);
|
||||
|
||||
// latest_attestation = await p2w_eth.latestAttestation(product_id, price_type);
|
||||
// } catch(e) {
|
||||
// console.error(`[seqno ${poolEntry}] Error: Could not call latestAttestation() on ETH:`, e);
|
||||
|
||||
// seqnoPool.set(seqno, attempts + 1);
|
||||
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// console.log(`[seqno ${poolEntry}] Latest price type ${price_type} attestation of ${product_id} is ${latest_attestation}`);
|
||||
// }
|
||||
|
||||
if (!readinessProbe) {
|
||||
console.log(`[seqno ${poolEntry}] Attestation successful. Starting readiness probe.`);
|
||||
readinessProbe = readinessProbeRoutine(READINESS_PROBE_PORT);
|
||||
}
|
||||
|
||||
seqnoPool.delete(seqno); // Everything went well, seqno no longer pending.
|
||||
}
|
||||
|
||||
await new Promise(f => {setTimeout(f, P2W_ATTESTATIONS_POLL_INTERVAL_MS);});
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
},
|
||||
"include": ["src", "types"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
{
|
||||
"extends": ["tslint:recommended", "tslint-config-prettier"],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"src/proto/**"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ yarn-error.log*
|
|||
|
||||
# ethereum contracts
|
||||
/contracts
|
||||
/src/ethers-contracts
|
||||
/src/*-contracts/
|
||||
|
||||
# tsproto output
|
||||
/src/proto
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
This project contains a library for interacting with pyth2wormhole and adjacent APIs.
|
||||
|
||||
# Install
|
||||
Firstly, please follow instructions in `//bridge_ui/README.md` where
|
||||
`//` is the Wormhole project root.
|
||||
For now, the in-house dependencies are referenced by relative
|
||||
path. The commands below will build those. For an automated version of
|
||||
this process, please refer to `p2w-relay`'s Dockerfile and/or our [Tilt](https://tilt.dev)
|
||||
devnet with `pyth` enabled.
|
||||
|
||||
# Usage
|
||||
```shell
|
||||
# Run the commands in this README's directory for --prefix to work
|
||||
$ npm --prefix ../../../ethereum ci && npm --prefix ../../../ethereum run build # ETH contracts
|
||||
$ npm --prefix ../../../sdk/js ci # Wormhole SDK
|
||||
$ npm ci && npm run build # Pyth2wormhole SDK
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,12 +8,14 @@
|
|||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && node scripts/copyWasm.js",
|
||||
"build-test": "webpack",
|
||||
"build": "npm run build-eth-types && npm run build-lib",
|
||||
"build-eth-types": "node scripts/copyEthContracts.cjs && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
|
||||
"build-lib": "npm run copy-artifacts && tsc",
|
||||
"build-watch": "npm run copy-artifacts && tsc --watch",
|
||||
"copy-artifacts": "node scripts/copyWasm.cjs && node scripts/copyEthersTypes.cjs",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"postversion": "git push && git push --tags",
|
||||
"preversion": "npm run lint",
|
||||
"test": "node lib/test.js",
|
||||
"version": "npm run format && git add -A src"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -24,22 +26,22 @@
|
|||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@openzeppelin/contracts": "^4.2.0",
|
||||
"@typechain/ethers-v5": "^7.0.1",
|
||||
"@typechain/ethers-v5": "^7.1.2",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^16.6.1",
|
||||
"copy-dir": "^1.3.0",
|
||||
"ethers": "^5.4.4",
|
||||
"find": "^0.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack-cli": "^4.8.0"
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "^1.24.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certusone/wormhole-sdk": "file:../../../sdk/js",
|
||||
"@solana/web3.js": "^1.26.0"
|
||||
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/certusone/wormhole/issues"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
const copydir = require("copy-dir");
|
||||
copydir.sync("../../../ethereum/build/contracts", "./contracts");
|
|
@ -0,0 +1,17 @@
|
|||
const find = require("find");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const SOURCE_ROOT = "src";
|
||||
const TARGET_ROOT = "lib";
|
||||
|
||||
find.eachfile(/\.d\.ts(\..*)?/, SOURCE_ROOT, fname => {
|
||||
|
||||
fname_copy = fname.replace(SOURCE_ROOT, TARGET_ROOT);
|
||||
|
||||
console.log("copying types:", fname, "to", fname_copy);
|
||||
|
||||
fs.mkdirSync(path.dirname(fname_copy), {recursive: true});
|
||||
|
||||
fs.copyFileSync(fname, fname_copy);
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
const find = require("find");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const SOURCE_ROOT = "src";
|
||||
const TARGET_ROOT = "lib";
|
||||
|
||||
find.eachfile(/\.wasm(\..*)?/, SOURCE_ROOT, fname => {
|
||||
|
||||
fname_copy = fname.replace(SOURCE_ROOT, TARGET_ROOT);
|
||||
|
||||
console.log("copyWasm:", fname, "to", fname_copy);
|
||||
|
||||
fs.mkdirSync(path.dirname(fname_copy), {recursive: true});
|
||||
|
||||
fs.copyFileSync(fname, fname_copy);
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
const find = require("find");
|
||||
const fs = require("fs");
|
||||
|
||||
const SOURCE_ROOT = "src";
|
||||
const TARGET_ROOT = "lib";
|
||||
|
||||
find.eachfile(/\.wasm(\..*)?/, SOURCE_ROOT, file => {
|
||||
|
||||
copy = file.replace(SOURCE_ROOT, TARGET_ROOT);
|
||||
|
||||
console.log("copyWasm:", file, "to", copy);
|
||||
|
||||
fs.copyFileSync(file, copy);
|
||||
});
|
|
@ -1,10 +1,42 @@
|
|||
// import {Connection, PublicKey, SystemProgram} from "@solana/web3.js";
|
||||
import { ixFromRust} from "@certusone/wormhole-sdk";
|
||||
import { getSignedVAA, CHAIN_ID_SOLANA} from "@certusone/wormhole-sdk";
|
||||
import { zeroPad } from "ethers/lib/utils";
|
||||
import { PublicKey} from "@solana/web3.js";
|
||||
|
||||
async function p2wHello() {
|
||||
const p2w = await import("./solana/p2w-core/pyth2wormhole");
|
||||
let s = p2w.hello_p2w();
|
||||
console.log(s);
|
||||
var P2W_INSTANCE: any = undefined;
|
||||
|
||||
// Import p2w wasm bindings; be smart about it
|
||||
export async function p2w_core(): Promise<any> {
|
||||
// Only import once if P2W wasm is needed
|
||||
if (!P2W_INSTANCE) {
|
||||
P2W_INSTANCE = await import("./solana/p2w-core/pyth2wormhole");
|
||||
}
|
||||
return P2W_INSTANCE;
|
||||
}
|
||||
|
||||
p2wHello();
|
||||
export function sol_addr2buf(addr: string): Buffer {
|
||||
return Buffer.from(zeroPad(new PublicKey(addr).toBytes(), 32));
|
||||
}
|
||||
|
||||
|
||||
export async function getSignedAttestation(host: string, p2w_addr: string, sequence: number, extraGrpcOpts = {}): Promise<any>
|
||||
{
|
||||
const p2w = await p2w_core();
|
||||
let emitter = p2w.get_emitter_address(p2w_addr);
|
||||
|
||||
let emitterHex = sol_addr2buf(emitter).toString("hex");
|
||||
return await getSignedVAA(host, CHAIN_ID_SOLANA, emitterHex, "" + sequence, extraGrpcOpts);
|
||||
}
|
||||
|
||||
export async function parseAttestation(vaa_payload: Uint8Array): Promise<any> {
|
||||
const p2w = await p2w_core();
|
||||
|
||||
return await p2w.parse_attestation(vaa_payload);
|
||||
}
|
||||
|
||||
export async function parseBatchAttestation(vaa_payload: Uint8Array): Promise<any> {
|
||||
const p2w = await p2w_core();
|
||||
|
||||
console.log("p2w.parse_batch_attestaion is", p2w.parse_batch_attestation);
|
||||
|
||||
return await p2w.parse_batch_attestation(vaa_payload);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
},
|
||||
"types": [],
|
||||
"include": ["src", "types"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.ts',
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: 'development',
|
||||
target: 'node',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'test.js',
|
||||
path: path.resolve(__dirname, 'lib'),
|
||||
},
|
||||
};
|
|
@ -1,44 +1,56 @@
|
|||
#!/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
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from http.client import HTTPConnection
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
from pyth_utils import *
|
||||
|
||||
P2W_ADDRESS = "P2WH424242424242424242424242424242424242424"
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, format="%(asctime)s | %(module)s | %(levelname)s | %(message)s"
|
||||
)
|
||||
|
||||
P2W_SOL_ADDRESS = os.environ.get(
|
||||
"P2W_SOL_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")
|
||||
"P2W_OWNER_KEYPAIR", "/usr/src/solana/keys/p2w_owner.json"
|
||||
)
|
||||
P2W_ATTESTATIONS_PORT = int(os.environ.get("P2W_ATTESTATIONS_PORT", 4343))
|
||||
P2W_INITIALIZE_SOL_CONTRACT = os.environ.get("P2W_INITIALIZE_SOL_CONTRACT", None)
|
||||
|
||||
PYTH_ACCOUNTS_HOST = "pyth"
|
||||
PYTH_ACCOUNTS_PORT = 4242
|
||||
PYTH_TEST_ACCOUNTS_HOST = "pyth"
|
||||
PYTH_TEST_ACCOUNTS_PORT = 4242
|
||||
|
||||
WORMHOLE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
P2W_ATTESTATION_CFG = os.environ.get("P2W_ATTESTATION_CFG", None)
|
||||
|
||||
WORMHOLE_ADDRESS = os.environ.get(
|
||||
"WORMHOLE_ADDRESS", "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
)
|
||||
|
||||
ATTESTATIONS = {
|
||||
"pendingSeqnos": [],
|
||||
}
|
||||
|
||||
|
||||
class P2WAutoattestStatusEndpoint(BaseHTTPRequestHandler):
|
||||
"""
|
||||
A dumb endpoint for last attested price metadata.
|
||||
"""
|
||||
|
||||
def do_GET(self):
|
||||
print(f"Got path {self.path}")
|
||||
logging.info(f"Got path {self.path}")
|
||||
sys.stdout.flush()
|
||||
data = json.dumps(ATTESTATIONS).encode("utf-8")
|
||||
print(f"Sending:\n{data}")
|
||||
logging.debug(f"Sending: {data}")
|
||||
|
||||
ATTESTATIONS["pendingSeqnos"] = []
|
||||
|
||||
|
@ -49,91 +61,144 @@ class P2WAutoattestStatusEndpoint(BaseHTTPRequestHandler):
|
|||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
def serve_attestations():
|
||||
"""
|
||||
Run a barebones HTTP server to share Pyth2wormhole attestation history
|
||||
"""
|
||||
server_address = ('', P2W_ATTESTATIONS_PORT)
|
||||
server_address = ("", P2W_ATTESTATIONS_PORT)
|
||||
httpd = HTTPServer(server_address, P2WAutoattestStatusEndpoint)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
# 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()
|
||||
if SOL_AIRDROP_AMT > 0:
|
||||
# Fund the p2w owner
|
||||
sol_run_or_die("airdrop", [
|
||||
str(SOL_AIRDROP_AMT),
|
||||
"--keypair", P2W_OWNER_KEYPAIR,
|
||||
"--commitment", "finalized",
|
||||
])
|
||||
|
||||
|
||||
# Top up pyth2wormhole owner
|
||||
sol_run_or_die("airdrop", [
|
||||
str(SOL_AIRDROP_AMT),
|
||||
"--keypair", P2W_OWNER_KEYPAIR,
|
||||
"--commitment", "finalized",
|
||||
], capture_output=True)
|
||||
if P2W_INITIALIZE_SOL_CONTRACT is not None:
|
||||
# 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()
|
||||
|
||||
# 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)
|
||||
init_result = run_or_die(
|
||||
[
|
||||
"pyth2wormhole-client",
|
||||
"--log-level",
|
||||
"4",
|
||||
"--p2w-addr",
|
||||
P2W_SOL_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([
|
||||
if init_result.returncode != 0:
|
||||
logging.error(
|
||||
"NOTE: pyth2wormhole-client init failed, retrying with set_config"
|
||||
)
|
||||
run_or_die(
|
||||
[
|
||||
"pyth2wormhole-client",
|
||||
"--log-level",
|
||||
"4",
|
||||
"--p2w-addr",
|
||||
P2W_SOL_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 available symbols from the test pyth publisher if not provided in envs
|
||||
if P2W_ATTESTATION_CFG is None:
|
||||
P2W_ATTESTATION_CFG = "./attestation_cfg_test.yaml"
|
||||
conn = HTTPConnection(PYTH_TEST_ACCOUNTS_HOST, PYTH_TEST_ACCOUNTS_PORT)
|
||||
|
||||
conn.request("GET", "/")
|
||||
|
||||
res = conn.getresponse()
|
||||
|
||||
pyth_accounts = None
|
||||
|
||||
if res.getheader("Content-Type") == "application/json":
|
||||
pyth_accounts = json.load(res)
|
||||
else:
|
||||
logging.error("Bad Content type")
|
||||
sys.exit(1)
|
||||
|
||||
cfg_yaml = f"""
|
||||
---
|
||||
symbols:"""
|
||||
|
||||
logging.info(f"Retrieved {len(pyth_accounts)} Pyth accounts from endpoint: {pyth_accounts}")
|
||||
|
||||
for acc in pyth_accounts:
|
||||
|
||||
name = acc["name"]
|
||||
price = acc["price"]
|
||||
product = acc["product"]
|
||||
|
||||
cfg_yaml += f"""
|
||||
- name: {name}
|
||||
price_addr: {price}
|
||||
product_addr: {product}"""
|
||||
|
||||
with open(P2W_ATTESTATION_CFG, "w") as f:
|
||||
f.write(cfg_yaml)
|
||||
f.flush()
|
||||
|
||||
|
||||
attest_result = 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)
|
||||
"--log-level",
|
||||
"4",
|
||||
"--p2w-addr",
|
||||
P2W_SOL_ADDRESS,
|
||||
"--rpc-url",
|
||||
SOL_RPC_URL,
|
||||
"--payer",
|
||||
P2W_OWNER_KEYPAIR,
|
||||
"attest",
|
||||
"-f",
|
||||
P2W_ATTESTATION_CFG
|
||||
|
||||
# Retrieve current price/product pubkeys from the pyth publisher
|
||||
conn = HTTPConnection(PYTH_ACCOUNTS_HOST, PYTH_ACCOUNTS_PORT)
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
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}")
|
||||
logging.info("p2w_autoattest ready to roll!")
|
||||
logging.info(f"Attest Interval: {P2W_ATTEST_INTERVAL}")
|
||||
|
||||
# Serve p2w endpoint
|
||||
endpoint_thread = threading.Thread(target=serve_attestations, daemon=True)
|
||||
|
@ -143,34 +208,32 @@ endpoint_thread.start()
|
|||
readiness_thread = threading.Thread(target=readiness, daemon=True)
|
||||
readiness_thread.start()
|
||||
|
||||
seqno_regex = re.compile(r"^Sequence number: (\d+)")
|
||||
seqno_regex = re.compile(r"Sequence number: (\d+)")
|
||||
|
||||
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)
|
||||
matches = seqno_regex.findall(attest_result.stdout)
|
||||
|
||||
seqnos = list(map(lambda m: int(m), matches))
|
||||
|
||||
ATTESTATIONS["pendingSeqnos"] += seqnos
|
||||
|
||||
logging.info(f"{len(seqnos)} batch seqno(s) received: {seqnos})")
|
||||
|
||||
attest_result = run_or_die(
|
||||
[
|
||||
"pyth2wormhole-client",
|
||||
"--log-level",
|
||||
"4",
|
||||
"--p2w-addr",
|
||||
P2W_SOL_ADDRESS,
|
||||
"--rpc-url",
|
||||
SOL_RPC_URL,
|
||||
"--payer",
|
||||
P2W_OWNER_KEYPAIR,
|
||||
"attest",
|
||||
"-f",
|
||||
P2W_ATTESTATION_CFG
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
time.sleep(P2W_ATTEST_INTERVAL)
|
||||
|
||||
matches = seqno_regex.match(attest_result.stdout)
|
||||
|
||||
if matches is not None:
|
||||
seqno = int(matches.group(1))
|
||||
print(f"Got seqno {seqno}")
|
||||
|
||||
ATTESTATIONS["pendingSeqnos"].append(seqno)
|
||||
|
||||
else:
|
||||
print(f"Warning: Could not get sequence number")
|
||||
|
||||
nonce += 1
|
||||
|
||||
readiness_thread.join()
|
||||
|
|
|
@ -5,11 +5,13 @@ from pyth_utils import *
|
|||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
PYTH_TEST_SYMBOL_COUNT = int(os.environ.get("PYTH_TEST_SYMBOL_COUNT", "9"))
|
||||
|
||||
class PythAccEndpoint(BaseHTTPRequestHandler):
|
||||
"""
|
||||
|
@ -19,7 +21,7 @@ class PythAccEndpoint(BaseHTTPRequestHandler):
|
|||
def do_GET(self):
|
||||
print(f"Got path {self.path}")
|
||||
sys.stdout.flush()
|
||||
data = json.dumps(ACCOUNTS).encode("utf-8")
|
||||
data = json.dumps(TEST_SYMBOLS).encode("utf-8")
|
||||
print(f"Sending:\n{data}")
|
||||
|
||||
self.send_response(200)
|
||||
|
@ -30,7 +32,7 @@ class PythAccEndpoint(BaseHTTPRequestHandler):
|
|||
self.wfile.flush()
|
||||
|
||||
|
||||
ACCOUNTS = dict()
|
||||
TEST_SYMBOLS = []
|
||||
|
||||
|
||||
def publisher_random_update(price_pubkey):
|
||||
|
@ -65,35 +67,50 @@ sol_run_or_die("airdrop", [
|
|||
# Create a mapping
|
||||
pyth_run_or_die("init_mapping")
|
||||
|
||||
# Add a product
|
||||
prod_pubkey = pyth_run_or_die(
|
||||
"add_product", capture_output=True).stdout.strip()
|
||||
print(f"Added product {prod_pubkey}")
|
||||
|
||||
# Add a price
|
||||
price_pubkey = pyth_run_or_die(
|
||||
"add_price",
|
||||
args=[prod_pubkey, "price"],
|
||||
confirm=False,
|
||||
capture_output=True
|
||||
).stdout.strip()
|
||||
|
||||
print(f"Added price {price_pubkey}")
|
||||
print(f"Creating {PYTH_TEST_SYMBOL_COUNT} test Pyth symbols")
|
||||
|
||||
publisher_pubkey = sol_run_or_die("address", args=[
|
||||
"--keypair", PYTH_PUBLISHER_KEYPAIR
|
||||
], capture_output=True).stdout.strip()
|
||||
|
||||
# Become a publisher
|
||||
pyth_run_or_die(
|
||||
"add_publisher", args=[publisher_pubkey, price_pubkey],
|
||||
confirm=False,
|
||||
debug=True,
|
||||
capture_output=True)
|
||||
print(f"Added publisher {publisher_pubkey}")
|
||||
for i in range(PYTH_TEST_SYMBOL_COUNT):
|
||||
symbol_name = f"Test symbol {i}"
|
||||
# Add a product
|
||||
prod_pubkey = pyth_run_or_die(
|
||||
"add_product", capture_output=True).stdout.strip()
|
||||
|
||||
# Update the price as the newly added publisher
|
||||
publisher_random_update(price_pubkey)
|
||||
print(f"{symbol_name}: Added product {prod_pubkey}")
|
||||
|
||||
# Add a price
|
||||
price_pubkey = pyth_run_or_die(
|
||||
"add_price",
|
||||
args=[prod_pubkey, "price"],
|
||||
confirm=False,
|
||||
capture_output=True
|
||||
).stdout.strip()
|
||||
|
||||
print(f"{symbol_name}: Added price {price_pubkey}")
|
||||
|
||||
# Become a publisher for the new price
|
||||
pyth_run_or_die(
|
||||
"add_publisher", args=[publisher_pubkey, price_pubkey],
|
||||
confirm=False,
|
||||
debug=True,
|
||||
capture_output=True)
|
||||
print(f"{symbol_name}: Added publisher {publisher_pubkey}")
|
||||
|
||||
# Update the prices as the newly added publisher
|
||||
publisher_random_update(price_pubkey)
|
||||
|
||||
sym = {
|
||||
"name": symbol_name,
|
||||
"product": prod_pubkey,
|
||||
"price": price_pubkey
|
||||
}
|
||||
|
||||
TEST_SYMBOLS.append(sym)
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
print(
|
||||
f"Mock updates ready to roll. Updating every {str(PYTH_PUBLISHER_INTERVAL)} seconds")
|
||||
|
@ -101,17 +118,16 @@ print(
|
|||
# Spin off the readiness probe endpoint into a separate thread
|
||||
readiness_thread = threading.Thread(target=readiness, daemon=True)
|
||||
|
||||
# Start an HTTP endpoint for looking up product/price address
|
||||
# Start an HTTP endpoint for looking up test product/price addresses
|
||||
http_service = threading.Thread(target=accounts_endpoint, daemon=True)
|
||||
|
||||
ACCOUNTS["product"] = prod_pubkey
|
||||
ACCOUNTS["price"] = price_pubkey
|
||||
|
||||
readiness_thread.start()
|
||||
http_service.start()
|
||||
|
||||
while True:
|
||||
publisher_random_update(price_pubkey)
|
||||
for sym in TEST_SYMBOLS:
|
||||
publisher_random_update(sym["price"])
|
||||
|
||||
time.sleep(PYTH_PUBLISHER_INTERVAL)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import os
|
||||
import socketserver
|
||||
import sys
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Settings specific to local devnet Pyth instance
|
||||
PYTH = os.environ.get("PYTH", "./pyth")
|
||||
PYTH_KEY_STORE = os.environ.get("PYTH_KEY_STORE", "/home/pyth/.pythd")
|
||||
PYTH_PROGRAM_KEYPAIR = os.environ.get(
|
||||
"PYTH_PROGRAM_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json")
|
||||
"PYTH_PROGRAM_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_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json"
|
||||
)
|
||||
PYTH_PUBLISHER_INTERVAL = float(os.environ.get("PYTH_PUBLISHER_INTERVAL", "5"))
|
||||
|
||||
SOL_AIRDROP_AMT = 100
|
||||
SOL_RPC_HOST = "solana-devnet"
|
||||
SOL_RPC_PORT = 8899
|
||||
SOL_RPC_URL = f"http://{SOL_RPC_HOST}:{str(SOL_RPC_PORT)}"
|
||||
# 0 setting disables airdropping
|
||||
SOL_AIRDROP_AMT = int(os.environ.get("SOL_AIRDROP_AMT", 0))
|
||||
|
||||
# SOL RPC settings
|
||||
SOL_RPC_HOST = os.environ.get("SOL_RPC_HOST", "solana-devnet")
|
||||
SOL_RPC_PORT = int(os.environ.get("SOL_RPC_PORT", 8899))
|
||||
SOL_RPC_URL = os.environ.get(
|
||||
"SOL_RPC_URL", "http://{0}:{1}".format(SOL_RPC_HOST, SOL_RPC_PORT)
|
||||
)
|
||||
|
||||
# A TCP port we open when a service is ready
|
||||
READINESS_PORT = int(os.environ.get("READINESS_PORT", "2000"))
|
||||
|
||||
|
||||
|
@ -24,12 +33,12 @@ 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)
|
||||
sys.stderr.flush()
|
||||
ret = subprocess.run(args, text=True, **kwargs)
|
||||
|
||||
if ret.returncode is not 0:
|
||||
if ret.returncode != 0:
|
||||
print(f"CMD FAIL {ret.returncode}\t{args_readable}", file=sys.stderr)
|
||||
|
||||
out = ret.stdout if ret.stdout is not None else "<not captured>"
|
||||
|
@ -41,7 +50,7 @@ def run_or_die(args, die=True, **kwargs):
|
|||
if die:
|
||||
sys.exit(ret.returncode)
|
||||
else:
|
||||
print(f"CMD DIE FALSE", file=sys.stderr)
|
||||
print(f'{"CMD DIE FALSE"}', file=sys.stderr)
|
||||
|
||||
else:
|
||||
print(f"CMD OK\t{args_readable}", file=sys.stderr)
|
||||
|
@ -54,23 +63,21 @@ 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 [])
|
||||
[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)
|
||||
+ ["-c", "finalized"],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def sol_run_or_die(subcommand, args=[], **kwargs):
|
||||
"""
|
||||
Solana boilerplate in front of run_or_die
|
||||
"""
|
||||
return run_or_die(["solana", subcommand]
|
||||
+ args
|
||||
+ ["--url", SOL_RPC_URL], **kwargs)
|
||||
return run_or_die(["solana", subcommand] + args + ["--url", SOL_RPC_URL], **kwargs)
|
||||
|
||||
|
||||
class ReadinessTCPHandler(socketserver.StreamRequestHandler):
|
||||
|
@ -83,6 +90,7 @@ def readiness():
|
|||
"""
|
||||
Accept connections from readiness probe
|
||||
"""
|
||||
with socketserver.TCPServer(("0.0.0.0", READINESS_PORT), ReadinessTCPHandler) as srv:
|
||||
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