Skip to content

Commit 6b611c8

Browse files
feat(solana-receiver-cli): Add post-twap-update cmd to CLI, update TWAP data model & validations (#2180)
* feat: removed verification_level & posted_slot from TwapUpdate struct, added post twap subcommand to CLI * refactor: address pr comments
1 parent 08e2427 commit 6b611c8

File tree

8 files changed

+292
-122
lines changed

8 files changed

+292
-122
lines changed

target_chains/solana/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/cli/Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-solana-receiver-cli"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55

66
[dependencies]
@@ -10,13 +10,13 @@ shellexpand = "2.1.2"
1010
solana-sdk = { workspace = true }
1111
solana-client = { workspace = true }
1212
anchor-client = { workspace = true }
13-
clap = {version ="3.2.22", features = ["derive"]}
14-
pyth-solana-receiver = {path = "../programs/pyth-solana-receiver" }
15-
wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"} # Used for initializing the wormhole receiver
13+
clap = { version = "3.2.22", features = ["derive"] }
14+
pyth-solana-receiver = { path = "../programs/pyth-solana-receiver" }
15+
wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana" } # Used for initializing the wormhole receiver
1616
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", version = "2.0.0" }
1717
wormhole-vaas-serde = { workspace = true }
1818
serde_wormhole = { workspace = true }
1919
hex = "0.4.3"
20-
borsh = "0.9.3" # Old version of borsh needed for wormhole-solana
21-
wormhole-core-bridge-solana = {workspace = true}
22-
pyth-solana-receiver-sdk = {path = "../pyth_solana_receiver_sdk"}
20+
borsh = "0.9.3" # Old version of borsh needed for wormhole-solana
21+
wormhole-core-bridge-solana = { workspace = true }
22+
pyth-solana-receiver-sdk = { path = "../pyth_solana_receiver_sdk" }

target_chains/solana/cli/src/cli.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,27 @@ pub enum Action {
4848
)]
4949
n_signatures: usize,
5050
},
51+
#[clap(about = "Post a TWAP update from Hermes to Solana")]
52+
PostTwapUpdate {
53+
#[clap(
54+
short = 's',
55+
long,
56+
help = "Start base64 data from Hermes (binary.data.0)"
57+
)]
58+
start_payload: String,
59+
#[clap(
60+
short = 'e',
61+
long,
62+
help = "End base64 data from Hermes (binary.data.1)"
63+
)]
64+
end_payload: String,
65+
},
5166
#[clap(
5267
about = "Initialize a wormhole receiver contract by sequentially replaying the guardian set updates"
5368
)]
5469
InitializeWormholeReceiver {},
5570
InitializePythReceiver {
56-
#[clap(short = 'f', long, help = "Fee in lmaports")]
71+
#[clap(short = 'f', long, help = "Fee in lamports")]
5772
fee: u64,
5873
#[clap(short = 'e', long, parse(try_from_str = Pubkey::from_str), help = "Source emitter")]
5974
emitter: Pubkey,

target_chains/solana/cli/src/main.rs

Lines changed: 197 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,32 @@ fn main() -> Result<()> {
9292
&merkle_price_updates[0],
9393
)?;
9494
}
95+
Action::PostTwapUpdate {
96+
start_payload,
97+
end_payload,
98+
} => {
99+
let rpc_client = RpcClient::new(url);
100+
let payer =
101+
read_keypair_file(&*shellexpand::tilde(&keypair)).expect("Keypair not found");
95102

103+
let start_payload_bytes: Vec<u8> = base64::decode(start_payload)?;
104+
let end_payload_bytes: Vec<u8> = base64::decode(end_payload)?;
105+
106+
let (start_vaa, start_merkle_price_updates) =
107+
deserialize_accumulator_update_data(start_payload_bytes)?;
108+
let (end_vaa, end_merkle_price_updates) =
109+
deserialize_accumulator_update_data(end_payload_bytes)?;
110+
111+
process_write_encoded_vaa_and_post_twap_update(
112+
&rpc_client,
113+
&start_vaa,
114+
&end_vaa,
115+
wormhole,
116+
&payer,
117+
&start_merkle_price_updates[0],
118+
&end_merkle_price_updates[0],
119+
)?;
120+
}
96121
Action::InitializeWormholeReceiver {} => {
97122
let rpc_client = RpcClient::new(url);
98123
let payer =
@@ -367,36 +392,174 @@ pub fn process_write_encoded_vaa_and_post_price_update(
367392
merkle_price_update: &MerklePriceUpdate,
368393
) -> Result<Pubkey> {
369394
let encoded_vaa_keypair = Keypair::new();
395+
396+
// Transaction 1: Create and initialize VAA
397+
let init_instructions = init_encoded_vaa_and_write_initial_data_ixs(
398+
&payer.pubkey(),
399+
vaa,
400+
&wormhole,
401+
&encoded_vaa_keypair,
402+
)?;
403+
process_transaction(
404+
rpc_client,
405+
init_instructions,
406+
&vec![payer, &encoded_vaa_keypair],
407+
)?;
408+
409+
// Transaction 2: Write remaining VAA data, verify VAA, and post price update
410+
let price_update_keypair = Keypair::new();
411+
let mut update_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(600_000)];
412+
413+
update_instructions.extend(write_remaining_data_and_verify_vaa_ixs(
414+
&payer.pubkey(),
415+
vaa,
416+
&encoded_vaa_keypair.pubkey(),
417+
wormhole,
418+
)?);
419+
420+
update_instructions.push(pyth_solana_receiver::instruction::PostUpdate::populate(
421+
payer.pubkey(),
422+
payer.pubkey(),
423+
encoded_vaa_keypair.pubkey(),
424+
price_update_keypair.pubkey(),
425+
merkle_price_update.clone(),
426+
get_random_treasury_id(),
427+
));
428+
429+
process_transaction(
430+
rpc_client,
431+
update_instructions,
432+
&vec![payer, &price_update_keypair],
433+
)?;
434+
435+
Ok(price_update_keypair.pubkey())
436+
}
437+
438+
/// This function verifies start & end VAAs from Hermes via Wormhole to produce encoded VAAs,
439+
/// and then posts a TWAP update using the encoded VAAs. Returns the TwapUpdate account pubkey.
440+
///
441+
/// The operation is split up into 4 transactions:
442+
/// 1. Creates and initializes the start VAA account and writes its first part
443+
/// 2. Creates and initializes the end VAA account and writes its first part
444+
/// 3. Writes the remaining data for both VAAs and verifies them
445+
/// 4. Posts the TWAP update
446+
pub fn process_write_encoded_vaa_and_post_twap_update(
447+
rpc_client: &RpcClient,
448+
start_vaa: &[u8],
449+
end_vaa: &[u8],
450+
wormhole: Pubkey,
451+
payer: &Keypair,
452+
start_merkle_price_update: &MerklePriceUpdate,
453+
end_merkle_price_update: &MerklePriceUpdate,
454+
) -> Result<Pubkey> {
455+
// Create keypairs for both encoded VAAs
456+
let start_encoded_vaa_keypair = Keypair::new();
457+
let end_encoded_vaa_keypair = Keypair::new();
458+
459+
// Transaction 1: Create and initialize start VAA
460+
let start_init_instructions = init_encoded_vaa_and_write_initial_data_ixs(
461+
&payer.pubkey(),
462+
start_vaa,
463+
&wormhole,
464+
&start_encoded_vaa_keypair,
465+
)?;
466+
process_transaction(
467+
rpc_client,
468+
start_init_instructions,
469+
&vec![payer, &start_encoded_vaa_keypair],
470+
)?;
471+
472+
// Transaction 2: Create and initialize end VAA
473+
let end_init_instructions = init_encoded_vaa_and_write_initial_data_ixs(
474+
&payer.pubkey(),
475+
end_vaa,
476+
&wormhole,
477+
&end_encoded_vaa_keypair,
478+
)?;
479+
process_transaction(
480+
rpc_client,
481+
end_init_instructions,
482+
&vec![payer, &end_encoded_vaa_keypair],
483+
)?;
484+
485+
// Transaction 3: Write remaining VAA data and verify both VAAs
486+
let mut verify_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(400_000)];
487+
verify_instructions.extend(write_remaining_data_and_verify_vaa_ixs(
488+
&payer.pubkey(),
489+
start_vaa,
490+
&start_encoded_vaa_keypair.pubkey(),
491+
wormhole,
492+
)?);
493+
verify_instructions.extend(write_remaining_data_and_verify_vaa_ixs(
494+
&payer.pubkey(),
495+
end_vaa,
496+
&end_encoded_vaa_keypair.pubkey(),
497+
wormhole,
498+
)?);
499+
process_transaction(rpc_client, verify_instructions, &vec![payer])?;
500+
501+
// Transaction 4: Post TWAP update
502+
let twap_update_keypair = Keypair::new();
503+
let post_instructions = vec![
504+
ComputeBudgetInstruction::set_compute_unit_limit(400_000),
505+
pyth_solana_receiver::instruction::PostTwapUpdate::populate(
506+
payer.pubkey(),
507+
payer.pubkey(),
508+
start_encoded_vaa_keypair.pubkey(),
509+
end_encoded_vaa_keypair.pubkey(),
510+
twap_update_keypair.pubkey(),
511+
start_merkle_price_update.clone(),
512+
end_merkle_price_update.clone(),
513+
get_random_treasury_id(),
514+
),
515+
];
516+
process_transaction(
517+
rpc_client,
518+
post_instructions,
519+
&vec![payer, &twap_update_keypair],
520+
)?;
521+
522+
Ok(twap_update_keypair.pubkey())
523+
}
524+
525+
/// Creates instructions to initialize an encoded VAA account and write the first part of the VAA data
526+
pub fn init_encoded_vaa_and_write_initial_data_ixs(
527+
payer: &Pubkey,
528+
vaa: &[u8],
529+
wormhole: &Pubkey,
530+
encoded_vaa_keypair: &Keypair,
531+
) -> Result<Vec<Instruction>> {
370532
let encoded_vaa_size: usize = vaa.len() + VAA_START;
371533

372534
let create_encoded_vaa = system_instruction::create_account(
373-
&payer.pubkey(),
535+
payer,
374536
&encoded_vaa_keypair.pubkey(),
375537
Rent::default().minimum_balance(encoded_vaa_size),
376538
encoded_vaa_size as u64,
377-
&wormhole,
539+
wormhole,
378540
);
541+
379542
let init_encoded_vaa_accounts = wormhole_core_bridge_solana::accounts::InitEncodedVaa {
380-
write_authority: payer.pubkey(),
543+
write_authority: *payer,
381544
encoded_vaa: encoded_vaa_keypair.pubkey(),
382545
}
383546
.to_account_metas(None);
384547

385548
let init_encoded_vaa_instruction = Instruction {
386-
program_id: wormhole,
549+
program_id: *wormhole,
387550
accounts: init_encoded_vaa_accounts,
388551
data: wormhole_core_bridge_solana::instruction::InitEncodedVaa.data(),
389552
};
390553

391554
let write_encoded_vaa_accounts = wormhole_core_bridge_solana::accounts::WriteEncodedVaa {
392-
write_authority: payer.pubkey(),
555+
write_authority: *payer,
393556
draft_vaa: encoded_vaa_keypair.pubkey(),
394557
}
395558
.to_account_metas(None);
396559

397-
let write_encoded_vaa_accounts_instruction = Instruction {
398-
program_id: wormhole,
399-
accounts: write_encoded_vaa_accounts.clone(),
560+
let write_encoded_vaa_instruction = Instruction {
561+
program_id: *wormhole,
562+
accounts: write_encoded_vaa_accounts,
400563
data: wormhole_core_bridge_solana::instruction::WriteEncodedVaa {
401564
args: WriteEncodedVaaArgs {
402565
index: 0,
@@ -406,18 +569,27 @@ pub fn process_write_encoded_vaa_and_post_price_update(
406569
.data(),
407570
};
408571

409-
// 1st transaction
410-
process_transaction(
411-
rpc_client,
412-
vec![
413-
create_encoded_vaa,
414-
init_encoded_vaa_instruction,
415-
write_encoded_vaa_accounts_instruction,
416-
],
417-
&vec![payer, &encoded_vaa_keypair],
418-
)?;
572+
Ok(vec![
573+
create_encoded_vaa,
574+
init_encoded_vaa_instruction,
575+
write_encoded_vaa_instruction,
576+
])
577+
}
578+
579+
/// Creates instructions to write remaining VAA data and verify the VAA
580+
pub fn write_remaining_data_and_verify_vaa_ixs(
581+
payer: &Pubkey,
582+
vaa: &[u8],
583+
encoded_vaa_keypair: &Pubkey,
584+
wormhole: Pubkey,
585+
) -> Result<Vec<Instruction>> {
586+
let write_encoded_vaa_accounts = wormhole_core_bridge_solana::accounts::WriteEncodedVaa {
587+
write_authority: *payer,
588+
draft_vaa: *encoded_vaa_keypair,
589+
}
590+
.to_account_metas(None);
419591

420-
let write_encoded_vaa_accounts_instruction_2 = Instruction {
592+
let write_encoded_vaa_instruction = Instruction {
421593
program_id: wormhole,
422594
accounts: write_encoded_vaa_accounts,
423595
data: wormhole_core_bridge_solana::instruction::WriteEncodedVaa {
@@ -432,13 +604,10 @@ pub fn process_write_encoded_vaa_and_post_price_update(
432604
let (header, _): (Header, Body<&RawMessage>) = serde_wormhole::from_slice(vaa).unwrap();
433605
let guardian_set = GuardianSet::key(&wormhole, header.guardian_set_index);
434606

435-
let request_compute_units_instruction: Instruction =
436-
ComputeBudgetInstruction::set_compute_unit_limit(600_000);
437-
438607
let verify_encoded_vaa_accounts = wormhole_core_bridge_solana::accounts::VerifyEncodedVaaV1 {
439608
guardian_set,
440-
write_authority: payer.pubkey(),
441-
draft_vaa: encoded_vaa_keypair.pubkey(),
609+
write_authority: *payer,
610+
draft_vaa: *encoded_vaa_keypair,
442611
}
443612
.to_account_metas(None);
444613

@@ -448,30 +617,10 @@ pub fn process_write_encoded_vaa_and_post_price_update(
448617
data: wormhole_core_bridge_solana::instruction::VerifyEncodedVaaV1 {}.data(),
449618
};
450619

451-
let price_update_keypair = Keypair::new();
452-
453-
let post_update_instructions = pyth_solana_receiver::instruction::PostUpdate::populate(
454-
payer.pubkey(),
455-
payer.pubkey(),
456-
encoded_vaa_keypair.pubkey(),
457-
price_update_keypair.pubkey(),
458-
merkle_price_update.clone(),
459-
get_random_treasury_id(),
460-
);
461-
462-
// 2nd transaction
463-
process_transaction(
464-
rpc_client,
465-
vec![
466-
request_compute_units_instruction,
467-
write_encoded_vaa_accounts_instruction_2,
468-
verify_encoded_vaa_instruction,
469-
post_update_instructions,
470-
],
471-
&vec![payer, &price_update_keypair],
472-
)?;
473-
474-
Ok(price_update_keypair.pubkey())
620+
Ok(vec![
621+
write_encoded_vaa_instruction,
622+
verify_encoded_vaa_instruction,
623+
])
475624
}
476625

477626
pub fn process_transaction(

target_chains/solana/programs/pyth-solana-receiver/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ pub enum ReceiverError {
1616
#[msg("Funds are insufficient to pay the receiving fee")]
1717
InsufficientFunds,
1818
#[msg("Cannot calculate TWAP, end slot must be greater than start slot")]
19+
FeedIdMismatch,
20+
#[msg("The start and end messages must have the same feed ID")]
21+
ExponentMismatch,
22+
#[msg("The start and end messages must have the same exponent")]
1923
InvalidTwapSlots,
2024
#[msg("Start message is not the first update for its timestamp")]
2125
InvalidTwapStartMessage,

0 commit comments

Comments
 (0)