mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-29 11:29:40 +00:00
feat!(wallet): Merged SigningGroup with AccountManager (#500)
* feat: account manager extension * feat(wallet): added unified way of sending public transactions to all facades * fix(wallet): no sign option added * fix(deny): deny fix * fix(wallet): suggestion 1 * fix(wallet): suggestion fix 1 * feat!: Add new path for externally provided seed to the circuit. BREAKING CHANGE: add identity variants to the circuit and change semantics for `Claim::Authorized` for private PDAs * feat(ci): use separate job per each integration tests module * feat(ci): cache rust artifacts * feat(ci): build integration tests binary once and reuse it * fix(wallet): fmt * ci: add bench-regression workflow with criterion-compare for crypto_primitives_bench * fix(wallet): merge postfix * feat!(wallet): SigningGroup merged with AccountManager * fix(ci): deny and artifacts fix * fix(deny): deny fix * fix keycard and lint --------- Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com> Co-authored-by: Daniil Polyakov <arjentix@gmail.com> Co-authored-by: Moudy <m.ellaz@hotmail.com> Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Co-authored-by: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com>
This commit is contained in:
parent
a927955e04
commit
8ada8ee2da
@ -13,9 +13,9 @@ ignore = [
|
||||
{ id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" },
|
||||
{ id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." },
|
||||
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
|
||||
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
|
||||
{ id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
|
||||
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
|
||||
{ id = "RUSTSEC-2025-0137", reason = "newest `rint` depends on rustc v1.90.0 and we build artifacts with v1.88.8" },
|
||||
]
|
||||
yanked = "deny"
|
||||
unused-ignored-advisory = "deny"
|
||||
|
||||
44
.github/workflows/bench-regression.yml
vendored
Normal file
44
.github/workflows/bench-regression.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "tools/crypto_primitives_bench/**"
|
||||
- "key_protocol/**"
|
||||
- "nssa/core/**"
|
||||
- ".github/workflows/bench-regression.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
name: bench-regression
|
||||
|
||||
jobs:
|
||||
crypto-primitives:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
# criterion-compare-action checks out the base branch in a second
|
||||
# working tree, so we need the full history.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- uses: ./.github/actions/install-logos-blockchain-circuits
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Run criterion-compare against base branch
|
||||
uses: boa-dev/criterion-compare-action@v3
|
||||
with:
|
||||
branchName: ${{ github.base_ref }}
|
||||
cwd: tools/crypto_primitives_bench
|
||||
benchName: primitives
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
100
.github/workflows/ci.yml
vendored
100
.github/workflows/ci.yml
vendored
@ -94,6 +94,12 @@ jobs:
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Lint workspace
|
||||
env:
|
||||
RISC0_SKIP_BUILD: "1"
|
||||
@ -123,6 +129,12 @@ jobs:
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
@ -132,9 +144,10 @@ jobs:
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests --all-features
|
||||
|
||||
integration-tests:
|
||||
integration-tests-prebuild:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90 # TODO: Apply CI cache to speed this up
|
||||
outputs:
|
||||
targets: ${{ steps.discover-targets.outputs.targets }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@ -151,6 +164,75 @@ jobs:
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
- name: Build integration test archive
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
run: cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager
|
||||
|
||||
- name: Upload integration test archive
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: integration-tests-archive
|
||||
path: integration-tests.tar.zst
|
||||
|
||||
- name: Discover integration test targets from archive
|
||||
id: discover-targets
|
||||
run: |
|
||||
cargo nextest list \
|
||||
--archive-file integration-tests.tar.zst \
|
||||
--list-type binaries-only \
|
||||
--message-format json \
|
||||
--no-pager > integration-tests-binaries.json
|
||||
|
||||
targets_json="$(jq -c '[."rust-binaries" | to_entries[] | select(.value.kind == "test" and .value."binary-name" != "tps") | .value."binary-name"] | sort | unique' integration-tests-binaries.json)"
|
||||
|
||||
if [[ "$targets_json" == "[]" ]]; then
|
||||
echo "No integration test targets were discovered." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "targets=$targets_json" >> "$GITHUB_OUTPUT"
|
||||
echo "Discovered integration targets: $targets_json"
|
||||
|
||||
integration-tests:
|
||||
needs: integration-tests-prebuild
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ${{ fromJson(needs.integration-tests-prebuild.outputs.targets) }}
|
||||
name: integration-tests (${{ matrix.target }})
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- uses: ./.github/actions/install-logos-blockchain-circuits
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Download integration test archive
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: integration-tests-archive
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
@ -158,7 +240,7 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run -p integration_tests -- --skip tps_test
|
||||
run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"
|
||||
|
||||
valid-proof-test:
|
||||
runs-on: ubuntu-latest
|
||||
@ -179,6 +261,12 @@ jobs:
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Test valid proof
|
||||
env:
|
||||
RUST_LOG: "info"
|
||||
@ -196,6 +284,12 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install just
|
||||
run: cargo install --locked just
|
||||
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -3646,7 +3646,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@ -7468,7 +7468,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@ -7505,7 +7505,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@ -8471,7 +8471,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pda_spend_proxy.bin
Normal file
BIN
artifacts/test_program_methods/pda_spend_proxy.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -332,7 +332,7 @@ Unlike the public version, `run_hello_world_private.rs` must:
|
||||
|
||||
Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function:
|
||||
```rust
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
wallet_core
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use nssa::{AccountId, program::Program};
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore};
|
||||
use wallet::{AccountIdentity, WalletCore};
|
||||
|
||||
// Before running this example, compile the `hello_world.rs` guest program with:
|
||||
//
|
||||
@ -44,7 +44,7 @@ async fn main() {
|
||||
// Define the desired greeting in ASCII
|
||||
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33];
|
||||
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
wallet_core
|
||||
@ -52,7 +52,6 @@ async fn main() {
|
||||
accounts,
|
||||
Program::serialize_instruction(greeting).unwrap(),
|
||||
&program.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -4,7 +4,7 @@ use nssa::{
|
||||
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
};
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore};
|
||||
use wallet::{AccountIdentity, WalletCore};
|
||||
|
||||
// Before running this example, compile the `simple_tail_call.rs` guest program with:
|
||||
//
|
||||
@ -51,7 +51,7 @@ async fn main() {
|
||||
std::iter::once((hello_world.id(), hello_world)).collect();
|
||||
let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies);
|
||||
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
|
||||
|
||||
// Construct and submit the privacy-preserving transaction
|
||||
let instruction = ();
|
||||
@ -60,7 +60,6 @@ async fn main() {
|
||||
accounts,
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
&program_with_dependencies,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -2,7 +2,7 @@ use clap::{Parser, Subcommand};
|
||||
use common::transaction::NSSATransaction;
|
||||
use nssa::{PublicTransaction, program::Program, public_transaction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use wallet::{PrivacyPreservingAccount, WalletCore};
|
||||
use wallet::{AccountIdentity, WalletCore};
|
||||
|
||||
// Before running this example, compile the `hello_world_with_move_function.rs` guest program with:
|
||||
//
|
||||
@ -99,14 +99,13 @@ async fn main() {
|
||||
} => {
|
||||
let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes());
|
||||
let account_id = account_id.parse().unwrap();
|
||||
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
|
||||
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
|
||||
|
||||
wallet_core
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
&program.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -139,8 +138,8 @@ async fn main() {
|
||||
let to = to.parse().unwrap();
|
||||
|
||||
let accounts = vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
AccountIdentity::Public(from),
|
||||
AccountIdentity::PrivateOwned(to),
|
||||
];
|
||||
|
||||
wallet_core
|
||||
@ -148,7 +147,6 @@ async fn main() {
|
||||
accounts,
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
&program.into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -704,6 +704,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
npk,
|
||||
ssk,
|
||||
identifier: 1337,
|
||||
seed: None,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
|
||||
@ -6,27 +6,37 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use common::transaction::NSSATransaction;
|
||||
use integration_tests::{
|
||||
NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
|
||||
NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
|
||||
verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use log::info;
|
||||
use nssa::{
|
||||
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
AccountId, PrivacyPreservingTransaction, ProgramId,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
program::Program,
|
||||
};
|
||||
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed};
|
||||
use nssa_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::PdaSeed,
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
PrivacyPreservingAccount, WalletCore,
|
||||
AccountIdentity, WalletCore,
|
||||
cli::{Command, account::AccountSubcommand},
|
||||
};
|
||||
|
||||
/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`.
|
||||
///
|
||||
/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses
|
||||
/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding
|
||||
/// via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
/// Funds a private PDA by calling `auth_transfer` directly.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "test helper — grouping args would obscure intent"
|
||||
@ -34,33 +44,68 @@ use wallet::{
|
||||
async fn fund_private_pda(
|
||||
wallet: &WalletCore,
|
||||
sender: AccountId,
|
||||
pda_account_id: AccountId,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: u128,
|
||||
seed: PdaSeed,
|
||||
authority_program_id: ProgramId,
|
||||
amount: u128,
|
||||
proxy_program: &ProgramWithDependencies,
|
||||
auth_transfer_id: ProgramId,
|
||||
auth_transfer: &ProgramWithDependencies,
|
||||
) -> Result<()> {
|
||||
wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender),
|
||||
PrivacyPreservingAccount::PrivatePdaForeign {
|
||||
account_id: pda_account_id,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
},
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true))
|
||||
.context("failed to serialize pda_fund_spend_proxy fund instruction")?,
|
||||
proxy_program,
|
||||
None,
|
||||
)
|
||||
let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
|
||||
let sender_account = wallet
|
||||
.get_account_public(sender)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
.map_err(|e| anyhow::anyhow!("failed to get sender account: {e}"))?;
|
||||
let sender_sk = wallet
|
||||
.get_account_public_signing_key(sender)
|
||||
.context("sender signing key not found")?;
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
|
||||
let epk = eph_holder.generate_ephemeral_public_key();
|
||||
|
||||
let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.context("failed to serialize auth_transfer instruction")?;
|
||||
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
},
|
||||
];
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, pda_pre],
|
||||
instruction,
|
||||
account_identities,
|
||||
auth_transfer,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender],
|
||||
vec![sender_account.nonce],
|
||||
vec![(npk, vpk, epk)],
|
||||
output,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
wallet
|
||||
.sequencer_client
|
||||
.send_transaction(NSSATransaction::PrivacyPreserving(tx))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("send transaction failed: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -79,22 +124,21 @@ async fn spend_private_pda(
|
||||
seed: PdaSeed,
|
||||
amount: u128,
|
||||
spend_program: &ProgramWithDependencies,
|
||||
auth_transfer_id: nssa::ProgramId,
|
||||
auth_transfer_id: ProgramId,
|
||||
) -> Result<()> {
|
||||
wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::PrivatePdaOwned(pda_account_id),
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
vpk: recipient_vpk,
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false))
|
||||
.context("failed to serialize pda_fund_spend_proxy instruction")?,
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id))
|
||||
.context("failed to serialize pda_spend_proxy instruction")?,
|
||||
spend_program,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
@ -126,9 +170,9 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
let proxy = {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../artifacts/test_program_methods")
|
||||
.join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY);
|
||||
.join(NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY);
|
||||
Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?)
|
||||
.context("invalid pda_fund_spend_proxy binary")?
|
||||
.context("invalid pda_spend_proxy binary")?
|
||||
};
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let proxy_id = proxy.id();
|
||||
@ -136,6 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let amount: u128 = 100;
|
||||
|
||||
let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into());
|
||||
let spend_program =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
@ -153,14 +198,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
sender_0,
|
||||
alice_pda_0_id,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
0,
|
||||
seed,
|
||||
proxy_id,
|
||||
amount,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
&auth_transfer_program,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -168,14 +212,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
sender_1,
|
||||
alice_pda_1_id,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
1,
|
||||
seed,
|
||||
proxy_id,
|
||||
amount,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
&auth_transfer_program,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::{
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::Ciphertext,
|
||||
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -60,15 +60,28 @@ pub enum InputAccountIdentity {
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
|
||||
/// external derivation check
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
|
||||
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
|
||||
/// == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
},
|
||||
/// Update of an existing private PDA, authorized, with membership proof. `npk` is derived
|
||||
/// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a
|
||||
/// Update of an existing private PDA, with membership proof. `npk` is derived
|
||||
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
|
||||
/// previously-seen authorization in a chained call.
|
||||
PrivatePdaUpdate {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
|
||||
/// external derivation check
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish
|
||||
/// the binding. The `pre_state` must have `is_authorized == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -461,6 +461,7 @@ mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier,
|
||||
seed: None,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
@ -488,7 +489,7 @@ mod tests {
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk());
|
||||
|
||||
// PDA (new, mask 3)
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
@ -506,6 +507,7 @@ mod tests {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
@ -557,6 +559,7 @@ mod tests {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
@ -747,7 +750,7 @@ mod tests {
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = Program::pda_fund_spend_proxy();
|
||||
let program = Program::pda_spend_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
@ -784,6 +787,7 @@ mod tests {
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
@ -819,6 +823,7 @@ mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -828,7 +833,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = Program::pda_fund_spend_proxy();
|
||||
let program = Program::pda_spend_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
@ -862,6 +867,7 @@ mod tests {
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
|
||||
@ -350,12 +350,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pda_fund_spend_proxy() -> Self {
|
||||
use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID};
|
||||
pub fn pda_spend_proxy() -> Self {
|
||||
use test_program_methods::{PDA_SPEND_PROXY_ELF, PDA_SPEND_PROXY_ID};
|
||||
|
||||
Self {
|
||||
id: PDA_FUND_SPEND_PROXY_ID,
|
||||
elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(),
|
||||
id: PDA_SPEND_PROXY_ID,
|
||||
elf: PDA_SPEND_PROXY_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2218,7 +2218,7 @@ pub mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via
|
||||
/// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via
|
||||
/// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`,
|
||||
/// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the
|
||||
/// second account, leaving position 1 unbound.
|
||||
@ -2249,6 +2249,7 @@ pub mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
@ -2257,7 +2258,7 @@ pub mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit
|
||||
/// Happy path: a program claims a new private PDA via `Claim::Pda(seed)`. The circuit
|
||||
/// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s
|
||||
/// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and
|
||||
/// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim
|
||||
@ -2280,11 +2281,12 @@ pub mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("mask-3 private PDA claim should succeed");
|
||||
let (output, _proof) = result.expect("private PDA claim should succeed");
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
@ -2319,6 +2321,7 @@ pub mod tests {
|
||||
npk: npk_b,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -2326,7 +2329,7 @@ pub mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a
|
||||
/// Happy path for the caller-seeds authorization of a private PDA. The delegator claims a
|
||||
/// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same
|
||||
/// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization
|
||||
/// is established via the private derivation
|
||||
@ -2354,12 +2357,13 @@ pub mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) =
|
||||
result.expect("caller-seeds authorization of mask-3 private PDA should succeed");
|
||||
result.expect("caller-seeds authorization of private PDA should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
}
|
||||
@ -2392,6 +2396,7 @@ pub mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
@ -2401,8 +2406,8 @@ pub mod tests {
|
||||
|
||||
/// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of
|
||||
/// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide
|
||||
/// family-binding check, a program could claim `PDA_alice` (mask-3, `alice_npk`) and
|
||||
/// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse
|
||||
/// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and
|
||||
/// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse
|
||||
/// is supported a later chained call could delegate both to a callee via
|
||||
/// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup
|
||||
/// here: after the first claim records `(program, seed) → PDA_alice`, the second claim
|
||||
@ -2430,11 +2435,13 @@ pub mod tests {
|
||||
npk: keys_a.npk(),
|
||||
ssk: shared_a,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: keys_b.npk(),
|
||||
ssk: shared_b,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
@ -2443,17 +2450,11 @@ pub mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction
|
||||
/// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a
|
||||
/// fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds`, neither is present when a
|
||||
/// program operates on an already-owned private PDA at top level. The reject site is the
|
||||
/// post-loop `private_pda_bound_positions` assertion in
|
||||
/// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller
|
||||
/// A private PDA that is reused at top level without an external seed in the identity still
|
||||
/// fails binding. The noop program emits no `Claim::Pda` and there is no caller
|
||||
/// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires.
|
||||
// TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a
|
||||
// `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit
|
||||
// can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a
|
||||
// claim.
|
||||
/// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is
|
||||
/// the correct path for top-level reuse; this test pins the failure when no seed is provided.
|
||||
#[test]
|
||||
fn private_pda_top_level_reuse_rejected_by_binding_check() {
|
||||
let program = Program::noop();
|
||||
@ -2481,6 +2482,7 @@ pub mod tests {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -4372,15 +4374,15 @@ pub mod tests {
|
||||
let alice_keys = test_private_account_keys_1();
|
||||
let alice_npk = alice_keys.npk();
|
||||
|
||||
let proxy = Program::pda_fund_spend_proxy();
|
||||
let proxy = Program::pda_spend_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let proxy_id = proxy.id();
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let amount: u128 = 100;
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
let spend_with_deps =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into());
|
||||
|
||||
let funder_id = funder_keys.account_id();
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
|
||||
@ -4406,7 +4408,7 @@ pub mod tests {
|
||||
let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk());
|
||||
let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk());
|
||||
|
||||
// Fund alice_pda_0
|
||||
// Fund alice_pda_0 via authenticated_transfer directly.
|
||||
{
|
||||
let funder_account = state.get_account_by_id(funder_id);
|
||||
let funder_nonce = funder_account.nonce;
|
||||
@ -4415,16 +4417,18 @@ pub mod tests {
|
||||
AccountWithMetadata::new(funder_account, true, funder_id),
|
||||
AccountWithMetadata::new(Account::default(), false, alice_pda_0_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
|
||||
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_0,
|
||||
identifier: 0,
|
||||
seed: Some((seed, proxy_id)),
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
&auth_transfer.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
@ -4448,7 +4452,7 @@ pub mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Fund alice_pda_1
|
||||
// Fund alice_pda_1 the same way with identifier 1.
|
||||
{
|
||||
let funder_account = state.get_account_by_id(funder_id);
|
||||
let funder_nonce = funder_account.nonce;
|
||||
@ -4457,16 +4461,18 @@ pub mod tests {
|
||||
AccountWithMetadata::new(funder_account, true, funder_id),
|
||||
AccountWithMetadata::new(Account::default(), false, alice_pda_1_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
|
||||
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_1,
|
||||
identifier: 1,
|
||||
seed: Some((seed, proxy_id)),
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
&auth_transfer.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
@ -4504,7 +4510,7 @@ pub mod tests {
|
||||
AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id),
|
||||
AccountWithMetadata::new(recipient_account, true, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: alice_shared_0,
|
||||
@ -4513,10 +4519,11 @@ pub mod tests {
|
||||
.get_proof_for_commitment(&commitment_pda_0)
|
||||
.expect("pda_0 must be in state"),
|
||||
identifier: 0,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
@ -4545,10 +4552,10 @@ pub mod tests {
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id),
|
||||
AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id),
|
||||
AccountWithMetadata::new(recipient_account, false, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: alice_shared_1,
|
||||
@ -4557,10 +4564,11 @@ pub mod tests {
|
||||
.get_proof_for_commitment(&commitment_pda_1)
|
||||
.expect("pda_1 must be in state"),
|
||||
identifier: 1,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
@ -4585,5 +4593,70 @@ pub mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount);
|
||||
|
||||
// Re-fund alice_pda_1 top-level via auth_transfer using PrivatePdaUpdate with an
|
||||
// external seed.
|
||||
let alice_pda_1_account_after_spend = Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: 0,
|
||||
nonce: alice_pda_1_account
|
||||
.nonce
|
||||
.private_account_nonce_increment(&alice_keys.nsk),
|
||||
..Account::default()
|
||||
};
|
||||
let commitment_pda_1_after_spend =
|
||||
Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend);
|
||||
let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk());
|
||||
{
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let recipient_nonce = recipient_account.nonce;
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(recipient_account, true, recipient_id),
|
||||
AccountWithMetadata::new(
|
||||
alice_pda_1_account_after_spend,
|
||||
false,
|
||||
alice_pda_1_id,
|
||||
),
|
||||
],
|
||||
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
nsk: alice_keys.nsk,
|
||||
ssk: alice_shared_1_refund,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&commitment_pda_1_after_spend)
|
||||
.expect("pda_1 after spend must be in state"),
|
||||
identifier: 1,
|
||||
seed: Some((seed, proxy_id)),
|
||||
},
|
||||
],
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![recipient_nonce],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([12; 32]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
5,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,6 +305,68 @@ impl ExecutionState {
|
||||
}
|
||||
Entry::Vacant(_) => {
|
||||
// Pre state for the initial call
|
||||
let pre_state_position = self.pre_states.len();
|
||||
let external_seed = match account_identities.get(pre_state_position) {
|
||||
Some(InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
}) => {
|
||||
let expected = AccountId::for_private_pda(
|
||||
authority_program_id,
|
||||
seed,
|
||||
npk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
pre_account_id, expected,
|
||||
"External seed mismatch for PrivatePdaInit at position {pre_state_position}"
|
||||
);
|
||||
Some((*seed, *authority_program_id))
|
||||
}
|
||||
Some(InputAccountIdentity::PrivatePdaUpdate {
|
||||
nsk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
}) => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let expected = AccountId::for_private_pda(
|
||||
authority_program_id,
|
||||
seed,
|
||||
&npk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
pre_account_id, expected,
|
||||
"External seed mismatch for PrivatePdaUpdate at position {pre_state_position}"
|
||||
);
|
||||
Some((*seed, *authority_program_id))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
// External seed is only consulted the first time the account is seen.
|
||||
// Subsequent calls need no re-check because the entry is already recorded on
|
||||
// private_pda_bound_positions.
|
||||
if let Some((seed, authority_program_id)) = external_seed {
|
||||
assert!(
|
||||
!pre.is_authorized,
|
||||
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
|
||||
);
|
||||
bind_private_pda_position(
|
||||
&mut self.private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
authority_program_id,
|
||||
seed,
|
||||
);
|
||||
assert_family_binding(
|
||||
&mut self.pda_family_binding,
|
||||
authority_program_id,
|
||||
seed,
|
||||
pre_account_id,
|
||||
);
|
||||
}
|
||||
self.pre_states.push(pre);
|
||||
}
|
||||
}
|
||||
@ -348,14 +410,11 @@ impl ExecutionState {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if account_identity.is_private_pda() {
|
||||
} else {
|
||||
// Private accounts: don't enforce the claim semantics. Unauthorized private
|
||||
// claiming is intentionally allowed
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
assert!(
|
||||
pre_is_authorized,
|
||||
"Cannot claim unauthorized private PDA {pre_account_id}"
|
||||
);
|
||||
}
|
||||
Claim::Authorized => {}
|
||||
Claim::Pda(seed) => {
|
||||
let (npk, identifier) = self
|
||||
.private_pda_npk_by_position
|
||||
@ -383,10 +442,6 @@ impl ExecutionState {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standalone private accounts: don't enforce the claim semantics.
|
||||
// Unauthorized private claiming is intentionally allowed since operating
|
||||
// these accounts requires the npk/nsk keypair anyway.
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = program_id;
|
||||
|
||||
@ -148,6 +148,7 @@ pub fn compute_circuit_output(
|
||||
npk: _,
|
||||
ssk,
|
||||
identifier,
|
||||
seed: _,
|
||||
} => {
|
||||
// The npk-to-account_id binding is established upstream in
|
||||
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
|
||||
@ -172,7 +173,7 @@ pub fn compute_circuit_output(
|
||||
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let (pda_program_id, seed) = pda_seed_by_position
|
||||
let (authority_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaInit position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
@ -181,7 +182,7 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
program_id: *authority_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
@ -195,14 +196,16 @@ pub fn compute_circuit_output(
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
seed: external_seed,
|
||||
} => {
|
||||
// The npk binding is established upstream. Authorization must already be set;
|
||||
// an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an
|
||||
// unbound PDA, which the upstream binding check would have rejected anyway,
|
||||
// but we assert here to fail fast and document the precondition.
|
||||
// With an external seed the binding comes from the circuit input and the
|
||||
// pre_state is intentionally unauthorized; without one the binding comes from
|
||||
// a Claim or caller pda_seeds, so the pre_state must already be authorized.
|
||||
// When `external_seed` is `Some`, execution_state already asserted
|
||||
// `!pre_state.is_authorized`.
|
||||
assert!(
|
||||
pre_state.is_authorized,
|
||||
"PrivatePdaUpdate requires authorized pre_state"
|
||||
pre_state.is_authorized ^ external_seed.is_some(),
|
||||
"PrivatePdaUpdate requires authorized pre_state or external seed"
|
||||
);
|
||||
|
||||
let new_nullifier = compute_update_nullifier_and_set_digest(
|
||||
@ -214,7 +217,7 @@ pub fn compute_circuit_output(
|
||||
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let (pda_program_id, seed) = pda_seed_by_position
|
||||
let (authority_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
@ -223,7 +226,7 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
program_id: *authority_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
|
||||
@ -34,7 +34,7 @@ pub mod setup;
|
||||
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
|
||||
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin";
|
||||
pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin";
|
||||
pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin";
|
||||
pub const NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY: &str = "pda_spend_proxy.bin";
|
||||
|
||||
pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0";
|
||||
pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080;
|
||||
|
||||
@ -9,9 +9,7 @@ use sequencer_service::{GenesisAction, SequencerHandle};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tempfile::TempDir;
|
||||
use testcontainers::compose::DockerCompose;
|
||||
use wallet::{
|
||||
AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides,
|
||||
};
|
||||
use wallet::{AccDecodeData::Decode, AccountIdentity, WalletCore, config::WalletConfigOverrides};
|
||||
|
||||
use crate::{
|
||||
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT,
|
||||
@ -293,12 +291,11 @@ async fn claim_funds_from_vault_to_private(
|
||||
let (tx_hash, mut secrets) = wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(owner_id),
|
||||
PrivacyPreservingAccount::Public(owner_vault_id),
|
||||
AccountIdentity::PrivateOwned(owner_id),
|
||||
AccountIdentity::Public(owner_vault_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program_with_dependencies,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to submit private vault claim transaction")?;
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
use nssa_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
},
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Proxy for interacting with private PDAs via `auth_transfer`.
|
||||
///
|
||||
/// The `is_fund` flag selects the operating mode:
|
||||
///
|
||||
/// - `false` (Spend): `pre_states = [pda (authorized), recipient]`. Debits the PDA. The PDA-to-npk
|
||||
/// binding is established via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
///
|
||||
/// - `true` (Fund): `pre_states = [sender (authorized), pda (foreign/uninitialized)]`. Credits the
|
||||
/// PDA. A direct call to `auth_transfer` cannot bind the PDA because `auth_transfer` uses
|
||||
/// `Claim::Authorized`, not `Claim::Pda`. Routing through this proxy establishes the binding via
|
||||
/// `pda_seeds` in the chained call.
|
||||
type Instruction = (PdaSeed, u128, ProgramId, bool);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (seed, amount, auth_transfer_id, is_fund),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert!(first.is_authorized, "first pre_state must be authorized");
|
||||
|
||||
let chained_pre_states = if is_fund {
|
||||
let pda_authorized = AccountWithMetadata {
|
||||
account: second.account.clone(),
|
||||
account_id: second.account_id,
|
||||
is_authorized: true,
|
||||
};
|
||||
vec![first.clone(), pda_authorized]
|
||||
} else {
|
||||
vec![first.clone(), second.clone()]
|
||||
};
|
||||
|
||||
let first_post = AccountPostState::new(first.account.clone());
|
||||
let second_post = AccountPostState::new(second.account.clone());
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
pre_states: chained_pre_states,
|
||||
pda_seeds: vec![seed],
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![first, second],
|
||||
vec![first_post, second_post],
|
||||
)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
50
test_program_methods/guest/src/bin/pda_spend_proxy.rs
Normal file
50
test_program_methods/guest/src/bin/pda_spend_proxy.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Proxy for spending from a private PDA via `auth_transfer`.
|
||||
///
|
||||
/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient.
|
||||
/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
type Instruction = (PdaSeed, u128, ProgramId);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (seed, amount, auth_transfer_id),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert!(first.is_authorized, "first pre_state must be authorized");
|
||||
|
||||
let first_post = AccountPostState::new(first.account.clone());
|
||||
let second_post = AccountPostState::new(second.account.clone());
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
pre_states: vec![first.clone(), second.clone()],
|
||||
pda_seeds: vec![seed],
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![first, second],
|
||||
vec![first_post, second_post],
|
||||
)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
@ -15,7 +15,7 @@ use test_fixtures::{DiskSizes, TestContext};
|
||||
use wallet::cli::SubcommandReturnValue;
|
||||
|
||||
const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
const TX_INCLUSION_TIMEOUT: Duration = Duration::from_mins(2);
|
||||
|
||||
/// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes`
|
||||
/// is the full Block (header + body + bedrock metadata) and is the closest
|
||||
|
||||
@ -181,7 +181,7 @@ async fn measure_bedrock_finality(ctx: &TestContext) -> Result<Duration> {
|
||||
.context("connect indexer WS")?;
|
||||
let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?;
|
||||
|
||||
let timeout = Duration::from_secs(60);
|
||||
let timeout = Duration::from_mins(1);
|
||||
let started = std::time::Instant::now();
|
||||
let poll = async {
|
||||
loop {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{AccountId, PrivateKey};
|
||||
use keycard_wallet::{KeycardWallet, python_path};
|
||||
use nssa::{AccountId, PrivateKey, PublicKey, Signature};
|
||||
use nssa_core::{
|
||||
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
|
||||
SharedSecretKey,
|
||||
@ -11,8 +12,15 @@ use nssa_core::{
|
||||
use crate::{ExecutionFailureKind, WalletCore};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PrivacyPreservingAccount {
|
||||
pub enum AccountIdentity {
|
||||
Public(AccountId),
|
||||
/// A public account without signing. Would not try to sign, even if account is owned.
|
||||
PublicNoSign(AccountId),
|
||||
/// A public account from keycard. Mandatory signing.
|
||||
PublicKeycard {
|
||||
account_id: AccountId,
|
||||
key_path: String,
|
||||
},
|
||||
PrivateOwned(AccountId),
|
||||
PrivateForeign {
|
||||
npk: NullifierPublicKey,
|
||||
@ -50,10 +58,15 @@ pub enum PrivacyPreservingAccount {
|
||||
},
|
||||
}
|
||||
|
||||
impl PrivacyPreservingAccount {
|
||||
impl AccountIdentity {
|
||||
#[must_use]
|
||||
/// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key
|
||||
/// lookup.
|
||||
pub const fn is_public(&self) -> bool {
|
||||
matches!(&self, Self::Public(_))
|
||||
matches!(
|
||||
&self,
|
||||
Self::Public(_) | Self::PublicNoSign(_) | Self::PublicKeycard { .. }
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -82,23 +95,29 @@ enum State {
|
||||
account: AccountWithMetadata,
|
||||
sk: Option<PrivateKey>,
|
||||
},
|
||||
PublicKeycard {
|
||||
account: AccountWithMetadata,
|
||||
key_path: String,
|
||||
},
|
||||
Private(AccountPreparedData),
|
||||
}
|
||||
|
||||
pub struct AccountManager {
|
||||
states: Vec<State>,
|
||||
pin: Option<String>,
|
||||
}
|
||||
|
||||
impl AccountManager {
|
||||
pub async fn new(
|
||||
wallet: &WalletCore,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
) -> Result<Self, ExecutionFailureKind> {
|
||||
let mut states = Vec::with_capacity(accounts.len());
|
||||
let mut pin = None;
|
||||
|
||||
for account in accounts {
|
||||
let state = match account {
|
||||
PrivacyPreservingAccount::Public(account_id) => {
|
||||
AccountIdentity::Public(account_id) => {
|
||||
let acc = wallet
|
||||
.get_account_public(account_id)
|
||||
.await
|
||||
@ -109,12 +128,52 @@ impl AccountManager {
|
||||
|
||||
State::Public { account, sk }
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateOwned(account_id) => {
|
||||
AccountIdentity::PublicNoSign(account_id) => {
|
||||
let acc = wallet
|
||||
.get_account_public(account_id)
|
||||
.await
|
||||
.map_err(ExecutionFailureKind::SequencerError)?;
|
||||
|
||||
let sk = None;
|
||||
let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id);
|
||||
|
||||
State::Public { account, sk }
|
||||
}
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id,
|
||||
key_path,
|
||||
} => {
|
||||
let acc = wallet
|
||||
.get_account_public(account_id)
|
||||
.await
|
||||
.map_err(ExecutionFailureKind::SequencerError)?;
|
||||
|
||||
let account = AccountWithMetadata::new(acc.clone(), true, account_id);
|
||||
|
||||
if pin.is_none() {
|
||||
pin = Some(
|
||||
crate::helperfunctions::read_pin()
|
||||
.map_err(|e| {
|
||||
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<
|
||||
pyo3::exceptions::PyRuntimeError,
|
||||
_,
|
||||
>(
|
||||
e.to_string()
|
||||
))
|
||||
})?
|
||||
.as_str()
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
State::PublicKeycard { account, key_path }
|
||||
}
|
||||
AccountIdentity::PrivateOwned(account_id) => {
|
||||
let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?;
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
@ -138,11 +197,11 @@ impl AccountManager {
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivatePdaOwned(account_id) => {
|
||||
AccountIdentity::PrivatePdaOwned(account_id) => {
|
||||
let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?;
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivatePdaForeign {
|
||||
AccountIdentity::PrivatePdaForeign {
|
||||
account_id,
|
||||
npk,
|
||||
vpk,
|
||||
@ -166,7 +225,7 @@ impl AccountManager {
|
||||
};
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateShared {
|
||||
AccountIdentity::PrivateShared {
|
||||
nsk,
|
||||
npk,
|
||||
vpk,
|
||||
@ -180,7 +239,7 @@ impl AccountManager {
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivatePdaShared {
|
||||
AccountIdentity::PrivatePdaShared {
|
||||
account_id,
|
||||
nsk,
|
||||
npk,
|
||||
@ -199,27 +258,33 @@ impl AccountManager {
|
||||
states.push(state);
|
||||
}
|
||||
|
||||
Ok(Self { states })
|
||||
Ok(Self { states, pin })
|
||||
}
|
||||
|
||||
pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
|
||||
self.states
|
||||
.iter()
|
||||
.map(|state| match state {
|
||||
State::Public { account, .. } => account.clone(),
|
||||
State::Public { account, .. } | State::PublicKeycard { account, .. } => {
|
||||
account.clone()
|
||||
}
|
||||
State::Private(pre) => pre.pre_state.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_account_nonces(&self) -> Vec<Nonce> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce),
|
||||
State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
// Must match the signature order produced by sign_message(): local accounts first,
|
||||
// keycard accounts second.
|
||||
let local = self.states.iter().filter_map(|state| match state {
|
||||
State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce),
|
||||
State::PublicKeycard { .. } | State::Private(_) => None,
|
||||
});
|
||||
let keycard = self.states.iter().filter_map(|state| match state {
|
||||
State::PublicKeycard { account, .. } => Some(account.account.nonce),
|
||||
State::Public { .. } | State::Private(_) => None,
|
||||
});
|
||||
local.chain(keycard).collect()
|
||||
}
|
||||
|
||||
pub fn private_account_keys(&self) -> Vec<PrivateAccountKeys> {
|
||||
@ -232,7 +297,7 @@ impl AccountManager {
|
||||
vpk: pre.vpk.clone(),
|
||||
epk: pre.epk.clone(),
|
||||
}),
|
||||
State::Public { .. } => None,
|
||||
State::Public { .. } | State::PublicKeycard { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@ -245,18 +310,20 @@ impl AccountManager {
|
||||
self.states
|
||||
.iter()
|
||||
.map(|state| match state {
|
||||
State::Public { .. } => InputAccountIdentity::Public,
|
||||
State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public,
|
||||
State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) {
|
||||
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: pre.ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier: pre.identifier,
|
||||
seed: None,
|
||||
},
|
||||
_ => InputAccountIdentity::PrivatePdaInit {
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
identifier: pre.identifier,
|
||||
seed: None,
|
||||
},
|
||||
},
|
||||
State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
|
||||
@ -287,21 +354,66 @@ impl AccountManager {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, .. } => Some(account.account_id),
|
||||
State::Public { account, .. } | State::PublicKeycard { account, .. } => {
|
||||
Some(account.account_id)
|
||||
}
|
||||
State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_account_auth(&self) -> Vec<&PrivateKey> {
|
||||
pub fn public_non_keycard_account_auth(&self) -> Vec<&PrivateKey> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { sk, .. } => sk.as_ref(),
|
||||
State::Private(_) => None,
|
||||
State::PublicKeycard { .. } | State::Private(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sign_message(&self, message_hash: [u8; 32]) -> Result<Vec<(Signature, PublicKey)>> {
|
||||
let mut sigs: Vec<(Signature, PublicKey)> = self
|
||||
.public_non_keycard_account_auth()
|
||||
.into_iter()
|
||||
.map(|key| {
|
||||
(
|
||||
Signature::new(key, &message_hash),
|
||||
PublicKey::new_from_private_key(key),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let keycard_paths = self
|
||||
.states
|
||||
.iter()
|
||||
.fold(vec![], |mut acc, state| match state {
|
||||
State::Private(_) | State::Public { .. } => acc,
|
||||
State::PublicKeycard {
|
||||
account: _,
|
||||
key_path,
|
||||
} => {
|
||||
acc.push(key_path.as_str());
|
||||
acc
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(pin) = self.pin.clone() {
|
||||
pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = KeycardWallet::new(py)?;
|
||||
wallet.connect(py, &pin)?;
|
||||
for path in keycard_paths {
|
||||
sigs.push(wallet.sign_message_for_path(py, path, &message_hash)?);
|
||||
}
|
||||
drop(wallet.close_session(py));
|
||||
Ok(())
|
||||
})
|
||||
.map_err(anyhow::Error::from)?;
|
||||
}
|
||||
|
||||
Ok(sigs)
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountPreparedData {
|
||||
@ -410,7 +522,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn private_shared_is_private() {
|
||||
let acc = PrivacyPreservingAccount::PrivateShared {
|
||||
let acc = AccountIdentity::PrivateShared {
|
||||
nsk: [0; 32],
|
||||
npk: NullifierPublicKey([1; 32]),
|
||||
vpk: ViewingPublicKey::from_scalar([2; 32]),
|
||||
@ -10,46 +10,39 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use account_manager::AccountIdentity;
|
||||
use anyhow::{Context as _, Result};
|
||||
use bip39::Mnemonic;
|
||||
use common::{HashType, transaction::NSSATransaction};
|
||||
use config::WalletConfig;
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use keycard_wallet::KeycardWallet;
|
||||
use log::info;
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, PublicKey, PublicTransaction, Signature,
|
||||
Account, AccountId, PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::ProgramWithDependencies, message::EncryptedAccountData,
|
||||
},
|
||||
program::Program,
|
||||
public_transaction::WitnessSet as PublicWitnessSet,
|
||||
};
|
||||
use nssa_core::{
|
||||
Commitment, MembershipProof, SharedSecretKey,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
program::InstructionData,
|
||||
Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData,
|
||||
};
|
||||
pub use privacy_preserving_tx::PrivacyPreservingAccount;
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||
use storage::Storage;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
|
||||
use crate::{
|
||||
account::{AccountIdWithPrivacy, Label},
|
||||
cli::CliAccountMention,
|
||||
config::WalletConfigOverrides,
|
||||
poller::TxPoller,
|
||||
signing::SigningGroup,
|
||||
storage::key_chain::SharedAccountEntry,
|
||||
};
|
||||
|
||||
pub mod account;
|
||||
mod account_manager;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod helperfunctions;
|
||||
pub mod poller;
|
||||
mod privacy_preserving_tx;
|
||||
pub mod program_facades;
|
||||
pub mod signing;
|
||||
pub mod storage;
|
||||
@ -295,13 +288,10 @@ impl WalletCore {
|
||||
self.storage.key_chain_mut().set_sealing_secret_key(key);
|
||||
}
|
||||
|
||||
/// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant.
|
||||
/// Resolve an `AccountId` to the appropriate `AccountIdentity` variant.
|
||||
/// Checks the key tree first, then shared private accounts.
|
||||
#[must_use]
|
||||
pub fn resolve_private_account(
|
||||
&self,
|
||||
account_id: nssa::AccountId,
|
||||
) -> Option<PrivacyPreservingAccount> {
|
||||
pub fn resolve_private_account(&self, account_id: nssa::AccountId) -> Option<AccountIdentity> {
|
||||
// Check key tree first
|
||||
if self
|
||||
.storage
|
||||
@ -309,7 +299,7 @@ impl WalletCore {
|
||||
.private_account(account_id)
|
||||
.is_some()
|
||||
{
|
||||
return Some(PrivacyPreservingAccount::PrivateOwned(account_id));
|
||||
return Some(AccountIdentity::PrivateOwned(account_id));
|
||||
}
|
||||
|
||||
// Check shared private accounts
|
||||
@ -322,9 +312,9 @@ impl WalletCore {
|
||||
.key_chain()
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
|
||||
if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.pda_program_id) {
|
||||
if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.authority_program_id) {
|
||||
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
|
||||
Some(PrivacyPreservingAccount::PrivatePdaShared {
|
||||
Some(AccountIdentity::PrivatePdaShared {
|
||||
account_id,
|
||||
nsk: keys.nullifier_secret_key,
|
||||
npk: keys.generate_nullifier_public_key(),
|
||||
@ -341,7 +331,7 @@ impl WalletCore {
|
||||
result
|
||||
};
|
||||
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
|
||||
Some(PrivacyPreservingAccount::PrivateShared {
|
||||
Some(AccountIdentity::PrivateShared {
|
||||
nsk: keys.nullifier_secret_key,
|
||||
npk: keys.generate_nullifier_public_key(),
|
||||
vpk: keys.generate_viewing_public_key(),
|
||||
@ -365,7 +355,7 @@ impl WalletCore {
|
||||
group_label: Label,
|
||||
identifier: nssa_core::Identifier,
|
||||
pda_seed: Option<nssa_core::program::PdaSeed>,
|
||||
pda_program_id: Option<nssa_core::program::ProgramId>,
|
||||
authority_program_id: Option<nssa_core::program::ProgramId>,
|
||||
) {
|
||||
self.storage.key_chain_mut().insert_shared_private_account(
|
||||
account_id,
|
||||
@ -373,7 +363,7 @@ impl WalletCore {
|
||||
group_label,
|
||||
identifier,
|
||||
pda_seed,
|
||||
pda_program_id,
|
||||
authority_program_id,
|
||||
account: Account::default(),
|
||||
},
|
||||
);
|
||||
@ -564,145 +554,28 @@ impl WalletCore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a public transaction, fetching nonces automatically from
|
||||
/// [`SigningGroup::signing_ids`].
|
||||
pub async fn send_public_tx<T: serde::Serialize>(
|
||||
&self,
|
||||
program: &Program,
|
||||
account_ids: Vec<AccountId>,
|
||||
instruction: T,
|
||||
groups: SigningGroup,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let nonces = self
|
||||
.get_accounts_nonces(groups.signing_ids())
|
||||
.await
|
||||
.map_err(ExecutionFailureKind::SequencerError)?;
|
||||
self.send_public_tx_with_nonces(program, account_ids, nonces, instruction, groups)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a public transaction with caller-supplied nonces.
|
||||
///
|
||||
/// Use this when the caller needs to assemble or augment nonces before submission
|
||||
/// (e.g. injecting a keycard account nonce that was fetched separately).
|
||||
pub async fn send_public_tx_with_nonces<T: serde::Serialize>(
|
||||
&self,
|
||||
program: &Program,
|
||||
account_ids: Vec<AccountId>,
|
||||
nonces: Vec<Nonce>,
|
||||
instruction: T,
|
||||
groups: SigningGroup,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction,
|
||||
)?;
|
||||
|
||||
let pin = if groups.needs_pin() {
|
||||
crate::helperfunctions::read_pin()
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?
|
||||
.as_str()
|
||||
.to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let sigs = groups
|
||||
.sign_all(&message.hash(), &pin)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
|
||||
let tx = PublicTransaction::new(message, PublicWitnessSet::from_raw_parts(sigs));
|
||||
Ok(self
|
||||
.sequencer_client
|
||||
.send_transaction(NSSATransaction::Public(tx))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn send_privacy_preserving_tx(
|
||||
&self,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
mention: Option<&CliAccountMention>,
|
||||
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
self.send_privacy_preserving_tx_with_pre_check(
|
||||
accounts,
|
||||
instruction_data,
|
||||
program,
|
||||
|_| Ok(()),
|
||||
mention,
|
||||
)
|
||||
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_privacy_preserving_tx_with_pre_check(
|
||||
&self,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
mention: Option<&CliAccountMention>,
|
||||
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?;
|
||||
let acc_manager = account_manager::AccountManager::new(self, accounts).await?;
|
||||
|
||||
let mut pre_states = acc_manager.pre_states();
|
||||
|
||||
let (keycard_account, keycard_pin, keycard_path) = if let Some(key_path_str) =
|
||||
mention.and_then(CliAccountMention::key_path)
|
||||
{
|
||||
let pin = crate::helperfunctions::read_pin().map_err(|e| {
|
||||
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<
|
||||
pyo3::exceptions::PyRuntimeError,
|
||||
_,
|
||||
>(e.to_string()))
|
||||
})?;
|
||||
let account_id_str =
|
||||
KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)?;
|
||||
let account_id: AccountId = match account_id_str
|
||||
.parse::<AccountIdWithPrivacy>()
|
||||
.expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: invalid account id parsed")
|
||||
{
|
||||
AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id,
|
||||
};
|
||||
let account = self
|
||||
.get_account_public(account_id)
|
||||
.await
|
||||
.expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: unable to retrieve public account");
|
||||
let pin_str = pin.as_str().to_owned();
|
||||
(
|
||||
Some(AccountWithMetadata {
|
||||
account,
|
||||
is_authorized: true,
|
||||
account_id,
|
||||
}),
|
||||
Some(pin_str),
|
||||
Some(key_path_str.to_owned()),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
let mut nonces: Vec<Nonce> = acc_manager.public_account_nonces().into_iter().collect();
|
||||
|
||||
let mut account_ids: Vec<AccountId> = acc_manager.public_account_ids();
|
||||
|
||||
if let Some(acc) = keycard_account.as_ref() {
|
||||
if acc_manager.public_account_ids().contains(&acc.account_id) {
|
||||
if let Some(pre) = pre_states
|
||||
.iter_mut()
|
||||
.find(|p| p.account_id == acc.account_id)
|
||||
{
|
||||
pre.is_authorized = true;
|
||||
}
|
||||
nonces.push(acc.account.nonce);
|
||||
} else {
|
||||
nonces.push(acc.account.nonce);
|
||||
account_ids.push(acc.account_id);
|
||||
pre_states.push(acc.clone());
|
||||
}
|
||||
}
|
||||
let pre_states = acc_manager.pre_states();
|
||||
|
||||
tx_pre_check(
|
||||
&pre_states
|
||||
@ -717,54 +590,30 @@ impl WalletCore {
|
||||
instruction_data,
|
||||
acc_manager.account_identities(),
|
||||
&program.to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let message =
|
||||
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
account_ids,
|
||||
nonces,
|
||||
acc_manager.public_account_ids(),
|
||||
acc_manager.public_account_nonces(),
|
||||
private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
|
||||
.collect(),
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let message_hash = message.hash();
|
||||
let signatures_public_keys = acc_manager
|
||||
.sign_message(message_hash)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
|
||||
let witness_set =
|
||||
if let (Some(pin), Some(path)) = (keycard_pin.as_deref(), keycard_path.as_deref()) {
|
||||
let hash = message.hash();
|
||||
let local_auth = acc_manager.public_account_auth();
|
||||
let mut sigs: Vec<(Signature, PublicKey)> = local_auth
|
||||
.iter()
|
||||
.map(|&key| {
|
||||
(
|
||||
Signature::new(key, &hash),
|
||||
PublicKey::new_from_private_key(key),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let keycard_sig = pyo3::Python::with_gil(|py| {
|
||||
let mut ctx = crate::signing::KeycardSessionContext::new(pin);
|
||||
let result = ctx
|
||||
.get_or_connect(py)
|
||||
.and_then(|w| w.sign_message_for_path(py, path, &hash));
|
||||
ctx.close(py);
|
||||
result
|
||||
})
|
||||
.map_err(ExecutionFailureKind::KeycardError)?;
|
||||
sigs.push(keycard_sig);
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts(
|
||||
sigs, proof,
|
||||
)
|
||||
} else {
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
|
||||
&message,
|
||||
proof,
|
||||
&acc_manager.public_account_auth(),
|
||||
)
|
||||
};
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts(
|
||||
signatures_public_keys,
|
||||
proof,
|
||||
);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let shared_secrets: Vec<_> = private_account_keys
|
||||
@ -780,6 +629,69 @@ impl WalletCore {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send_pub_tx(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
self.send_pub_tx_with_pre_check(accounts, instruction_data, program, |_| Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_pub_tx_with_pre_check(
|
||||
&self,
|
||||
accounts: Vec<AccountIdentity>,
|
||||
instruction_data: InstructionData,
|
||||
program: &ProgramWithDependencies,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
// Public transaction, all accounts must be public
|
||||
if accounts.iter().any(AccountIdentity::is_private) {
|
||||
return Err(ExecutionFailureKind::TransactionBuildError(
|
||||
nssa::error::NssaError::InvalidInput(
|
||||
"Private accounts are not allowed in public transactions".to_owned(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let acc_manager = account_manager::AccountManager::new(self, accounts).await?;
|
||||
|
||||
let pre_states = acc_manager.pre_states();
|
||||
tx_pre_check(
|
||||
&pre_states
|
||||
.iter()
|
||||
.map(|pre| &pre.account)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
let account_ids = acc_manager.public_account_ids();
|
||||
let program_id = program.program.id();
|
||||
let nonces = acc_manager.public_account_nonces();
|
||||
|
||||
let message = nssa::public_transaction::Message::new_preserialized(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction_data,
|
||||
);
|
||||
|
||||
let message_hash = message.hash();
|
||||
let signatures_public_keys = acc_manager
|
||||
.sign_message(message_hash)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
|
||||
let witness_set =
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(signatures_public_keys);
|
||||
|
||||
let tx = nssa::public_transaction::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self
|
||||
.sequencer_client
|
||||
.send_transaction(NSSATransaction::Public(tx))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn sync_to_latest_block(&mut self) -> Result<u64> {
|
||||
let latest_block_id = self.sequencer_client.get_last_block_id().await?;
|
||||
println!("Latest block is {latest_block_id}");
|
||||
@ -899,7 +811,7 @@ impl WalletCore {
|
||||
.key_chain()
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
|
||||
let keys = match (&entry.pda_seed, &entry.pda_program_id) {
|
||||
let keys = match (&entry.pda_seed, &entry.authority_program_id) {
|
||||
(Some(pda_seed), Some(program_id)) => {
|
||||
holder.derive_keys_for_pda(program_id, pda_seed)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ use common::HashType;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroup};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
|
||||
pub struct Amm<'wallet>(pub &'wallet WalletCore);
|
||||
|
||||
impl Amm<'_> {
|
||||
@ -15,18 +15,36 @@ impl Amm<'_> {
|
||||
user_holding_lp: AccountId,
|
||||
balance_a: u128,
|
||||
balance_b: u128,
|
||||
a_mention: &CliAccountMention,
|
||||
b_mention: &CliAccountMention,
|
||||
lp_mention: &CliAccountMention,
|
||||
user_holding_a_mention: &CliAccountMention,
|
||||
user_holding_b_mention: &CliAccountMention,
|
||||
user_holding_lp_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let user_holding_a_identity = user_holding_a_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_a),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_a,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let user_holding_b_identity = user_holding_b_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_b),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_b,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_lp),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_lp,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
let instruction = amm_core::Instruction::NewDefinition {
|
||||
token_a_amount: balance_a,
|
||||
token_b_amount: balance_b,
|
||||
amm_program_id,
|
||||
};
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
@ -50,26 +68,28 @@ impl Amm<'_> {
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
pool_lp,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
];
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(a_mention, user_holding_a, self.0)
|
||||
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
|
||||
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let instruction = amm_core::Instruction::NewDefinition {
|
||||
token_a_amount: balance_a,
|
||||
token_b_amount: balance_b,
|
||||
amm_program_id,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(amm_pool),
|
||||
AccountIdentity::PublicNoSign(vault_holding_a),
|
||||
AccountIdentity::PublicNoSign(vault_holding_b),
|
||||
AccountIdentity::PublicNoSign(pool_lp),
|
||||
user_holding_a_identity,
|
||||
user_holding_b_identity,
|
||||
user_holding_lp_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -81,17 +101,11 @@ impl Amm<'_> {
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
a_mention: &CliAccountMention,
|
||||
b_mention: &CliAccountMention,
|
||||
user_holding_a_mention: &CliAccountMention,
|
||||
user_holding_b_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let instruction = amm_core::Instruction::SwapExactInput {
|
||||
swap_amount_in,
|
||||
min_amount_out,
|
||||
token_definition_id_in,
|
||||
};
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
@ -114,31 +128,58 @@ impl Amm<'_> {
|
||||
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
let instruction = amm_core::Instruction::SwapExactInput {
|
||||
swap_amount_in,
|
||||
min_amount_out,
|
||||
token_definition_id_in,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
];
|
||||
|
||||
let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in {
|
||||
(user_holding_a, a_mention)
|
||||
} else if definition_token_b_id == token_definition_id_in {
|
||||
(user_holding_b, b_mention)
|
||||
} else {
|
||||
if (token_definition_id_in != definition_token_a_id)
|
||||
&& (token_definition_id_in != definition_token_b_id)
|
||||
{
|
||||
return Err(ExecutionFailureKind::AccountDataError(
|
||||
token_definition_id_in,
|
||||
));
|
||||
}
|
||||
|
||||
let user_a_signing_identity = if token_definition_id_in == definition_token_a_id {
|
||||
user_holding_a_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_a),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_a,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AccountIdentity::PublicNoSign(user_holding_a)
|
||||
};
|
||||
|
||||
let user_b_signing_identity = if token_definition_id_in == definition_token_b_id {
|
||||
user_holding_b_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_b),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_b,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AccountIdentity::PublicNoSign(user_holding_b)
|
||||
};
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(seller_mention, account_id_auth, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(amm_pool),
|
||||
AccountIdentity::PublicNoSign(vault_holding_a),
|
||||
AccountIdentity::PublicNoSign(vault_holding_b),
|
||||
user_a_signing_identity,
|
||||
user_b_signing_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -150,17 +191,11 @@ impl Amm<'_> {
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
a_mention: &CliAccountMention,
|
||||
b_mention: &CliAccountMention,
|
||||
user_holding_a_mention: &CliAccountMention,
|
||||
user_holding_b_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let instruction = amm_core::Instruction::SwapExactOutput {
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
};
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
@ -183,31 +218,58 @@ impl Amm<'_> {
|
||||
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
let instruction = amm_core::Instruction::SwapExactOutput {
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
token_definition_id_in,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
];
|
||||
|
||||
let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in {
|
||||
(user_holding_a, a_mention)
|
||||
} else if definition_token_b_id == token_definition_id_in {
|
||||
(user_holding_b, b_mention)
|
||||
} else {
|
||||
if (token_definition_id_in != definition_token_a_id)
|
||||
&& (token_definition_id_in != definition_token_b_id)
|
||||
{
|
||||
return Err(ExecutionFailureKind::AccountDataError(
|
||||
token_definition_id_in,
|
||||
));
|
||||
}
|
||||
|
||||
let user_a_signing_identity = if token_definition_id_in == definition_token_a_id {
|
||||
user_holding_a_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_a),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_a,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AccountIdentity::PublicNoSign(user_holding_a)
|
||||
};
|
||||
|
||||
let user_b_signing_identity = if token_definition_id_in == definition_token_b_id {
|
||||
user_holding_b_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_b),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_b,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AccountIdentity::PublicNoSign(user_holding_b)
|
||||
};
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(seller_mention, account_id_auth, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(amm_pool),
|
||||
AccountIdentity::PublicNoSign(vault_holding_a),
|
||||
AccountIdentity::PublicNoSign(vault_holding_b),
|
||||
user_a_signing_identity,
|
||||
user_b_signing_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -220,18 +282,36 @@ impl Amm<'_> {
|
||||
min_amount_liquidity: u128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
max_amount_to_add_token_b: u128,
|
||||
a_mention: &CliAccountMention,
|
||||
b_mention: &CliAccountMention,
|
||||
lp_mention: &CliAccountMention,
|
||||
user_holding_a_mention: &CliAccountMention,
|
||||
user_holding_b_mention: &CliAccountMention,
|
||||
user_holding_lp_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let instruction = amm_core::Instruction::AddLiquidity {
|
||||
min_amount_liquidity,
|
||||
max_amount_to_add_token_a,
|
||||
max_amount_to_add_token_b,
|
||||
};
|
||||
let user_holding_a_identity = user_holding_a_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_a),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_a,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let user_holding_b_identity = user_holding_b_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_b),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_b,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_lp),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_lp,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
@ -255,26 +335,28 @@ impl Amm<'_> {
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
pool_lp,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
];
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(a_mention, user_holding_a, self.0)
|
||||
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
|
||||
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let instruction = amm_core::Instruction::AddLiquidity {
|
||||
min_amount_liquidity,
|
||||
max_amount_to_add_token_a,
|
||||
max_amount_to_add_token_b,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(amm_pool),
|
||||
AccountIdentity::PublicNoSign(vault_holding_a),
|
||||
AccountIdentity::PublicNoSign(vault_holding_b),
|
||||
AccountIdentity::PublicNoSign(pool_lp),
|
||||
user_holding_a_identity,
|
||||
user_holding_b_identity,
|
||||
user_holding_lp_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -287,16 +369,18 @@ impl Amm<'_> {
|
||||
remove_liquidity_amount: u128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
min_amount_to_remove_token_b: u128,
|
||||
lp_mention: &CliAccountMention,
|
||||
user_holding_lp_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let instruction = amm_core::Instruction::RemoveLiquidity {
|
||||
remove_liquidity_amount,
|
||||
min_amount_to_remove_token_a,
|
||||
min_amount_to_remove_token_b,
|
||||
};
|
||||
let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
|
||||
AccountIdentity::Public(user_holding_lp),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: user_holding_lp,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::amm();
|
||||
let amm_program_id = Program::amm().id();
|
||||
|
||||
let user_a_acc = self
|
||||
.0
|
||||
.get_account_public(user_holding_a)
|
||||
@ -320,23 +404,28 @@ impl Amm<'_> {
|
||||
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
|
||||
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
|
||||
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
|
||||
let instruction = amm_core::Instruction::RemoveLiquidity {
|
||||
remove_liquidity_amount,
|
||||
min_amount_to_remove_token_a,
|
||||
min_amount_to_remove_token_b,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let account_ids = vec![
|
||||
amm_pool,
|
||||
vault_holding_a,
|
||||
vault_holding_b,
|
||||
pool_lp,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
];
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(lp_mention, user_holding_lp, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(amm_pool),
|
||||
AccountIdentity::PublicNoSign(vault_holding_a),
|
||||
AccountIdentity::PublicNoSign(vault_holding_b),
|
||||
AccountIdentity::PublicNoSign(pool_lp),
|
||||
AccountIdentity::PublicNoSign(user_holding_a),
|
||||
AccountIdentity::PublicNoSign(user_holding_b),
|
||||
user_holding_lp_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,7 @@ use nssa::{
|
||||
};
|
||||
use nssa_core::SharedSecretKey;
|
||||
|
||||
use crate::{
|
||||
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
|
||||
signing::SigningGroup,
|
||||
};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
|
||||
|
||||
pub struct Ata<'wallet>(pub &'wallet WalletCore);
|
||||
|
||||
@ -21,22 +18,36 @@ impl Ata<'_> {
|
||||
definition_id: AccountId,
|
||||
owner_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let owner_identity =
|
||||
owner_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(owner_id), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: owner_id,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let program = Program::ata();
|
||||
let ata_program_id = program.id();
|
||||
let ata_id = get_associated_token_account_id(
|
||||
&ata_program_id,
|
||||
&compute_ata_seed(owner_id, definition_id),
|
||||
);
|
||||
|
||||
let account_ids = vec![owner_id, definition_id, ata_id];
|
||||
let instruction = ata_core::Instruction::Create { ata_program_id };
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(owner_mention, owner_id, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
owner_identity,
|
||||
AccountIdentity::PublicNoSign(definition_id),
|
||||
AccountIdentity::PublicNoSign(ata_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -48,25 +59,39 @@ impl Ata<'_> {
|
||||
amount: u128,
|
||||
owner_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let owner_identity =
|
||||
owner_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(owner_id), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: owner_id,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let program = Program::ata();
|
||||
let ata_program_id = program.id();
|
||||
let sender_ata_id = get_associated_token_account_id(
|
||||
&ata_program_id,
|
||||
&compute_ata_seed(owner_id, definition_id),
|
||||
);
|
||||
|
||||
let account_ids = vec![owner_id, sender_ata_id, recipient_id];
|
||||
let instruction = ata_core::Instruction::Transfer {
|
||||
ata_program_id,
|
||||
amount,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(owner_mention, owner_id, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
owner_identity,
|
||||
AccountIdentity::PublicNoSign(sender_ata_id),
|
||||
AccountIdentity::PublicNoSign(recipient_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -77,25 +102,39 @@ impl Ata<'_> {
|
||||
amount: u128,
|
||||
owner_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let owner_identity =
|
||||
owner_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(owner_id), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: owner_id,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let program = Program::ata();
|
||||
let ata_program_id = program.id();
|
||||
let holder_ata_id = get_associated_token_account_id(
|
||||
&ata_program_id,
|
||||
&compute_ata_seed(owner_id, definition_id),
|
||||
);
|
||||
|
||||
let account_ids = vec![owner_id, holder_ata_id, definition_id];
|
||||
let instruction = ata_core::Instruction::Burn {
|
||||
ata_program_id,
|
||||
amount,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(owner_mention, owner_id, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
self.0
|
||||
.send_public_tx(&program, account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
owner_identity,
|
||||
AccountIdentity::PublicNoSign(holder_ata_id),
|
||||
AccountIdentity::PublicNoSign(definition_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -118,17 +157,12 @@ impl Ata<'_> {
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(definition_id),
|
||||
PrivacyPreservingAccount::Public(ata_id),
|
||||
AccountIdentity::Public(definition_id),
|
||||
AccountIdentity::Public(ata_id),
|
||||
];
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
instruction_data,
|
||||
&ata_with_token_dependency(),
|
||||
None,
|
||||
)
|
||||
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
|
||||
.await
|
||||
.map(|(hash, mut secrets)| {
|
||||
let secret = secrets.pop().expect("expected owner's secret");
|
||||
@ -160,17 +194,12 @@ impl Ata<'_> {
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(sender_ata_id),
|
||||
PrivacyPreservingAccount::Public(recipient_id),
|
||||
AccountIdentity::Public(sender_ata_id),
|
||||
AccountIdentity::Public(recipient_id),
|
||||
];
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
instruction_data,
|
||||
&ata_with_token_dependency(),
|
||||
None,
|
||||
)
|
||||
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
|
||||
.await
|
||||
.map(|(hash, mut secrets)| {
|
||||
let secret = secrets.pop().expect("expected owner's secret");
|
||||
@ -201,17 +230,12 @@ impl Ata<'_> {
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_ata_id),
|
||||
PrivacyPreservingAccount::Public(definition_id),
|
||||
AccountIdentity::Public(holder_ata_id),
|
||||
AccountIdentity::Public(definition_id),
|
||||
];
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
accounts,
|
||||
instruction_data,
|
||||
&ata_with_token_dependency(),
|
||||
None,
|
||||
)
|
||||
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
|
||||
.await
|
||||
.map(|(hash, mut secrets)| {
|
||||
let secret = secrets.pop().expect("expected owner's secret");
|
||||
|
||||
@ -2,7 +2,7 @@ use common::HashType;
|
||||
use nssa::AccountId;
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::{ExecutionFailureKind, PrivacyPreservingAccount};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind};
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_deshielded_transfer(
|
||||
@ -19,12 +19,11 @@ impl NativeTokenTransfer<'_> {
|
||||
self.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(to),
|
||||
AccountIdentity::Public(to),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
|
||||
@ -5,7 +5,7 @@ use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::{ExecutionFailureKind, PrivacyPreservingAccount};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind};
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn register_account_private(
|
||||
@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> {
|
||||
vec![account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -50,7 +49,7 @@ impl NativeTokenTransfer<'_> {
|
||||
self.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
identifier: to_identifier,
|
||||
@ -59,7 +58,6 @@ impl NativeTokenTransfer<'_> {
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -93,7 +91,6 @@ impl NativeTokenTransfer<'_> {
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
|
||||
@ -3,7 +3,10 @@ use common::HashType;
|
||||
use nssa::{AccountId, program::Program};
|
||||
|
||||
use super::NativeTokenTransfer;
|
||||
use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroup};
|
||||
use crate::{
|
||||
AccountIdentity, ExecutionFailureKind, cli::CliAccountMention,
|
||||
program_facades::native_token_transfer::auth_transfer_preparation,
|
||||
};
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_public_transfer(
|
||||
@ -14,20 +17,33 @@ impl NativeTokenTransfer<'_> {
|
||||
from_mention: &CliAccountMention,
|
||||
to_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(from_mention, from, self.0)
|
||||
.and_then(|()| groups.add_optional(to_mention, to, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
let from_identity =
|
||||
from_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(from), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: from,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let to_identity = to_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(to), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: to,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
self.0
|
||||
.send_public_tx(
|
||||
&Program::authenticated_transfer_program(),
|
||||
vec![from, to],
|
||||
AuthTransferInstruction::Transfer {
|
||||
amount: balance_to_move,
|
||||
},
|
||||
groups,
|
||||
.send_pub_tx_with_pre_check(
|
||||
vec![from_identity, to_identity],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -37,18 +53,21 @@ impl NativeTokenTransfer<'_> {
|
||||
from: AccountId,
|
||||
account_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(account_mention, from, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let from_identity =
|
||||
account_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(from), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: from,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let instruction_data = Program::serialize_instruction(AuthTransferInstruction::Initialize)?;
|
||||
|
||||
self.0
|
||||
.send_public_tx(
|
||||
&Program::authenticated_transfer_program(),
|
||||
vec![from],
|
||||
AuthTransferInstruction::Initialize,
|
||||
groups,
|
||||
)
|
||||
.send_pub_tx(vec![from_identity], instruction_data, &program.into())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ use nssa::AccountId;
|
||||
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::{ExecutionFailureKind, PrivacyPreservingAccount, cli::CliAccountMention};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, cli::CliAccountMention};
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_shielded_transfer(
|
||||
@ -13,11 +13,21 @@ impl NativeTokenTransfer<'_> {
|
||||
balance_to_move: u128,
|
||||
from_mention: &CliAccountMention,
|
||||
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
|
||||
let from_identity =
|
||||
from_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(from), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: from,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
from_identity,
|
||||
self.0
|
||||
.resolve_private_account(to)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
@ -25,7 +35,6 @@ impl NativeTokenTransfer<'_> {
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
Some(from_mention),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -46,12 +55,22 @@ impl NativeTokenTransfer<'_> {
|
||||
balance_to_move: u128,
|
||||
from_mention: &CliAccountMention,
|
||||
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
|
||||
let from_identity =
|
||||
from_mention
|
||||
.key_path()
|
||||
.map_or(AccountIdentity::Public(from), |key_path| {
|
||||
AccountIdentity::PublicKeycard {
|
||||
account_id: from,
|
||||
key_path: key_path.to_owned(),
|
||||
}
|
||||
});
|
||||
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
from_identity,
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
identifier: to_identifier,
|
||||
@ -60,7 +79,6 @@ impl NativeTokenTransfer<'_> {
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
Some(from_mention),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
use common::{HashType, transaction::NSSATransaction};
|
||||
use nssa::AccountId;
|
||||
use common::HashType;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{MembershipProof, SharedSecretKey};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
|
||||
use crate::{ExecutionFailureKind, PrivacyPreservingAccount, WalletCore};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore};
|
||||
|
||||
pub struct Pinata<'wallet>(pub &'wallet WalletCore);
|
||||
|
||||
@ -14,20 +13,21 @@ impl Pinata<'_> {
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let account_ids = vec![pinata_account_id, winner_account_id];
|
||||
let program_id = nssa::program::Program::pinata().id();
|
||||
let message =
|
||||
nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution)
|
||||
.unwrap();
|
||||
let program = Program::pinata();
|
||||
let instruction = solution;
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self
|
||||
.0
|
||||
.sequencer_client
|
||||
.send_transaction(NSSATransaction::Public(tx))
|
||||
.await?)
|
||||
self.0
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(pinata_account_id),
|
||||
AccountIdentity::PublicNoSign(winner_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Claim a pinata reward using a privacy-preserving transaction for an already-initialized
|
||||
@ -55,14 +55,13 @@ impl Pinata<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(pinata_account_id),
|
||||
AccountIdentity::Public(pinata_account_id),
|
||||
self.0
|
||||
.resolve_private_account(winner_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&nssa::program::Program::pinata().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
|
||||
@ -3,10 +3,7 @@ use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
|
||||
use token_core::Instruction;
|
||||
|
||||
use crate::{
|
||||
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
|
||||
signing::SigningGroup,
|
||||
};
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
|
||||
|
||||
pub struct Token<'wallet>(pub &'wallet WalletCore);
|
||||
|
||||
@ -20,17 +17,33 @@ impl Token<'_> {
|
||||
definition_mention: &CliAccountMention,
|
||||
supply_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let account_ids = vec![definition_account_id, supply_account_id];
|
||||
let instruction = Instruction::NewFungibleDefinition { name, total_supply };
|
||||
let definition_identity = definition_mention.key_path().map_or(
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: definition_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(definition_mention, definition_account_id, self.0)
|
||||
.and_then(|()| groups.add_required(supply_mention, supply_account_id, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let supply_identity = supply_mention.key_path().map_or(
|
||||
AccountIdentity::Public(supply_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: supply_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::token();
|
||||
let instruction = Instruction::NewFungibleDefinition { name, total_supply };
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&Program::token(), account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![definition_identity, supply_identity],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -48,14 +61,13 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(supply_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -84,11 +96,10 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(supply_account_id),
|
||||
AccountIdentity::Public(supply_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -123,7 +134,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -142,19 +152,35 @@ impl Token<'_> {
|
||||
sender_mention: &CliAccountMention,
|
||||
recipient_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let account_ids = vec![sender_account_id, recipient_account_id];
|
||||
let sender_identity = sender_mention.key_path().map_or(
|
||||
AccountIdentity::Public(sender_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: sender_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let recipient_identity = recipient_mention.key_path().map_or(
|
||||
AccountIdentity::Public(recipient_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: recipient_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::token();
|
||||
let instruction = Instruction::Transfer {
|
||||
amount_to_transfer: amount,
|
||||
};
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(sender_mention, sender_account_id, self.0)
|
||||
.and_then(|()| groups.add_optional(recipient_mention, recipient_account_id, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&Program::token(), account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![sender_identity, recipient_identity],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -182,7 +208,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -213,7 +238,7 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(sender_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
vpk: recipient_vpk,
|
||||
identifier: recipient_identifier,
|
||||
@ -221,7 +246,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -250,11 +274,10 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(sender_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(recipient_account_id),
|
||||
AccountIdentity::Public(recipient_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -273,6 +296,14 @@ impl Token<'_> {
|
||||
amount: u128,
|
||||
sender_mention: &CliAccountMention,
|
||||
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
|
||||
let sender_identity = sender_mention.key_path().map_or(
|
||||
AccountIdentity::Public(sender_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: sender_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let instruction = Instruction::Transfer {
|
||||
amount_to_transfer: amount,
|
||||
};
|
||||
@ -281,14 +312,13 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender_account_id),
|
||||
sender_identity,
|
||||
self.0
|
||||
.resolve_private_account(recipient_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
Some(sender_mention),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -309,6 +339,14 @@ impl Token<'_> {
|
||||
amount: u128,
|
||||
sender_mention: &CliAccountMention,
|
||||
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
|
||||
let sender_identity = sender_mention.key_path().map_or(
|
||||
AccountIdentity::Public(sender_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: sender_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let instruction = Instruction::Transfer {
|
||||
amount_to_transfer: amount,
|
||||
};
|
||||
@ -317,8 +355,8 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
sender_identity,
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
vpk: recipient_vpk,
|
||||
identifier: recipient_identifier,
|
||||
@ -326,7 +364,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
Some(sender_mention),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -345,18 +382,30 @@ impl Token<'_> {
|
||||
amount: u128,
|
||||
holder_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let account_ids = vec![definition_account_id, holder_account_id];
|
||||
let holder_identity = holder_mention.key_path().map_or(
|
||||
AccountIdentity::Public(holder_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: holder_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::token();
|
||||
let instruction = Instruction::Burn {
|
||||
amount_to_burn: amount,
|
||||
};
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(holder_mention, holder_account_id, self.0)
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&Program::token(), account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::PublicNoSign(definition_account_id),
|
||||
holder_identity,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -384,7 +433,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -413,11 +461,10 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_account_id),
|
||||
AccountIdentity::Public(holder_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -444,14 +491,13 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -471,19 +517,35 @@ impl Token<'_> {
|
||||
definition_mention: &CliAccountMention,
|
||||
holder_mention: &CliAccountMention,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let account_ids = vec![definition_account_id, holder_account_id];
|
||||
let definition_identity = definition_mention.key_path().map_or(
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: definition_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let holder_identity = holder_mention.key_path().map_or(
|
||||
AccountIdentity::Public(holder_account_id),
|
||||
|key_path| AccountIdentity::PublicKeycard {
|
||||
account_id: holder_account_id,
|
||||
key_path: key_path.to_owned(),
|
||||
},
|
||||
);
|
||||
|
||||
let program = Program::token();
|
||||
let instruction = Instruction::Mint {
|
||||
amount_to_mint: amount,
|
||||
};
|
||||
|
||||
let mut groups = SigningGroup::new();
|
||||
groups
|
||||
.add_required(definition_mention, definition_account_id, self.0)
|
||||
.and_then(|()| groups.add_optional(holder_mention, holder_account_id, self.0))
|
||||
.map_err(ExecutionFailureKind::from_anyhow)?;
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_public_tx(&Program::token(), account_ids, instruction, groups)
|
||||
.send_pub_tx(
|
||||
vec![definition_identity, holder_identity],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -511,7 +573,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -542,7 +603,7 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: holder_npk,
|
||||
vpk: holder_vpk,
|
||||
identifier: holder_identifier,
|
||||
@ -550,7 +611,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -579,11 +639,10 @@ impl Token<'_> {
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_account_id),
|
||||
AccountIdentity::Public(holder_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -610,14 +669,13 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
@ -646,8 +704,8 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
AccountIdentity::Public(definition_account_id),
|
||||
AccountIdentity::PrivateForeign {
|
||||
npk: holder_npk,
|
||||
vpk: holder_vpk,
|
||||
identifier: holder_identifier,
|
||||
@ -655,7 +713,6 @@ impl Token<'_> {
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
|
||||
@ -1,119 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use keycard_wallet::{KeycardWallet, python_path};
|
||||
use nssa::{AccountId, PrivateKey, PublicKey, Signature};
|
||||
use pyo3::Python;
|
||||
|
||||
use crate::{WalletCore, cli::CliAccountMention};
|
||||
|
||||
/// Groups transaction signers by type to minimise Python GIL acquisition.
|
||||
///
|
||||
/// Local signers are signed in pure Rust; all keycard signers share a single Python session
|
||||
/// with one `connect` / `close_session` pair.
|
||||
#[derive(Default)]
|
||||
pub struct SigningGroup {
|
||||
local: Vec<(AccountId, PrivateKey)>,
|
||||
keycard: Vec<(AccountId, String)>,
|
||||
}
|
||||
|
||||
impl SigningGroup {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add a sender. Keycard paths are queued for the hardware session; local accounts
|
||||
/// have their signing key resolved eagerly. Errors if no key is found.
|
||||
pub fn add_required(
|
||||
&mut self,
|
||||
mention: &CliAccountMention,
|
||||
account_id: AccountId,
|
||||
wallet_core: &WalletCore,
|
||||
) -> Result<()> {
|
||||
if let CliAccountMention::KeyPath(path) = mention {
|
||||
self.keycard.push((account_id, path.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
let key = wallet_core
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(account_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("signing key not found for account {account_id}"))?
|
||||
.clone();
|
||||
self.local.push((account_id, key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a recipient. Same as [`add_required`] but silently skips accounts with no local
|
||||
/// key and no keycard path — they are foreign and require neither a signature nor a nonce.
|
||||
pub fn add_optional(
|
||||
&mut self,
|
||||
mention: &CliAccountMention,
|
||||
account_id: AccountId,
|
||||
wallet_core: &WalletCore,
|
||||
) -> Result<()> {
|
||||
if let CliAccountMention::KeyPath(path) = mention {
|
||||
self.keycard.push((account_id, path.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(key) = wallet_core
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(account_id)
|
||||
{
|
||||
self.local.push((account_id, key.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` when a PIN is required (at least one keycard signer is present).
|
||||
#[must_use]
|
||||
pub const fn needs_pin(&self) -> bool {
|
||||
!self.keycard.is_empty()
|
||||
}
|
||||
|
||||
/// Account IDs that require a nonce (every non-foreign signer).
|
||||
#[must_use]
|
||||
pub fn signing_ids(&self) -> Vec<AccountId> {
|
||||
self.local
|
||||
.iter()
|
||||
.map(|(id, _)| *id)
|
||||
.chain(self.keycard.iter().map(|(id, _)| *id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sign `hash` for every account in the group.
|
||||
///
|
||||
/// Local accounts are signed in pure Rust. Keycard accounts share one Python session.
|
||||
pub fn sign_all(&self, hash: &[u8; 32], pin: &str) -> Result<Vec<(Signature, PublicKey)>> {
|
||||
let mut sigs: Vec<(Signature, PublicKey)> = self
|
||||
.local
|
||||
.iter()
|
||||
.map(|(_, key)| {
|
||||
(
|
||||
Signature::new(key, hash),
|
||||
PublicKey::new_from_private_key(key),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !self.keycard.is_empty() {
|
||||
pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = KeycardWallet::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
for (_, path) in &self.keycard {
|
||||
sigs.push(wallet.sign_message_for_path(py, path, hash)?);
|
||||
}
|
||||
drop(wallet.close_session(py));
|
||||
Ok(())
|
||||
})
|
||||
.map_err(anyhow::Error::from)?;
|
||||
}
|
||||
|
||||
Ok(sigs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction.
|
||||
pub struct KeycardSessionContext {
|
||||
pin: String,
|
||||
|
||||
@ -55,7 +55,7 @@ pub struct SharedAccountEntry {
|
||||
/// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`.
|
||||
/// `None` for regular shared accounts (keys derived from identifier via derivation seed).
|
||||
pub pda_seed: Option<nssa_core::program::PdaSeed>,
|
||||
pub pda_program_id: Option<nssa_core::program::ProgramId>,
|
||||
pub authority_program_id: Option<nssa_core::program::ProgramId>,
|
||||
pub account: Account,
|
||||
}
|
||||
|
||||
@ -858,7 +858,7 @@ mod tests {
|
||||
group_label: Label::new("test-group"),
|
||||
identifier: 42,
|
||||
pda_seed: None,
|
||||
pda_program_id: None,
|
||||
authority_program_id: None,
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let encoded = bincode::serialize(&entry).expect("serialize");
|
||||
@ -871,7 +871,7 @@ mod tests {
|
||||
group_label: Label::new("pda-group"),
|
||||
identifier: u128::MAX,
|
||||
pda_seed: Some(PdaSeed::new([7_u8; 32])),
|
||||
pda_program_id: Some([9; 8]),
|
||||
authority_program_id: Some([9; 8]),
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
|
||||
@ -890,7 +890,7 @@ mod tests {
|
||||
group_label: Label::new("old"),
|
||||
identifier: 1,
|
||||
pda_seed: None,
|
||||
pda_program_id: None,
|
||||
authority_program_id: None,
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let encoded = bincode::serialize(&entry).expect("serialize");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user