Skip to content

Commit

Permalink
Add simplified and JSON bundle views
Browse files Browse the repository at this point in the history
  • Loading branch information
Rigidity committed Oct 29, 2024
1 parent 6ab8cbe commit 747a72b
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 104 deletions.
22 changes: 21 additions & 1 deletion crates/sage-api/src/records/transaction_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,27 @@ use crate::Amount;
pub struct TransactionSummary {
pub fee: Amount,
pub inputs: Vec<Input>,
pub data: String,
pub coin_spends: Vec<CoinSpendJson>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct SpendBundleJson {
pub coin_spends: Vec<CoinSpendJson>,
pub aggregated_signature: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct CoinSpendJson {
pub coin: CoinJson,
pub puzzle_reveal: String,
pub solution: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct CoinJson {
pub parent_coin_info: String,
pub puzzle_hash: String,
pub amount: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@tanstack/react-table": "^8.20.5",
"@tauri-apps/api": "2.0.0-rc.4",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
Expand Down
32 changes: 32 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ bigdecimal = { workspace = true }
bech32 = { workspace = true }
base64 = { workspace = true }
tauri-plugin-shell = { workspace = true }
serde_json = { workspace = true }
serde_json = { workspace = true, features = ["arbitrary_precision"] }

# This is to ensure that the bindgen feature is enabled for the aws-lc-rs crate.
# https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms
Expand Down
125 changes: 101 additions & 24 deletions src-tauri/src/commands/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ use std::{collections::HashMap, time::Duration};

use base64::prelude::*;
use chia::{
protocol::{Bytes32, CoinSpend},
bls::Signature,
protocol::{Bytes32, Coin, CoinSpend, SpendBundle},
puzzles::nft::NftMetadata,
traits::Streamable,
};
use chia_wallet_sdk::{
decode_address, encode_address, AggSigConstants, MAINNET_CONSTANTS, TESTNET11_CONSTANTS,
};
use sage_api::{
Amount, BulkMintNfts, BulkMintNftsResponse, Input, InputKind, Output, TransactionSummary,
Amount, BulkMintNfts, BulkMintNftsResponse, CoinJson, CoinSpendJson, Input, InputKind, Output,
SpendBundleJson, TransactionSummary,
};
use sage_database::{CatRow, Database};
use sage_wallet::{
Expand Down Expand Up @@ -523,33 +524,21 @@ pub async fn transfer_nft(

#[command]
#[specta]
pub async fn submit_transaction(state: State<'_, AppState>, data: String) -> Result<()> {
pub async fn sign_transaction(
state: State<'_, AppState>,
coin_spends: Vec<CoinSpendJson>,
) -> Result<SpendBundleJson> {
let state = state.lock().await;
let wallet = state.wallet()?;
let data = hex::decode(data)?;
let coin_spends = Vec::<CoinSpend>::from_bytes_unchecked(&data)?;
submit(&state, &wallet, coin_spends).await
}

#[derive(Default)]
struct ConfirmationInfo {
did_names: HashMap<Bytes32, String>,
nft_data: HashMap<Bytes32, Data>,
}

async fn submit(
state: &MutexGuard<'_, AppStateInner>,
wallet: &Wallet,
coin_spends: Vec<CoinSpend>,
) -> Result<()> {
let (_mnemonic, Some(master_sk)) = state.keychain.extract_secrets(wallet.fingerprint, b"")?
else {
return Err(Error::no_secret_key());
};

let spend_bundle = wallet
.sign_transaction(
coin_spends,
coin_spends.iter().map(rust_spend).collect::<Result<_>>()?,
&if state.config.network.network_id == "mainnet" {
AggSigConstants::new(MAINNET_CONSTANTS.agg_sig_me_additional_data)
} else {
Expand All @@ -559,19 +548,39 @@ async fn submit(
)
.await?;

wallet.insert_transaction(spend_bundle).await?;
Ok(json_bundle(&spend_bundle))
}

#[command]
#[specta]
pub async fn submit_transaction(
state: State<'_, AppState>,
spend_bundle: SpendBundleJson,
) -> Result<()> {
let state = state.lock().await;
let wallet = state.wallet()?;

wallet
.insert_transaction(rust_bundle(&spend_bundle)?)
.await?;

Ok(())
}

#[derive(Default)]
struct ConfirmationInfo {
did_names: HashMap<Bytes32, String>,
nft_data: HashMap<Bytes32, Data>,
}

async fn summarize(
state: &MutexGuard<'_, AppStateInner>,
wallet: &Wallet,
coin_spends: Vec<CoinSpend>,
cache: ConfirmationInfo,
) -> Result<TransactionSummary> {
let data = coin_spends.to_bytes()?;
let transaction = Transaction::from_coin_spends(coin_spends).map_err(WalletError::Parse)?;
let transaction =
Transaction::from_coin_spends(coin_spends.clone()).map_err(WalletError::Parse)?;

let mut inputs = Vec::with_capacity(transaction.inputs.len());

Expand Down Expand Up @@ -670,7 +679,7 @@ async fn summarize(
Ok(TransactionSummary {
fee: Amount::from_mojos(transaction.fee as u128, state.unit.decimals),
inputs,
data: hex::encode(data),
coin_spends: coin_spends.iter().map(json_spend).collect(),
})
}

Expand Down Expand Up @@ -724,3 +733,71 @@ async fn extract_nft_data(

Ok(result)
}

fn json_bundle(spend_bundle: &SpendBundle) -> SpendBundleJson {
SpendBundleJson {
coin_spends: spend_bundle.coin_spends.iter().map(json_spend).collect(),
aggregated_signature: format!(
"0x{}",
hex::encode(spend_bundle.aggregated_signature.to_bytes())
),
}
}

fn json_spend(coin_spend: &CoinSpend) -> CoinSpendJson {
CoinSpendJson {
coin: json_coin(&coin_spend.coin),
puzzle_reveal: hex::encode(&coin_spend.puzzle_reveal),
solution: hex::encode(&coin_spend.solution),
}
}

fn json_coin(coin: &Coin) -> CoinJson {
CoinJson {
parent_coin_info: format!("0x{}", hex::encode(coin.parent_coin_info)),
puzzle_hash: format!("0x{}", hex::encode(coin.puzzle_hash)),
amount: coin.amount,
}
}

fn rust_bundle(spend_bundle: &SpendBundleJson) -> Result<SpendBundle> {
Ok(SpendBundle {
coin_spends: spend_bundle
.coin_spends
.iter()
.map(rust_spend)
.collect::<Result<_>>()?,
aggregated_signature: Signature::from_bytes(&decode_hex_sized(
&spend_bundle.aggregated_signature,
)?)?,
})
}

fn rust_spend(coin_spend: &CoinSpendJson) -> Result<CoinSpend> {
Ok(CoinSpend {
coin: rust_coin(&coin_spend.coin)?,
puzzle_reveal: decode_hex(&coin_spend.puzzle_reveal)?.into(),
solution: decode_hex(&coin_spend.solution)?.into(),
})
}

fn rust_coin(coin: &CoinJson) -> Result<Coin> {
Ok(Coin {
parent_coin_info: decode_hex_sized(&coin.parent_coin_info)?.into(),
puzzle_hash: decode_hex_sized(&coin.puzzle_hash)?.into(),
amount: coin.amount,
})
}

fn decode_hex(hex: &str) -> Result<Vec<u8>> {
if let Some(stripped) = hex.strip_prefix("0x") {
Ok(hex::decode(stripped)?)
} else {
Ok(hex::decode(hex)?)
}
}

fn decode_hex_sized<const N: usize>(hex: &str) -> Result<[u8; N]> {
let bytes = decode_hex(hex)?;
Ok(bytes.try_into()?)
}
9 changes: 9 additions & 0 deletions src-tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,12 @@ impl From<chia::traits::Error> for Error {
}
}
}

impl From<Vec<u8>> for Error {
fn from(value: Vec<u8>) -> Self {
Self {
kind: ErrorKind::Serialization,
reason: format!("Unexpected bytes with length {}", value.len()),
}
}
}
8 changes: 6 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod error;
mod models;

#[cfg(all(debug_assertions, not(mobile)))]
use specta_typescript::Typescript;
use specta_typescript::{BigIntExportBehavior, Typescript};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
Expand Down Expand Up @@ -70,6 +70,7 @@ pub fn run() {
commands::create_did,
commands::bulk_mint_nfts,
commands::transfer_nft,
commands::sign_transaction,
commands::submit_transaction,
// Peers
commands::get_peers,
Expand All @@ -81,7 +82,10 @@ pub fn run() {
// On mobile or release mode we should not export the TypeScript bindings
#[cfg(all(debug_assertions, not(mobile)))]
builder
.export(Typescript::default(), "../src/bindings.ts")
.export(
Typescript::default().bigint(BigIntExportBehavior::Number),
"../src/bindings.ts",
)
.expect("Failed to export TypeScript bindings");

tauri::Builder::default()
Expand Down
17 changes: 14 additions & 3 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,17 @@ async transferNft(nftId: string, address: string, fee: Amount) : Promise<Result<
else return { status: "error", error: e as any };
}
},
async submitTransaction(data: string) : Promise<Result<null, Error>> {
async signTransaction(coinSpends: CoinSpendJson[]) : Promise<Result<SpendBundleJson, Error>> {
try {
return { status: "ok", data: await TAURI_INVOKE("submit_transaction", { data }) };
return { status: "ok", data: await TAURI_INVOKE("sign_transaction", { coinSpends }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async submitTransaction(spendBundle: SpendBundleJson) : Promise<Result<null, Error>> {
try {
return { status: "ok", data: await TAURI_INVOKE("submit_transaction", { spendBundle }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
Expand Down Expand Up @@ -418,7 +426,9 @@ export type Amount = string
export type BulkMintNfts = { nft_mints: NftMint[]; did_id: string; fee: Amount }
export type BulkMintNftsResponse = { nft_ids: string[]; summary: TransactionSummary }
export type CatRecord = { asset_id: string; name: string | null; ticker: string | null; description: string | null; icon_url: string | null; visible: boolean; balance: Amount }
export type CoinJson = { parent_coin_info: string; puzzle_hash: string; amount: number }
export type CoinRecord = { coin_id: string; address: string; amount: Amount; created_height: number | null; spent_height: number | null; create_transaction_id: string | null; spend_transaction_id: string | null }
export type CoinSpendJson = { coin: CoinJson; puzzle_reveal: string; solution: string }
export type DidRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; created_height: number | null; create_transaction_id: string | null }
export type Error = { kind: ErrorKind; reason: string }
export type ErrorKind = "Io" | "Database" | "Client" | "Keychain" | "Logging" | "Serialization" | "InvalidAddress" | "InvalidMnemonic" | "InvalidKey" | "InvalidAmount" | "InvalidRoyalty" | "InvalidAssetId" | "InvalidLauncherId" | "InsufficientFunds" | "TransactionFailed" | "UnknownNetwork" | "UnknownFingerprint" | "NotLoggedIn" | "Sync" | "Wallet"
Expand All @@ -432,9 +442,10 @@ export type NftRecord = { launcher_id: string; launcher_id_hex: string; owner_di
export type Output = { coin_id: string; amount: Amount; address: string; receiving: boolean }
export type PeerRecord = { ip_addr: string; port: number; trusted: boolean; peak_height: number }
export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: string | null }
export type SpendBundleJson = { coin_spends: CoinSpendJson[]; aggregated_signature: string }
export type SyncEvent = { type: "start"; ip: string } | { type: "stop" } | { type: "subscribed" } | { type: "derivation" } | { type: "coin_state" } | { type: "puzzle_batch_synced" } | { type: "cat_info" } | { type: "did_info" } | { type: "nft_data" }
export type SyncStatus = { balance: Amount; unit: Unit; synced_coins: number; total_coins: number; receive_address: string }
export type TransactionSummary = { fee: Amount; inputs: Input[]; data: string }
export type TransactionSummary = { fee: Amount; inputs: Input[]; coin_spends: CoinSpendJson[] }
export type Unit = { ticker: string; decimals: number }
export type WalletConfig = { name?: string; derive_automatically?: boolean; derivation_batch_size?: number }
export type WalletInfo = { name: string; fingerprint: number; public_key: string; kind: WalletKind }
Expand Down
Loading

0 comments on commit 747a72b

Please sign in to comment.