Skip to content
Open
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
14 changes: 14 additions & 0 deletions .cursor/rules/generated-artifacts.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Protect generated bindings and release-time translation catalogs
alwaysApply: true
---

# Generated Artifacts

- Never edit `src/bindings.ts` manually. Change its Rust/Specta source definitions instead.
- Never run Sage automatically to regenerate `src/bindings.ts`; Sage is a GUI application.
- Regenerate bindings with `pnpm generate:bindings` when needed; this command must not launch Sage.
- Release builds must never generate or modify `src/bindings.ts`.
- Do not run `pnpm extract` or modify `src/locales/**/*.po` during normal feature work.
- Update `.po` catalogs only when explicitly requested, typically as part of release preparation.
- If a task requires generated artifacts to be refreshed, report that follow-up rather than hand-editing them.
19 changes: 19 additions & 0 deletions .cursor/rules/validation.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
description: Required validation after code changes
alwaysApply: true
---

# Validation

Before completing substantive code changes, run the applicable checks for every language changed:

1. TypeScript formatting: `pnpm prettier:check`
2. TypeScript linting: `pnpm lint`
3. Rust formatting: `cargo fmt --all -- --files-with-diff --check`
4. Rust linting: `cargo clippy --workspace --all-features --all-targets`
5. Relevant targeted tests, expanded to broader test suites when warranted by the change's risk

- Run both TypeScript checks for TypeScript/TSX changes and both Rust checks for Rust changes.
- Builds, type checks, and IDE diagnostics do not replace formatting or linting.
- Do not claim validation passed unless the corresponding commands ran successfully.
- Report any skipped, blocked, or failing checks, including failures unrelated to the current changes.
4 changes: 4 additions & 0 deletions crates/sage-api/src/requests/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ pub struct IssueCat {
pub ticker: String,
/// Initial supply amount
pub amount: Amount,
/// Whether the CAT can be revoked by the issuer
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(default = false))]
pub revocable: bool,
/// Transaction fee
pub fee: Amount,
/// Whether to automatically submit the transaction
Expand Down
40 changes: 38 additions & 2 deletions crates/sage-wallet/src/wallet/cats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@ impl Wallet {
amount: u64,
fee: u64,
multi_issuance_key: Option<PublicKey>,
) -> Result<(Vec<CoinSpend>, Bytes32), WalletError> {
self.issue_cat_with_hidden_puzzle_hash(amount, fee, multi_issuance_key, None)
.await
}

pub async fn issue_cat_with_hidden_puzzle_hash(
&self,
amount: u64,
fee: u64,
multi_issuance_key: Option<PublicKey>,
hidden_puzzle_hash: Option<Bytes32>,
) -> Result<(Vec<CoinSpend>, Bytes32), WalletError> {
let mut ctx = SpendContext::new();

let issue_cat = if let Some(public_key) = multi_issuance_key {
let tail = ctx.curry(EverythingWithSignatureTailArgs::new(public_key))?;
let tail_spend = Spend::new(tail, NodePtr::NIL);
Action::issue_cat(tail_spend, None, amount)
Action::issue_cat(tail_spend, hidden_puzzle_hash, amount)
} else {
Action::single_issue_cat(None, amount)
Action::single_issue_cat(hidden_puzzle_hash, amount)
};
let actions = vec![Action::fee(fee), issue_cat];
let outputs = self.spend(&mut ctx, vec![], &actions).await?;
Expand Down Expand Up @@ -87,6 +98,31 @@ mod tests {

use crate::TestWallet;

#[test(tokio::test)]
async fn test_issue_revocable_cat() -> anyhow::Result<()> {
let mut test = TestWallet::new(1000).await?;
let hidden_puzzle_hash = test.wallet.change_p2_puzzle_hash().await?;

let (coin_spends, asset_id) = test
.wallet
.issue_cat_with_hidden_puzzle_hash(1000, 0, None, Some(hidden_puzzle_hash))
.await?;

test.transact(coin_spends).await?;
test.wait_for_coins().await;

assert_eq!(
test.wallet
.db
.asset(asset_id)
.await?
.and_then(|asset| asset.hidden_puzzle_hash),
Some(hidden_puzzle_hash)
);

Ok(())
}

#[test(tokio::test)]
async fn test_send_cat() -> anyhow::Result<()> {
let mut test = TestWallet::new(1500).await?;
Expand Down
11 changes: 9 additions & 2 deletions crates/sage/src/endpoints/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,15 @@ impl Sage {
let wallet = self.wallet()?;
let amount = parse_amount(req.amount)?;
let fee = parse_amount(req.fee)?;
let hidden_puzzle_hash = if req.revocable {
Some(wallet.change_p2_puzzle_hash().await?)
} else {
None
};

let (coin_spends, asset_id) = wallet.issue_cat(amount, fee, None).await?;
let (coin_spends, asset_id) = wallet
.issue_cat_with_hidden_puzzle_hash(amount, fee, None, hidden_puzzle_hash)
.await?;
let mut tx = wallet.db.tx().await?;

tx.insert_asset(Asset {
Expand All @@ -170,7 +177,7 @@ impl Sage {
description: None,
is_sensitive_content: false,
is_visible: true,
hidden_puzzle_hash: None,
hidden_puzzle_hash,
kind: AssetKind::Token,
})
.await?;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"dev": "vite",
"dev:system-apps": "node scripts/dev-system-apps-watch.mjs",
"generate:bindings": "cargo run -p sage-tauri --bin generate-bindings",
"generate:bridge-types": "node scripts/generate-bridge-types.mjs",
"generate:app-docs": "cargo run -p sage-apps --bin generate_docs",
"generate:sdk-types": "pnpm --silent --filter @sage-app/sdk run generate:types && pnpm --silent --filter @sage-system-app/sdk run generate:types",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "sage-tauri"
version = "0.12.11"
default-run = "sage-tauri"
description = "A next generation Chia wallet."
authors = ["Rigidity <me@rigidnetwork.com>"]
license = "Apache-2.0"
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/bin/generate-bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#[cfg(debug_assertions)]
fn main() {
sage_lib::export_bindings();
}

#[cfg(not(debug_assertions))]
fn main() {
eprintln!("Binding generation is disabled in release builds.");
std::process::exit(1);
}
46 changes: 29 additions & 17 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(all(debug_assertions, not(mobile)))]
use std::path::PathBuf;

use app_state::{AppState, Initialized, RpcTask};
use rustls::crypto::aws_lc_rs::default_provider;
use sage::Sage;
Expand Down Expand Up @@ -150,14 +153,9 @@ macro_rules! sage_commands {
};
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
default_provider()
.install_default()
.expect("could not install AWS LC provider");

#[cfg(not(mobile))]
let builder = Builder::<tauri::Wry>::new()
#[cfg(not(mobile))]
fn specta_builder() -> Builder<tauri::Wry> {
Builder::<tauri::Wry>::new()
.error_handling(ErrorHandlingMode::Throw)
.commands(sage_commands![
apps::apps_enter_workspace,
Expand All @@ -183,21 +181,35 @@ pub fn run() {
apps::apps_get_auto_update_enabled,
apps::apps_set_auto_update_enabled,
])
.events(collect_events![SyncEvent]);
.events(collect_events![SyncEvent])
}

#[cfg(mobile)]
let builder = Builder::<tauri::Wry>::new()
.error_handling(ErrorHandlingMode::Throw)
.commands(sage_commands![])
.events(collect_events![SyncEvent]);
#[cfg(all(debug_assertions, not(mobile)))]
pub fn export_bindings() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../src/bindings.ts");

#[cfg(all(debug_assertions, not(mobile)))]
builder
specta_builder()
.export(
Typescript::default().bigint(BigIntExportBehavior::Number),
"../src/bindings.ts",
path,
)
.expect("Failed to export TypeScript bindings");
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
default_provider()
.install_default()
.expect("could not install AWS LC provider");

#[cfg(not(mobile))]
let builder = specta_builder();

#[cfg(mobile)]
let builder = Builder::<tauri::Wry>::new()
.error_handling(ErrorHandlingMode::Throw)
.commands(sage_commands![])
.events(collect_events![SyncEvent]);

let mut tauri_builder = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
Expand Down
4 changes: 4 additions & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,10 @@ ticker: string;
* Initial supply amount
*/
amount: Amount;
/**
* Whether the CAT can be revoked by the issuer
*/
revocable?: boolean;
/**
* Transaction fee
*/
Expand Down
55 changes: 46 additions & 9 deletions src/components/confirmations/TokenConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { CoinRecord } from '@/bindings';
import { CopyButton } from '@/components/CopyButton';
import { MemoDisplay } from '@/components/MemoDisplay.tsx';
import { formatNumber } from '@/i18n.ts';
import { fromMojos } from '@/lib/utils';
import { formatMemo, Memo } from '@/types/CoinMemo.ts';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { CoinsIcon, MergeIcon, SplitIcon } from 'lucide-react';
import {
CoinsIcon,
MergeIcon,
SplitIcon,
TriangleAlertIcon,
} from 'lucide-react';
import { toast } from 'react-toastify';
import { formatNumber } from '@/i18n.ts';
import { ConfirmationAlert } from './ConfirmationAlert';
import { ConfirmationCard } from './ConfirmationCard';
import { formatMemo, Memo } from '@/types/CoinMemo.ts';
import { MemoDisplay } from '@/components/MemoDisplay.tsx';

type TokenOperationType =
| 'split'
Expand All @@ -27,6 +32,7 @@ interface TokenConfirmationProps {
precision?: number;
name?: string;
amount?: string;
revocable?: boolean;
currentMemo?: Memo;
}

Expand All @@ -38,6 +44,7 @@ export function TokenConfirmation({
precision,
name,
amount,
revocable,
currentMemo,
}: TokenConfirmationProps) {
const config = {
Expand Down Expand Up @@ -68,10 +75,18 @@ export function TokenConfirmation({
title: <Trans>Token Issuance</Trans>,
variant: 'info' as const,
message: (
<Trans>
You are issuing a new token. This will create a CAT (Chia Asset Token)
that can be sent to other users and traded on exchanges.
</Trans>
<>
<Trans>
You are issuing a new token. This will create a CAT (Chia Asset
Token) that can be sent to other users and traded on exchanges.
</Trans>{' '}
{revocable && (
<Trans>
This wallet&apos;s change address will be used as the revocation
address.
</Trans>
)}
</>
),
},
clawback: {
Expand Down Expand Up @@ -124,6 +139,19 @@ export function TokenConfirmation({
</ConfirmationAlert>
)}

{type === 'issue' && revocable && (
<ConfirmationAlert
icon={TriangleAlertIcon}
title={<Trans>Revocable CAT Warning</Trans>}
variant='warning'
>
<Trans>
Only issue a revocable CAT if you understand the risks. Sage cannot
revoke it; revocation currently requires external tools.
</Trans>
</ConfirmationAlert>
)}

{type === 'send' && currentMemo && (
<ConfirmationCard>
<div className='flex items-center justify-between'>
Expand All @@ -144,7 +172,7 @@ export function TokenConfirmation({
icon={<CoinsIcon className='h-8 w-8 text-blue-500' />}
title={name}
>
<div className='grid grid-cols-2 gap-2'>
<div className='grid grid-cols-3 gap-2'>
<div>
<div className='text-muted-foreground text-xs mb-1'>
<Trans>Ticker</Trans>
Expand All @@ -166,6 +194,15 @@ export function TokenConfirmation({
{ticker}
</div>
</div>

<div>
<div className='text-muted-foreground text-xs mb-1'>
<Trans>Revocable</Trans>
</div>
<div className='font-medium'>
{revocable ? <Trans>Yes</Trans> : <Trans>No</Trans>}
</div>
</div>
</div>
</ConfirmationCard>
)}
Expand Down
Loading
Loading