Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simplified and JSON bundle views #62

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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