fix(lez/wallet): drop signing with recipient keys

This commit is contained in:
Sergio Chouhy 2026-07-03 15:43:57 -03:00
parent fb98e2d564
commit b3eb74cf46
13 changed files with 233 additions and 131 deletions

View File

@ -12,6 +12,7 @@ use log::info;
use sequencer_service_rpc::RpcClient as _;
pub use test_fixtures::*;
use wallet::{
AccountIdentity,
cli::{
CliAccountMention, Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
@ -19,6 +20,7 @@ use wallet::{
native_token_transfer::AuthTransferSubcommand, token::TokenProgramAgnosticSubcommand,
},
},
program_facades::{native_token_transfer::NativeTokenTransfer, token::Token},
storage::key_chain::FoundPrivateAccount,
};
@ -68,6 +70,34 @@ pub async fn send(
Ok(())
}
/// Like [`send`], but for a `to` that is still a fresh, unclaimed account.
///
/// The wallet CLI's `AuthTransfer::Send` never signs with the recipient's key (by design: the
/// sender's wallet must not sign on behalf of an account it doesn't own). But claiming a fresh
/// account is only possible if that account's own key signs the transaction, so this bypasses
/// the CLI and calls the program facade directly with an explicit `AccountIdentity::Public` for
/// the recipient, using the key the test wallet holds for the account it just created.
///
/// Unlike `send`, this doesn't go through the CLI's own poll-until-included step, so it waits
/// for block creation itself before returning.
pub async fn send_claiming_new_account(
ctx: &mut TestContext,
from: AccountId,
to: AccountId,
amount: u128,
) -> Result<()> {
NativeTokenTransfer(ctx.wallet())
.send_public_transfer(
AccountIdentity::Public(from),
AccountIdentity::Public(to),
amount,
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
/// Create a token (New) and wait for the block to be included.
pub async fn create_token(
ctx: &mut TestContext,
@ -110,6 +140,26 @@ pub async fn token_send(
Ok(())
}
/// Like [`token_send`], but for a `to` that is still a fresh, unclaimed holding account. See
/// [`send_claiming_new_account`] for why the CLI can't be used here.
pub async fn token_send_claiming_new_account(
ctx: &mut TestContext,
from: AccountId,
to: AccountId,
amount: u128,
) -> Result<()> {
Token(ctx.wallet())
.send_transfer_transaction(
AccountIdentity::Public(from),
AccountIdentity::Public(to),
amount,
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
/// Retrieve the native token balance for `account_id`.
pub async fn account_balance(ctx: &TestContext, account_id: AccountId) -> Result<u128> {
Ok(ctx

View File

@ -9,7 +9,7 @@ use std::time::Duration;
use anyhow::Result;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, create_token, get_account, new_account,
public_mention, token_send,
public_mention, token_send_claiming_new_account,
};
use log::info;
use tokio::test;
@ -54,14 +54,11 @@ async fn amm_public() -> Result<()> {
)
.await?;
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`
token_send(
&mut ctx,
public_mention(supply_account_id_1),
public_mention(recipient_account_id_1),
7,
)
.await?;
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`.
// `recipient_account_id_1` is still unclaimed, so this bypasses the wallet CLI (which never
// signs with the recipient's key) and signs with the recipient's own key directly.
token_send_claiming_new_account(&mut ctx, supply_account_id_1, recipient_account_id_1, 7)
.await?;
// Create new token
create_token(
@ -73,14 +70,10 @@ async fn amm_public() -> Result<()> {
)
.await?;
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`
token_send(
&mut ctx,
public_mention(supply_account_id_2),
public_mention(recipient_account_id_2),
7,
)
.await?;
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`.
// `recipient_account_id_2` is still unclaimed, so this bypasses the wallet CLI the same way.
token_send_claiming_new_account(&mut ctx, supply_account_id_2, recipient_account_id_2, 7)
.await?;
info!("=================== SETUP FINISHED ===============");
@ -351,13 +344,9 @@ async fn amm_new_pool_using_labels() -> Result<()> {
)
.await?;
token_send(
&mut ctx,
public_mention(supply_account_id_1),
public_mention(holding_a_id),
5,
)
.await?;
// `holding_a_id` is still unclaimed, so bypass the wallet CLI (see
// `token_send_claiming_new_account`'s docs for why).
token_send_claiming_new_account(&mut ctx, supply_account_id_1, holding_a_id, 5).await?;
// Create token 2 and distribute to holding_b
create_token(
@ -369,13 +358,8 @@ async fn amm_new_pool_using_labels() -> Result<()> {
)
.await?;
token_send(
&mut ctx,
public_mention(supply_account_id_2),
public_mention(holding_b_id),
5,
)
.await?;
// `holding_b_id` is still unclaimed, so bypass the wallet CLI the same way.
token_send_claiming_new_account(&mut ctx, supply_account_id_2, holding_b_id, 5).await?;
// Create AMM pool using account labels instead of IDs
let subcommand = AmmProgramAgnosticSubcommand::New {

View File

@ -128,6 +128,47 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
Ok(())
}
/// A deshielded transfer's public recipient must not be asked to sign the transaction: the
/// sender's private-side proof is the only authorization the protocol requires, and signing
/// with the recipient's key (when the wallet happens to hold it) would leak a link between
/// the two accounts.
#[test]
async fn deshielded_transfer_does_not_sign_with_recipient_key() -> Result<()> {
let mut ctx = TestContext::new().await?;
let from: AccountId = ctx.existing_private_accounts()[0];
let to: AccountId = ctx.existing_public_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: private_mention(from),
to: Some(public_mention(to)),
to_npk: None,
to_vpk: None,
to_keys: None,
to_identifier: Some(0),
amount: 100,
});
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
assert!(
tx.witness_set().signatures_and_public_keys().is_empty(),
"deshielded transfer must not carry any signature, in particular not the recipient's"
);
info!("Deshielded transfer correctly did not sign with the recipient's key");
Ok(())
}
#[test]
async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
let mut ctx = TestContext::new().await?;

View File

@ -1,19 +1,19 @@
use std::time::Duration;
use anyhow::Result;
use anyhow::{Context as _, Result};
use common::transaction::LeeTransaction;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, new_account,
public_mention, send,
public_mention, send, send_claiming_new_account,
};
use lee::public_transaction;
use lee::{PublicKey, public_transaction};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
account::Label,
cli::{
CliAccountMention, Command, account::AccountSubcommand,
CliAccountMention, Command, SubcommandReturnValue, account::AccountSubcommand,
programs::native_token_transfer::AuthTransferSubcommand,
},
};
@ -24,13 +24,20 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
let sender = ctx.existing_public_accounts()[0];
let receiver = ctx.existing_public_accounts()[1];
send(
&mut ctx,
public_mention(sender),
public_mention(receiver),
100,
)
.await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: public_mention(sender),
to: Some(public_mention(receiver)),
to_npk: None,
to_vpk: None,
to_keys: None,
to_identifier: Some(0),
amount: 100,
});
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
@ -45,6 +52,34 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
// The recipient already exists, so the protocol doesn't require its signature, and the
// wallet must never sign with a key it doesn't need to use. Assert the transfer's witness
// set contains exactly the sender's signature, not the recipient's.
let (tx, _block_id) = ctx
.sequencer_client()
.get_transaction(tx_hash)
.await?
.context("transfer transaction should be included in a block")?;
let LeeTransaction::Public(tx) = tx else {
anyhow::bail!("Expected a public transaction");
};
let sender_public_key = PublicKey::new_from_private_key(
ctx.wallet()
.get_account_public_signing_key(sender)
.context("sender should have a signing key")?,
);
let signers: Vec<_> = tx
.witness_set()
.signatures_and_public_keys()
.iter()
.map(|(_, public_key)| public_key)
.collect();
assert_eq!(
signers,
vec![&sender_public_key],
"only the sender should sign a transfer to an existing account"
);
Ok(())
}
@ -55,16 +90,9 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
let new_persistent_account_id = new_account(&mut ctx, false, None).await?;
let sender = ctx.existing_public_accounts()[0];
send(
&mut ctx,
public_mention(sender),
public_mention(new_persistent_account_id),
100,
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// The wallet CLI never signs with the recipient's key, but claiming this fresh account
// requires it, so bypass the CLI for this one send.
send_claiming_new_account(&mut ctx, sender, new_persistent_account_id, 100).await?;
info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, sender).await?;

View File

@ -10,7 +10,7 @@ use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, assert_public_account_restored,
fetch_privacy_preserving_tx, new_account, private_mention, public_mention,
restored_private_account, send, verify_commitment_is_in_state,
restored_private_account, send, send_claiming_new_account, verify_commitment_is_in_state,
};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use lee::AccountId;
@ -121,21 +121,10 @@ async fn restore_keys_from_seed() -> Result<()> {
let to_account_id3 = new_account(&mut ctx, false, Some(ChainIndex::root())).await?;
let to_account_id4 = new_account(&mut ctx, false, Some(ChainIndex::from_str("/0")?)).await?;
// Send to both public accounts
send(
&mut ctx,
public_mention(from),
public_mention(to_account_id3),
102,
)
.await?;
send(
&mut ctx,
public_mention(from),
public_mention(to_account_id4),
103,
)
.await?;
// Send to both public accounts. Both are still unclaimed, so bypass the wallet CLI (which
// never signs with the recipient's key) and sign with the recipient's own key directly.
send_claiming_new_account(&mut ctx, from, to_account_id3, 102).await?;
send_claiming_new_account(&mut ctx, from, to_account_id4, 103).await?;
info!("Preparation complete, performing keys restoration");

View File

@ -9,7 +9,7 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account, private_mention,
public_mention, sync_private, verify_commitment_is_in_state,
public_mention, sync_private, token_send_claiming_new_account, verify_commitment_is_in_state,
};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use log::info;
@ -79,22 +79,17 @@ async fn create_and_transfer_public_token() -> Result<()> {
}
);
// Transfer 7 tokens from supply_acc to recipient_account_id
// Transfer 7 tokens from supply_acc to recipient_account_id. `recipient_account_id` is
// still unclaimed, so this bypasses the wallet CLI (which never signs with the recipient's
// key) and signs with the recipient's own key directly.
let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: public_mention(supply_account_id),
to: Some(public_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_keys: None,
to_identifier: Some(0),
amount: transfer_amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
token_send_claiming_new_account(
&mut ctx,
supply_account_id,
recipient_account_id,
transfer_amount,
)
.await?;
// Check the status of the supply account after transfer
let supply_acc = get_account(&ctx, supply_account_id).await?;
@ -962,21 +957,28 @@ async fn transfer_token_using_from_label() -> Result<()> {
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Transfer token using from_label instead of from
let transfer_amount = 20;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: supply_label.into(),
to: Some(public_mention(recipient_account_id)),
to_npk: None,
to_vpk: None,
to_keys: None,
to_identifier: Some(0),
amount: transfer_amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
// Confirm the label resolves to the account created for it.
let resolved_sender = ctx
.wallet()
.storage()
.resolve_label(&supply_label)
.context("supply_label should resolve to an account")?;
assert_eq!(
resolved_sender,
wallet::account::AccountIdWithPrivacy::Public(supply_account_id)
);
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Transfer token from the label-resolved account. `recipient_account_id` is still
// unclaimed, so this bypasses the wallet CLI (which never signs with the recipient's key)
// and signs with the recipient's own key directly.
let transfer_amount = 20;
token_send_claiming_new_account(
&mut ctx,
supply_account_id,
recipient_account_id,
transfer_amount,
)
.await?;
let recipient_acc = get_account(&ctx, recipient_account_id).await?;
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;

View File

@ -191,7 +191,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_shielded_transfer_to_outer_account(
from_mention.into_public_identity(from_id),
from_mention.into_public_identity(from_id, true),
to_npk,
to_vpk,
to_identifier,
@ -464,7 +464,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned(
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_shielded_transfer(
from_mention.into_public_identity(from_id),
from_mention.into_public_identity(from_id, true),
to_id,
amount,
)) {

View File

@ -159,14 +159,22 @@ impl CliAccountMention {
}
}
/// Convert to an [`crate::AccountIdentity`] for use in a public transaction.
///
/// The `sign` flag indicates whether to sign or not with the account keys.
#[must_use]
pub fn into_public_identity(self, account_id: lee::AccountId) -> crate::AccountIdentity {
pub fn into_public_identity(
self,
account_id: lee::AccountId,
sign: bool,
) -> crate::AccountIdentity {
match self {
Self::KeyPath(key_path) => crate::AccountIdentity::PublicKeycard {
account_id,
key_path,
},
Self::Id(_) | Self::Label(_) => crate::AccountIdentity::Public(account_id),
Self::Id(_) | Self::Label(_) if sign => crate::AccountIdentity::Public(account_id),
Self::Id(_) | Self::Label(_) => crate::AccountIdentity::PublicNoSign(account_id),
}
}
}

View File

@ -138,9 +138,9 @@ impl AmmProgramAgnosticSubcommand {
) => {
let tx_hash = Amm(wallet_core)
.send_new_definition(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
user_holding_a.into_public_identity(a, true),
user_holding_b.into_public_identity(b, true),
user_holding_lp.into_public_identity(lp, true),
balance_a,
balance_b,
)
@ -170,8 +170,8 @@ impl AmmProgramAgnosticSubcommand {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_input(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_a.into_public_identity(a, true),
user_holding_b.into_public_identity(b, true),
amount_in,
min_amount_out,
token_definition,
@ -202,8 +202,8 @@ impl AmmProgramAgnosticSubcommand {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = Amm(wallet_core)
.send_swap_exact_output(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_a.into_public_identity(a, true),
user_holding_b.into_public_identity(b, true),
exact_amount_out,
max_amount_in,
token_definition,
@ -240,9 +240,9 @@ impl AmmProgramAgnosticSubcommand {
) => {
let tx_hash = Amm(wallet_core)
.send_add_liquidity(
user_holding_a.into_public_identity(a),
user_holding_b.into_public_identity(b),
user_holding_lp.into_public_identity(lp),
user_holding_a.into_public_identity(a, true),
user_holding_b.into_public_identity(b, true),
user_holding_lp.into_public_identity(lp, true),
min_amount_lp,
max_amount_a,
max_amount_b,
@ -281,7 +281,7 @@ impl AmmProgramAgnosticSubcommand {
.send_remove_liquidity(
a,
b,
user_holding_lp.into_public_identity(lp),
user_holding_lp.into_public_identity(lp, true),
balance_lp,
min_amount_a,
min_amount_b,

View File

@ -94,7 +94,7 @@ impl AtaSubcommand {
match owner_resolved {
AccountIdWithPrivacy::Public(owner_id) => {
let tx_hash = Ata(wallet_core)
.send_create(owner.into_public_identity(owner_id), definition_id)
.send_create(owner.into_public_identity(owner_id, true), definition_id)
.await?;
wallet_core
.poll_and_finalize_public_transaction(tx_hash)
@ -127,7 +127,7 @@ impl AtaSubcommand {
AccountIdWithPrivacy::Public(from_id) => {
let tx_hash = Ata(wallet_core)
.send_transfer(
from.into_public_identity(from_id),
from.into_public_identity(from_id, true),
definition_id,
to_id,
amount,
@ -162,7 +162,7 @@ impl AtaSubcommand {
AccountIdWithPrivacy::Public(holder_id) => {
let tx_hash = Ata(wallet_core)
.send_burn(
holder.into_public_identity(holder_id),
holder.into_public_identity(holder_id, true),
definition_id,
amount,
)

View File

@ -61,7 +61,7 @@ impl AuthTransferSubcommand {
match resolved {
AccountIdWithPrivacy::Public(pub_account_id) => {
let tx_hash = NativeTokenTransfer(wallet_core)
.register_account(account_id.into_public_identity(pub_account_id))
.register_account(account_id.into_public_identity(pub_account_id, true))
.await?;
wallet_core
@ -123,8 +123,8 @@ impl AuthTransferSubcommand {
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => {
let to_mention = to_account.expect("matched Some branch");
NativeTokenTransferProgramSubcommand::Public {
from: Some(from_account.into_public_identity(from)),
to: Some(to_mention.into_public_identity(to)),
from: Some(from_account.into_public_identity(from, true)),
to: Some(to_mention.into_public_identity(to, false)),
amount,
}
}
@ -143,7 +143,7 @@ impl AuthTransferSubcommand {
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedOwned {
from: Some(from_account.into_public_identity(from)),
from: Some(from_account.into_public_identity(from, true)),
to,
amount,
},
@ -165,7 +165,7 @@ impl AuthTransferSubcommand {
AccountIdWithPrivacy::Public(from) => {
NativeTokenTransferProgramSubcommand::Shielded(
NativeTokenTransferProgramSubcommandShielded::ShieldedForeign {
from: Some(from_account.into_public_identity(from)),
from: Some(from_account.into_public_identity(from, true)),
to_npk,
to_vpk,
to_identifier,

View File

@ -208,7 +208,7 @@ impl TokenProgramAgnosticSubcommand {
(AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => {
TokenProgramSubcommand::Shielded(
TokenProgramSubcommandShielded::TransferTokenShieldedOwned {
sender: Some(from_mention.into_public_identity(from)),
sender: Some(from_mention.into_public_identity(from, true)),
recipient_account_id: to,
balance_to_move: amount,
},
@ -237,7 +237,7 @@ impl TokenProgramAgnosticSubcommand {
),
AccountIdWithPrivacy::Public(from) => TokenProgramSubcommand::Shielded(
TokenProgramSubcommandShielded::TransferTokenShieldedForeign {
sender: Some(from_mention.into_public_identity(from)),
sender: Some(from_mention.into_public_identity(from, true)),
recipient_npk: to_npk,
recipient_vpk: to_vpk,
recipient_identifier: to_identifier,
@ -830,8 +830,8 @@ impl TokenProgramSubcommandPublic {
};
let tx_hash = Token(wallet_core)
.send_transfer_transaction(
sender_account_id.into_public_identity(sender_id),
recipient_account_id.into_public_identity(recipient_id),
sender_account_id.into_public_identity(sender_id, true),
recipient_account_id.into_public_identity(recipient_id, false),
balance_to_move,
)
.await?;
@ -855,7 +855,7 @@ impl TokenProgramSubcommandPublic {
let tx_hash = Token(wallet_core)
.send_burn_transaction(
definition_account_id,
holder_account_id.into_public_identity(holder_id),
holder_account_id.into_public_identity(holder_id, true),
amount,
)
.await?;
@ -881,8 +881,8 @@ impl TokenProgramSubcommandPublic {
};
let tx_hash = Token(wallet_core)
.send_mint_transaction(
definition_account_id.into_public_identity(def_id),
holder_account_id.into_public_identity(holder_id),
definition_account_id.into_public_identity(def_id, true),
holder_account_id.into_public_identity(holder_id, false),
amount,
)
.await?;
@ -1546,8 +1546,8 @@ impl CreateNewTokenProgramSubcommand {
};
let tx_hash = Token(wallet_core)
.send_new_definition(
definition_account_id.into_public_identity(def_id),
supply_account_id.into_public_identity(sup_id),
definition_account_id.into_public_identity(def_id, true),
supply_account_id.into_public_identity(sup_id, true),
name,
total_supply,
)

View File

@ -19,7 +19,7 @@ impl NativeTokenTransfer<'_> {
self.0
.resolve_private_account(from)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
AccountIdentity::Public(to),
AccountIdentity::PublicNoSign(to),
],
instruction_data,
&program.into(),