From 314fab67ea6cff0f3cf65fdafbffa4d6d9d17ee2 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 25 Dec 2025 09:11:34 -0600 Subject: [PATCH 01/14] Merge branch 'main' of https://github.com/xch-dev/sage --- ...411721ec4b428a5b0739300f927b609e985d8.json | 38 ------ ...d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json | 38 ++++++ Cargo.lock | 24 ++-- crates/sage-api/Cargo.toml | 2 +- crates/sage-api/endpoints.json | 1 + crates/sage-api/macro/Cargo.toml | 2 +- crates/sage-api/src/requests/data.rs | 2 +- crates/sage-api/src/requests/transactions.rs | 24 ++++ crates/sage-assets/Cargo.toml | 2 +- crates/sage-cli/Cargo.toml | 2 +- crates/sage-client/Cargo.toml | 2 +- crates/sage-config/Cargo.toml | 2 +- crates/sage-database/Cargo.toml | 2 +- crates/sage-database/src/tables/p2_puzzles.rs | 6 +- crates/sage-keychain/Cargo.toml | 2 +- crates/sage-rpc/Cargo.toml | 2 +- crates/sage-wallet/Cargo.toml | 2 +- crates/sage-wallet/src/error.rs | 6 + crates/sage-wallet/src/wallet.rs | 6 +- crates/sage-wallet/src/wallet/xch.rs | 86 ++++++++++++- crates/sage/Cargo.toml | 2 +- crates/sage/src/endpoints/transactions.rs | 18 ++- migrations/0005_clawback_coin_fix.sql | 8 ++ src-tauri/Cargo.toml | 2 +- src-tauri/gen/apple/project.yml | 4 +- src-tauri/gen/apple/sage-tauri_iOS/Info.plist | 4 +- src-tauri/src/lib.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/bindings.ts | 19 +++ src/components/ClawbackCoinsCard.tsx | 120 +++++++++++++++++- .../confirmations/TokenConfirmation.tsx | 40 +++++- src/locales/de-DE/messages.po | 106 +++++++++++----- src/locales/en-US/messages.po | 108 +++++++++++----- src/locales/es-MX/messages.po | 106 +++++++++++----- src/locales/zh-CN/messages.po | 106 +++++++++++----- src/pages/Token.tsx | 12 ++ 36 files changed, 682 insertions(+), 227 deletions(-) delete mode 100644 .sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json create mode 100644 .sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json create mode 100644 migrations/0005_clawback_coin_fix.sql diff --git a/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json b/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json deleted file mode 100644 index f6bc5bea6..000000000 --- a/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds\n FROM p2_puzzles\n INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id\n INNER JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)\n OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)\n LIMIT 1\n )\n WHERE p2_puzzles.hash = ?\n ", - "describe": { - "columns": [ - { - "name": "key", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "sender_puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "receiver_puzzle_hash", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "expiration_seconds", - "ordinal": 3, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8" -} diff --git a/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json b/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json new file mode 100644 index 000000000..e1ffb8d0e --- /dev/null +++ b/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT key AS 'key?', sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds\n FROM p2_puzzles\n INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id\n LEFT JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)\n OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)\n LIMIT 1\n )\n WHERE p2_puzzles.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "key?", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "sender_puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "receiver_puzzle_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "expiration_seconds", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4" +} diff --git a/Cargo.lock b/Cargo.lock index dabf0eff9..be527604b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5793,7 +5793,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "sage" -version = "0.12.6" +version = "0.12.7" dependencies = [ "base64 0.22.1", "bech32", @@ -5828,7 +5828,7 @@ dependencies = [ [[package]] name = "sage-api" -version = "0.12.6" +version = "0.12.7" dependencies = [ "sage-api-macro", "sage-config", @@ -5840,7 +5840,7 @@ dependencies = [ [[package]] name = "sage-api-macro" -version = "0.12.6" +version = "0.12.7" dependencies = [ "convert_case 0.8.0", "indexmap 2.11.4", @@ -5852,7 +5852,7 @@ dependencies = [ [[package]] name = "sage-assets" -version = "0.12.6" +version = "0.12.7" dependencies = [ "base64 0.22.1", "chia", @@ -5872,7 +5872,7 @@ dependencies = [ [[package]] name = "sage-cli" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "clap", @@ -5890,7 +5890,7 @@ dependencies = [ [[package]] name = "sage-client" -version = "0.12.6" +version = "0.12.7" dependencies = [ "dirs 5.0.1", "reqwest", @@ -5905,7 +5905,7 @@ dependencies = [ [[package]] name = "sage-config" -version = "0.12.6" +version = "0.12.7" dependencies = [ "chia", "chia-wallet-sdk", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "sage-database" -version = "0.12.6" +version = "0.12.7" dependencies = [ "chia", "chia-wallet-sdk", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "sage-keychain" -version = "0.12.6" +version = "0.12.7" dependencies = [ "aes-gcm", "argon2", @@ -5949,7 +5949,7 @@ dependencies = [ [[package]] name = "sage-rpc" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "axum", @@ -5969,7 +5969,7 @@ dependencies = [ [[package]] name = "sage-tauri" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "aws-lc-rs", @@ -6007,7 +6007,7 @@ dependencies = [ [[package]] name = "sage-wallet" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "chia", diff --git a/crates/sage-api/Cargo.toml b/crates/sage-api/Cargo.toml index 34817fc00..7f7fd840f 100644 --- a/crates/sage-api/Cargo.toml +++ b/crates/sage-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-api" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "API definitions for the Sage wallet." diff --git a/crates/sage-api/endpoints.json b/crates/sage-api/endpoints.json index 277ef307a..57b4ccc92 100644 --- a/crates/sage-api/endpoints.json +++ b/crates/sage-api/endpoints.json @@ -58,6 +58,7 @@ "mint_option": true, "transfer_options": true, "exercise_options": true, + "finalize_clawback": true, "sign_coin_spends": true, "view_coin_spends": true, "submit_transaction": true, diff --git a/crates/sage-api/macro/Cargo.toml b/crates/sage-api/macro/Cargo.toml index cfcd675b4..d592d096a 100644 --- a/crates/sage-api/macro/Cargo.toml +++ b/crates/sage-api/macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-api-macro" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A macro for implementing things for each of the Sage API endpoints." diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index b16754840..0eac3b347 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -204,7 +204,7 @@ pub struct GetVersion {} #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct GetVersionResponse { /// Semantic version string - #[cfg_attr(feature = "openapi", schema(example = "0.12.6"))] + #[cfg_attr(feature = "openapi", schema(example = "0.12.7"))] pub version: String, } diff --git a/crates/sage-api/src/requests/transactions.rs b/crates/sage-api/src/requests/transactions.rs index ddc932e34..2f21a67ff 100644 --- a/crates/sage-api/src/requests/transactions.rs +++ b/crates/sage-api/src/requests/transactions.rs @@ -696,6 +696,29 @@ pub struct TransferOptions { pub auto_submit: bool, } +/// Send CAT tokens to an address +#[cfg_attr( + feature = "openapi", + crate::openapi_attr( + tag = "XCH Transactions", + description = "Finalize the clawback for a set of coins.", + response_type = "TransactionResponse" + ) +)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct FinalizeClawback { + /// The coins to finalize the clawback for + pub coin_ids: Vec, + /// Transaction fee + pub fee: Amount, + /// Whether to automatically submit the transaction + #[serde(default)] + #[cfg_attr(feature = "openapi", schema(default = false))] + pub auto_submit: bool, +} + /// Sign coin spends to create a transaction #[cfg_attr( feature = "openapi", @@ -807,3 +830,4 @@ pub type TransferDidsResponse = TransactionResponse; pub type NormalizeDidsResponse = TransactionResponse; pub type TransferOptionsResponse = TransactionResponse; pub type ExerciseOptionsResponse = TransactionResponse; +pub type FinalizeClawbackResponse = TransactionResponse; diff --git a/crates/sage-assets/Cargo.toml b/crates/sage-assets/Cargo.toml index 1ea0acdbd..eb1b069bb 100644 --- a/crates/sage-assets/Cargo.toml +++ b/crates/sage-assets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-assets" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "Fetches non-critical data from various APIs for use in Sage wallet." diff --git a/crates/sage-cli/Cargo.toml b/crates/sage-cli/Cargo.toml index 952b4ed62..181e8b922 100644 --- a/crates/sage-cli/Cargo.toml +++ b/crates/sage-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-cli" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A CLI and RPC for Sage wallet." diff --git a/crates/sage-client/Cargo.toml b/crates/sage-client/Cargo.toml index 71daaf740..bdf7c8ec3 100644 --- a/crates/sage-client/Cargo.toml +++ b/crates/sage-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-client" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "An RPC client for Sage wallet." diff --git a/crates/sage-config/Cargo.toml b/crates/sage-config/Cargo.toml index ed0582e75..03e2f8e62 100644 --- a/crates/sage-config/Cargo.toml +++ b/crates/sage-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-config" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "Configuration for the Sage wallet." diff --git a/crates/sage-database/Cargo.toml b/crates/sage-database/Cargo.toml index 76b9b715c..f3e00a36d 100644 --- a/crates/sage-database/Cargo.toml +++ b/crates/sage-database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-database" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "The SQLite database for Sage." diff --git a/crates/sage-database/src/tables/p2_puzzles.rs b/crates/sage-database/src/tables/p2_puzzles.rs index 0ba66e16e..7c3f70896 100644 --- a/crates/sage-database/src/tables/p2_puzzles.rs +++ b/crates/sage-database/src/tables/p2_puzzles.rs @@ -25,7 +25,7 @@ pub enum P2Puzzle { #[derive(Debug, Clone, Copy)] pub struct Clawback { - pub public_key: PublicKey, + pub public_key: Option, pub sender_puzzle_hash: Bytes32, pub receiver_puzzle_hash: Bytes32, pub seconds: u64, @@ -490,10 +490,10 @@ async fn clawback(conn: impl SqliteExecutor<'_>, p2_puzzle_hash: Bytes32) -> Res let row = query!( " - SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds + SELECT key AS 'key?', sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds FROM p2_puzzles INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id - INNER JOIN public_keys ON public_keys.p2_puzzle_id IN ( + LEFT JOIN public_keys ON public_keys.p2_puzzle_id IN ( SELECT id FROM p2_puzzles WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds) OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds) diff --git a/crates/sage-keychain/Cargo.toml b/crates/sage-keychain/Cargo.toml index 47da88e79..ad8ee26c3 100644 --- a/crates/sage-keychain/Cargo.toml +++ b/crates/sage-keychain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-keychain" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A simple password based keychain implementation for Sage." diff --git a/crates/sage-rpc/Cargo.toml b/crates/sage-rpc/Cargo.toml index 8e8a40969..25f4a1851 100644 --- a/crates/sage-rpc/Cargo.toml +++ b/crates/sage-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-rpc" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "An RPC server for Sage wallet." diff --git a/crates/sage-wallet/Cargo.toml b/crates/sage-wallet/Cargo.toml index d62890fda..e38a244d1 100644 --- a/crates/sage-wallet/Cargo.toml +++ b/crates/sage-wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-wallet" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "The driver code and sync logic for Sage wallet." diff --git a/crates/sage-wallet/src/error.rs b/crates/sage-wallet/src/error.rs index 3c6db9309..290e772f7 100644 --- a/crates/sage-wallet/src/error.rs +++ b/crates/sage-wallet/src/error.rs @@ -117,6 +117,12 @@ pub enum WalletError { #[error("Unsupported underlying coin kind: {0:?}")] UnsupportedUnderlyingCoinKind(CoinKind), + #[error("Unsupported clawback coin kind: {0:?}")] + UnsupportedClawbackCoinKind(CoinKind), + + #[error("Cannot find clawback info for coin with id {0}")] + MissingClawbackInfo(Bytes32), + #[error("Try from int error: {0}")] TryFromInt(#[from] TryFromIntError), } diff --git a/crates/sage-wallet/src/wallet.rs b/crates/sage-wallet/src/wallet.rs index 733a95725..ef5a33e84 100644 --- a/crates/sage-wallet/src/wallet.rs +++ b/crates/sage-wallet/src/wallet.rs @@ -309,7 +309,11 @@ impl Wallet { P2Puzzle::PublicKey(public_key) => StandardLayer::new(*public_key) .spend_with_conditions(ctx, spend.finish()), P2Puzzle::Clawback(clawback) => { - let custody = StandardLayer::new(clawback.public_key); + let Some(public_key) = clawback.public_key else { + return Err(DriverError::MissingKey); + }; + + let custody = StandardLayer::new(public_key); let spend = custody.spend_with_conditions(ctx, spend.finish())?; let clawback = ClawbackV2::new( diff --git a/crates/sage-wallet/src/wallet/xch.rs b/crates/sage-wallet/src/wallet/xch.rs index 11b346d77..17b27dc4f 100644 --- a/crates/sage-wallet/src/wallet/xch.rs +++ b/crates/sage-wallet/src/wallet/xch.rs @@ -2,7 +2,11 @@ use chia::{ clvm_utils::ToTreeHash, protocol::{Bytes, Bytes32, CoinSpend}, }; -use chia_wallet_sdk::driver::{Action, ClawbackV2, Id, SpendContext}; +use chia_wallet_sdk::{ + driver::{Action, Cat, CatSpend, ClawbackV2, Id, SpendContext}, + prelude::AssertConcurrentSpend, +}; +use sage_database::{CoinKind, P2Puzzle}; use crate::{ wallet::memos::{calculate_memos, Hint}, @@ -53,6 +57,86 @@ impl Wallet { Ok(ctx.take()) } + + pub async fn finalize_clawback( + &self, + coin_ids: Vec, + fee: u64, + ) -> Result, WalletError> { + let mut ctx = SpendContext::new(); + + for &coin_id in &coin_ids { + let Some(coin_kind) = self.db.coin_kind(coin_id).await? else { + return Err(WalletError::MissingCoin(coin_id)); + }; + + match coin_kind { + CoinKind::Xch => { + let Some(coin) = self.db.xch_coin(coin_id).await? else { + return Err(WalletError::MissingXchCoin(coin_id)); + }; + + let P2Puzzle::Clawback(clawback) = self.db.p2_puzzle(coin.puzzle_hash).await? + else { + return Err(WalletError::MissingClawbackInfo(coin_id)); + }; + + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + coin.amount, + false, + ); + + clawback.push_through_coin_spend(&mut ctx, coin)?; + } + CoinKind::Cat => { + let Some(cat) = self.db.cat_coin(coin_id).await? else { + return Err(WalletError::MissingCatCoin(coin_id)); + }; + + let P2Puzzle::Clawback(clawback) = + self.db.p2_puzzle(cat.info.p2_puzzle_hash).await? + else { + return Err(WalletError::MissingClawbackInfo(coin_id)); + }; + + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + cat.coin.amount, + true, + ); + + let spend = clawback.push_through_spend(&mut ctx)?; + Cat::spend_all(&mut ctx, &[CatSpend::new(cat, spend)])?; + } + _ => { + return Err(WalletError::UnsupportedClawbackCoinKind(coin_kind)); + } + } + } + + if fee > 0 { + let actions = [Action::fee(fee)]; + + let mut spends = self.prepare_spends(&mut ctx, vec![], &actions).await?; + + for &coin_id in &coin_ids { + spends + .conditions + .required + .push(AssertConcurrentSpend::new(coin_id)); + } + + let deltas = spends.apply(&mut ctx, &actions)?; + self.complete_spends(&mut ctx, &deltas, spends).await?; + } + + Ok(ctx.take()) + } } #[cfg(test)] diff --git a/crates/sage/Cargo.toml b/crates/sage/Cargo.toml index a9c06a46f..2774300e4 100644 --- a/crates/sage/Cargo.toml +++ b/crates/sage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A high level abstraction for running Sage wallet." diff --git a/crates/sage/src/endpoints/transactions.rs b/crates/sage/src/endpoints/transactions.rs index 7ce47830f..25f0ae048 100644 --- a/crates/sage/src/endpoints/transactions.rs +++ b/crates/sage/src/endpoints/transactions.rs @@ -13,10 +13,11 @@ use itertools::Itertools; use sage_api::{ AddNftUri, AssignNftsToDid, AutoCombineCat, AutoCombineCatResponse, AutoCombineXch, AutoCombineXchResponse, BulkMintNfts, BulkMintNftsResponse, BulkSendCat, BulkSendXch, Combine, - CreateDid, ExerciseOptions, IssueCat, MintOption, MintOptionResponse, MultiSend, NftUriKind, - NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends, SignCoinSpendsResponse, Split, - SubmitTransaction, SubmitTransactionResponse, TransactionResponse, TransferDids, TransferNfts, - TransferOptions, ViewCoinSpends, ViewCoinSpendsResponse, + CreateDid, ExerciseOptions, FinalizeClawback, IssueCat, MintOption, MintOptionResponse, + MultiSend, NftUriKind, NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends, + SignCoinSpendsResponse, Split, SubmitTransaction, SubmitTransactionResponse, + TransactionResponse, TransferDids, TransferNfts, TransferOptions, ViewCoinSpends, + ViewCoinSpendsResponse, }; use sage_assets::fetch_uris_without_hash; use sage_database::{Asset, AssetKind}; @@ -552,6 +553,15 @@ impl Sage { self.transact(coin_spends, req.auto_submit).await } + pub async fn finalize_clawback(&self, req: FinalizeClawback) -> Result { + let wallet = self.wallet()?; + let coin_ids = parse_coin_ids(req.coin_ids)?; + let fee = parse_amount(req.fee)?; + + let coin_spends = wallet.finalize_clawback(coin_ids, fee).await?; + self.transact(coin_spends, req.auto_submit).await + } + pub async fn sign_coin_spends(&self, req: SignCoinSpends) -> Result { let coin_spends = req .coin_spends diff --git a/migrations/0005_clawback_coin_fix.sql b/migrations/0005_clawback_coin_fix.sql new file mode 100644 index 000000000..6ec11f6e5 --- /dev/null +++ b/migrations/0005_clawback_coin_fix.sql @@ -0,0 +1,8 @@ +DROP VIEW clawback_coins; + +CREATE VIEW clawback_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND spent_height IS NULL + AND clawback_expiration_seconds IS NOT NULL; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 66af530c8..8a33e1e0d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-tauri" -version = "0.12.6" +version = "0.12.7" description = "A next generation Chia wallet." authors = ["Rigidity "] license = "Apache-2.0" diff --git a/src-tauri/gen/apple/project.yml b/src-tauri/gen/apple/project.yml index d0548e523..936b1261a 100644 --- a/src-tauri/gen/apple/project.yml +++ b/src-tauri/gen/apple/project.yml @@ -52,8 +52,8 @@ targets: - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - CFBundleShortVersionString: 0.12.6 - CFBundleVersion: 0.12.6 + CFBundleShortVersionString: 0.12.7 + CFBundleVersion: 0.12.7 entitlements: path: sage-tauri_iOS/sage-tauri_iOS.entitlements scheme: diff --git a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist index 44f9e6010..8565587cd 100644 --- a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist +++ b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.12.6 + 0.12.7 CFBundleVersion - 0.12.6 + 0.12.7 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index abc653fcf..0b07633b4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,6 +55,7 @@ pub fn run() { commands::exercise_options, commands::add_nft_uri, commands::assign_nfts_to_did, + commands::finalize_clawback, commands::sign_coin_spends, commands::view_coin_spends, commands::submit_transaction, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 109a6bd38..910c1372b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Sage", - "version": "0.12.6", + "version": "0.12.7", "identifier": "com.rigidnetwork.sage", "build": { "frontendDist": "../dist", diff --git a/src/bindings.ts b/src/bindings.ts index e7a3dcd19..836353790 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -101,6 +101,9 @@ async addNftUri(req: AddNftUri) : Promise { async assignNftsToDid(req: AssignNftsToDid) : Promise { return await TAURI_INVOKE("assign_nfts_to_did", { req }); }, +async finalizeClawback(req: FinalizeClawback) : Promise { + return await TAURI_INVOKE("finalize_clawback", { req }); +}, async signCoinSpends(req: SignCoinSpends) : Promise { return await TAURI_INVOKE("sign_coin_spends", { req }); }, @@ -817,6 +820,22 @@ export type FilterUnlockedCoinsResponse = { * List of unlocked coin IDs */ coin_ids: string[] } +/** + * Send CAT tokens to an address + */ +export type FinalizeClawback = { +/** + * The coins to finalize the clawback for + */ +coin_ids: string[]; +/** + * Transaction fee + */ +fee: Amount; +/** + * Whether to automatically submit the transaction + */ +auto_submit?: boolean } /** * Generate a new mnemonic phrase for wallet creation */ diff --git a/src/components/ClawbackCoinsCard.tsx b/src/components/ClawbackCoinsCard.tsx index 166f1dfac..08e43df2d 100644 --- a/src/components/ClawbackCoinsCard.tsx +++ b/src/components/ClawbackCoinsCard.tsx @@ -26,7 +26,7 @@ import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { RowSelectionState } from '@tanstack/react-table'; import BigNumber from 'bignumber.js'; -import { UndoIcon, XIcon } from 'lucide-react'; +import { CheckIcon, UndoIcon, XIcon } from 'lucide-react'; import { Dispatch, SetStateAction, @@ -73,6 +73,10 @@ export function ClawbackCoinsCard({ const [sortMode, setSortMode] = useState('created_height'); const [sortDirection, setSortDirection] = useState(false); // false = descending, true = ascending const [includeSpentCoins, setIncludeSpentCoins] = useState(false); + const [canClawBack, setCanClawBack] = useState(false); + const [clawBackOpen, setClawBackOpen] = useState(false); + const [finalizeOpen, setFinalizeOpen] = useState(false); + const pageSize = 10; // Use ref to track current page to avoid dependency issues @@ -104,8 +108,6 @@ export function ClawbackCoinsCard({ }); }, [selectedCoinIds, coins]); - const [canClawBack, setCanClawBack] = useState(false); - useEffect(() => { let isMounted = true; @@ -123,7 +125,7 @@ export function ClawbackCoinsCard({ }); if (isMounted) { - setCanClawBack(selectedCoinIds.length > 0 && isSpendable.spendable); + setCanClawBack(isSpendable.spendable); } } catch (error) { console.error('Error checking if coins are spendable:', error); @@ -189,8 +191,6 @@ export function ClawbackCoinsCard({ updateCoins(currentPage); }, [currentPage, updateCoins]); - const [clawBackOpen, setClawBackOpen] = useState(false); - const clawBackFormSchema = z.object({ clawBackFee: amount(walletState.sync.unit.precision).refine( (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), @@ -219,7 +219,7 @@ export function ClawbackCoinsCard({ // Add confirmation data to the response const resultWithDetails = Object.assign({}, result, { additionalData: { - title: t`Claw back Details`, + title: t`Claw Back Details`, content: { type: 'clawback', coins: selectedCoinRecords, @@ -235,6 +235,50 @@ export function ClawbackCoinsCard({ .finally(() => setClawBackOpen(false)); }; + const finalizeFormSchema = z.object({ + finalizeFee: amount(walletState.sync.unit.precision).refine( + (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), + t`Not enough funds to cover the fee`, + ), + }); + + const finalizeForm = useForm>({ + resolver: zodResolver(finalizeFormSchema), + }); + + const onFinalizeSubmit = (values: z.infer) => { + const fee = toMojos(values.finalizeFee, walletState.sync.unit.precision); + + // Get IDs from the selected coin records + const coinIdsForRequest = selectedCoinRecords.map( + (record) => record.coin_id, + ); + + commands + .finalizeClawback({ + coin_ids: coinIdsForRequest, + fee, + }) + .then((result) => { + // Add confirmation data to the response + const resultWithDetails = Object.assign({}, result, { + additionalData: { + title: t`Finalize Clawback Details`, + content: { + type: 'finalize_clawback', + coins: selectedCoinRecords, + ticker: asset.ticker, + precision: asset.precision, + }, + }, + }); + + setResponse(resultWithDetails); + }) + .catch(addError) + .finally(() => setFinalizeOpen(false)); + }; + const pageCount = Math.ceil(totalCoins / pageSize); const selectedCoinCount = selectedCoinIds.length; const selectedCoinLabel = selectedCoinCount === 1 ? t`coin` : t`coins`; @@ -277,6 +321,17 @@ export function ClawbackCoinsCard({ Claw Back + + } /> @@ -342,6 +397,57 @@ export function ClawbackCoinsCard({ + + + + + + Finalize {asset.ticker} Clawback + + + + This will complete the clawback for all of the selected coins, + and send the funds to the original recipient (even if the + recipient wallet does not support clawbacks). + + + +
+ + ( + + + Network Fee + + + + + + + )} + /> + + + + + + +
+
); } diff --git a/src/components/confirmations/TokenConfirmation.tsx b/src/components/confirmations/TokenConfirmation.tsx index c2b609540..07373cb20 100644 --- a/src/components/confirmations/TokenConfirmation.tsx +++ b/src/components/confirmations/TokenConfirmation.tsx @@ -9,7 +9,13 @@ import { formatNumber } from '../../i18n'; import { ConfirmationAlert } from './ConfirmationAlert'; import { ConfirmationCard } from './ConfirmationCard'; -type TokenOperationType = 'split' | 'combine' | 'issue' | 'send' | 'clawback'; +type TokenOperationType = + | 'split' + | 'combine' + | 'issue' + | 'send' + | 'clawback' + | 'finalize_clawback'; interface TokenConfirmationProps { type: TokenOperationType; @@ -83,6 +89,18 @@ export function TokenConfirmation({ variant: 'info' as const, message: null, }, + finalize_clawback: { + icon: CoinsIcon, + title: Finalize Clawback, + variant: 'info' as const, + message: ( + + You are about to finalize the clawback transaction. This will send the + funds to the original recipient (even if the recipient wallet does not + support clawbacks). + + ), + }, }; const { icon: Icon, title, variant, message } = config[type]; @@ -150,7 +168,10 @@ export function TokenConfirmation({ )} - {(type === 'split' || type === 'combine' || type === 'clawback') && + {(type === 'split' || + type === 'combine' || + type === 'clawback' || + type === 'finalize_clawback') && coins && ( <> } @@ -209,12 +232,17 @@ export function TokenConfirmation({ {outputCount} coins ) : type === 'combine' ? ( 1 combined coin - ) : ( + ) : type === 'clawback' ? ( - {coins.length} clawed back coin {coins.length} coin + {coins.length} clawed back coin {coins.length === 1 ? '' : 's'} - )} + ) : type === 'finalize_clawback' ? ( + + {coins.length} finalized clawback + {coins.length === 1 ? '' : 's'} + + ) : null} diff --git a/src/locales/de-DE/messages.po b/src/locales/de-DE/messages.po index 3aedd963e..169c3ce55 100644 --- a/src/locales/de-DE/messages.po +++ b/src/locales/de-DE/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {Dadurch wird der Peer aus Ihrer Verbin msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Verhindert vorübergehend die Verbindung zum Peer.} other {Verhindert vorübergehend die Verbindung zu den ausgewählten Peers.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "" @@ -150,7 +158,7 @@ msgstr "{selectedCount} ausgewählt" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Betrag" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "Wenn Sie diese Funktion deaktivieren, erstellen Sie ein Cold Wallet, in #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "" @@ -812,7 +822,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "{ticker} zusammenfügen" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "" @@ -1185,7 +1195,7 @@ msgstr "Daten" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Neue Addresse" @@ -2409,7 +2437,8 @@ msgstr "Netzwerk" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "Nicht genügend Guthaben, um die Gebühr zu decken" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "" @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Output Anzahl" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "" @@ -3538,7 +3568,7 @@ msgstr "{ticker} senden" msgid "Send in bulk (airdrop)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "{ticker} splitten" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "" @@ -4215,7 +4245,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "" -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4227,6 +4257,10 @@ msgstr "Dadurch werden alle ausgewählten Coins zu einem einzigen kombiniert." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "" +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4289,7 +4323,7 @@ msgstr "Dadurch werden alle ausgewählten Coins aufgeteilt." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "" msgid "Token Icon" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "" @@ -4373,7 +4407,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "" @@ -4954,7 +4988,7 @@ msgstr "" msgid "You" msgstr "Sie" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4966,6 +5000,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "" @@ -4978,7 +5016,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "" @@ -4986,7 +5024,7 @@ msgstr "" msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "" @@ -5002,7 +5040,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "" diff --git a/src/locales/en-US/messages.po b/src/locales/en-US/messages.po index 1fb7d122d..31ab531d0 100644 --- a/src/locales/en-US/messages.po +++ b/src/locales/en-US/messages.po @@ -41,13 +41,21 @@ msgstr "{0, plural, one {You do not currently have any option contracts. Would y msgid "{0}" msgstr "{0}" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "{0} {1} coin{2}" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" -msgstr "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "{0} clawed back coin {1} coin{2}" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "{0} clawed back coin{1}" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" +msgstr "{0} finalized clawback{1}" #: src/components/MarketplaceCard.tsx:107 msgid "{0} link" @@ -89,7 +97,7 @@ msgstr "{label}: {content} (navigate within app)" msgid "{label}: {content} (opens in external application)" msgstr "{label}: {content} (opens in external application)" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "{outputCount} coins" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {This will remove the peer from your co msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "{selectedCoinCount} {selectedCoinLabel} selected" @@ -150,7 +158,7 @@ msgstr "{selectedCount} selected" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "1 combined coin" @@ -340,7 +348,7 @@ msgstr "All Levels" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Amount" @@ -412,7 +420,7 @@ msgstr "Asset can be revoked by the issuer" msgid "Asset ID" msgstr "Asset ID" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "By disabling this you are creating a cold wallet, with no ability to sig #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "Choose your preferred theme" msgid "Choose Your Theme" msgstr "Choose Your Theme" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "Claw Back" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "Claw Back {0}" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "Claw back Details" +#~ msgid "Claw back Details" +#~ msgstr "Claw back Details" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "Claw Back Details" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "Clawback Coins" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "Clear search" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "Clear Selection" @@ -781,7 +791,7 @@ msgstr "Close" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "coin" @@ -812,7 +822,7 @@ msgstr "Coin ID copied to clipboard" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "coins" @@ -897,7 +907,7 @@ msgstr "Combine {0}" #~ msgstr "Combine {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "Combine Coins" @@ -1185,7 +1195,7 @@ msgstr "Data" msgid "Data and License Details" msgstr "Data and License Details" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "Data copied to clipboard" @@ -1775,6 +1785,24 @@ msgstr "Fetching transactions..." msgid "Files Processed" msgstr "Files Processed" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "Finalize" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "Finalize {0} Clawback" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "Finalize Clawback" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "Finalize Clawback Details" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Fresh Address" @@ -2409,7 +2437,8 @@ msgstr "Network" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "Normalize Profiles" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "Not enough funds to cover the fee" @@ -2732,7 +2762,7 @@ msgstr "Offers Requesting This NFT" msgid "OK" msgstr "OK" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "Outline" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Output Count" @@ -3238,7 +3268,7 @@ msgstr "Requesting in exchange:" msgid "Require biometrics for sensitive actions" msgstr "Require biometrics for sensitive actions" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "Result" @@ -3538,7 +3568,7 @@ msgstr "Send {ticker}" msgid "Send in bulk (airdrop)" msgstr "Send in bulk (airdrop)" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "Send Token" @@ -3775,7 +3805,7 @@ msgstr "Split {0}" #~ msgstr "Split {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "Split Coins" @@ -4215,7 +4245,7 @@ msgstr "This will cancel all {0} active offers on-chain with transactions, preve msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "This will claw back all of the selected coins." @@ -4227,6 +4257,10 @@ msgstr "This will combine all of the selected coins into one." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "This will combine small enough coins automatically, so you don't have to manually select them." +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." @@ -4289,7 +4323,7 @@ msgstr "This will split all of the selected coins." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "Token Grid" msgid "Token Icon" msgstr "Token Icon" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "Token Issuance" @@ -4373,7 +4407,7 @@ msgstr "Tokens exported successfully" msgid "Tokens must have a positive amount." msgstr "Tokens must have a positive amount." -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "Total Amount" @@ -4954,7 +4988,7 @@ msgstr "with {syncedCoins} / {totalCoins} coins synced" msgid "You" msgstr "You" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "You are about to claw back coins. This will return them to your wallet." @@ -4966,6 +5000,10 @@ msgstr "You are about to create <0>{numberOfOffers} individual offers. Each msgid "You are about to create <0>1 offer." msgstr "You are about to create <0>1 offer." +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." @@ -4978,7 +5016,7 @@ msgstr "You are canceling {offerCount} offers on-chain. This will prevent them f msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." @@ -4986,7 +5024,7 @@ msgstr "You are combining multiple coins into a single coin. This can help reduc msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "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." @@ -5002,7 +5040,7 @@ msgstr "You Are Offering" msgid "You Are Requesting" msgstr "You Are Requesting" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." diff --git a/src/locales/es-MX/messages.po b/src/locales/es-MX/messages.po index 5fa1ffa17..92d06fd5b 100644 --- a/src/locales/es-MX/messages.po +++ b/src/locales/es-MX/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "{outputCount} monedas" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {Esto eliminará al nodo de tu conexió msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Evitará temporalmente que el nodo se conecte.} other {Evitará temporalmente que los nodos se conecten.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "{selectedCoinCount} {selectedCoinLabel} seleccionados" @@ -150,7 +158,7 @@ msgstr "{selectedCount} seleccionados" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "{transactionSpentCount} entradas, {transactionCreatedCount} salidas" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "1 moneda combinada" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Cantidad" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "ID de activo" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "Al deshabilitar esto, estás creando una billetera fría, sin capacidad #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "Limpiar búsqueda" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "Limpiar selección" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "moneda" @@ -812,7 +822,7 @@ msgstr "ID de moneda copiado al portapapeles" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "monedas" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "Combinar {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "Combinar Monedas" @@ -1185,7 +1195,7 @@ msgstr "Datos" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "Datos copiados al portapapeles" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Dirección nueva" @@ -2409,7 +2437,8 @@ msgstr "Red" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "Normalizar perfiles" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "No hay fondos suficientes para cubrir la tarifa" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "Una vez emitido, este token aparecerá en tu billetera. Luego, puedes enviarlo a otras direcciones o crear ofertas para intercambiarlo." @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Cantidad de salidas" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "Resultado" @@ -3538,7 +3568,7 @@ msgstr "Enviar {ticker}" msgid "Send in bulk (airdrop)" msgstr "Enviar en masa (airdrop)" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "Enviar Token" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "Dividir {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "Dividir Monedas" @@ -4215,7 +4245,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "Esto cancelará la oferta en la cadena con una transacción, evitando que sea aceptada incluso si alguien tiene el archivo de oferta original." -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4227,6 +4257,10 @@ msgstr "Esto combinará todas las monedas seleccionadas en una." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "Esto combinará automáticamente las monedas lo suficientemente pequeñas, así que no tienes que seleccionarlas manualmente." +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4289,7 +4323,7 @@ msgstr "Esto dividirá todas las monedas seleccionadas." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "Cuadrícula de tokens" msgid "Token Icon" msgstr "Icono de Token" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "Emisión de tokens" @@ -4373,7 +4407,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "Cantidad total" @@ -4954,7 +4988,7 @@ msgstr "con {syncedCoins} / {totalCoins} monedas sincronizadas" msgid "You" msgstr "Tú" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4966,6 +5000,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "Estás agregando una URL a este NFT. Esto se almacenará en la cadena de bloques y se puede utilizar para vincularse a contenido externo." @@ -4978,7 +5016,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "Estás cancelando esta oferta en la cadena de bloques. Esto evitará que sea aceptada, incluso si alguien tiene el archivo de oferta original." -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "Estás combinando varias monedas en una sola moneda. Esto puede ayudar a reducir la cantidad de monedas en tu billetera." @@ -4986,7 +5024,7 @@ msgstr "Estás combinando varias monedas en una sola moneda. Esto puede ayudar a msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "Estás creando un nuevo perfil. Esto generará un identificador descentralizado (DID) que se puede utilizar para asociar NFT y otros activos digitales con tu identidad." -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "Estás emitiendo un nuevo token. Esto creará un CAT (Token de Activos Chia) que se puede enviar a otros usuarios y negociar en bolsas de valores." @@ -5002,7 +5040,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "Estás dividiendo monedas en múltiples monedas de igual valor. Esto puede ayudar con transacciones paralelas y la creación de ofertas." diff --git a/src/locales/zh-CN/messages.po b/src/locales/zh-CN/messages.po index ff170aa5e..85e177c82 100644 --- a/src/locales/zh-CN/messages.po +++ b/src/locales/zh-CN/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {这将移除与该节点的连接. 如 msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {将暂时阻止与该节点的连接.} other {将暂时阻止与该节点的连接.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "" @@ -150,7 +158,7 @@ msgstr "已选择 {selectedCount}" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "金额" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "禁用此功能后, 将创建一个冷钱包, 该钱包无法签署交 #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "" @@ -812,7 +822,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "合并 {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "" @@ -1185,7 +1195,7 @@ msgstr "数据" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "刷新地址" @@ -2409,7 +2437,8 @@ msgstr "网络" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "余额不足以支付手续费" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "" @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "转出数量" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "" @@ -3538,7 +3568,7 @@ msgstr "发送 {ticker}" msgid "Send in bulk (airdrop)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "拆分 {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "" @@ -4214,7 +4244,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "" -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4226,6 +4256,10 @@ msgstr "这将把所有选中的币合并为一个." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "" +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4288,7 +4322,7 @@ msgstr "这将拆分所有选定的硬币." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "简称" @@ -4338,7 +4372,7 @@ msgstr "" msgid "Token Icon" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "" @@ -4372,7 +4406,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "" @@ -4952,7 +4986,7 @@ msgstr "" msgid "You" msgstr "我" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4964,6 +4998,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "" @@ -4976,7 +5014,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "" @@ -4984,7 +5022,7 @@ msgstr "" msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "" @@ -5000,7 +5038,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "" diff --git a/src/pages/Token.tsx b/src/pages/Token.tsx index 2f770bd0e..d932475ca 100644 --- a/src/pages/Token.tsx +++ b/src/pages/Token.tsx @@ -162,6 +162,18 @@ export default function Token() { /> ), }; + } else if (content.type === 'finalize_clawback') { + return { + title: t`Finalize Clawback Details`, + content: ( + + ), + }; } return undefined; From 6669ff7377327bc7debe5a7f4ab81330fd93fb9d Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 14 Jan 2026 19:40:21 -0600 Subject: [PATCH 02/14] Merge branch 'main' of https://github.com/xch-dev/sage --- ...411721ec4b428a5b0739300f927b609e985d8.json | 38 ------ ...d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json | 38 ++++++ Cargo.lock | 24 ++-- crates/sage-api/Cargo.toml | 2 +- crates/sage-api/endpoints.json | 1 + crates/sage-api/macro/Cargo.toml | 2 +- crates/sage-api/src/requests/data.rs | 2 +- crates/sage-api/src/requests/transactions.rs | 24 ++++ crates/sage-assets/Cargo.toml | 2 +- crates/sage-cli/Cargo.toml | 2 +- crates/sage-client/Cargo.toml | 2 +- crates/sage-config/Cargo.toml | 2 +- crates/sage-database/Cargo.toml | 2 +- crates/sage-database/src/tables/p2_puzzles.rs | 6 +- crates/sage-keychain/Cargo.toml | 2 +- crates/sage-rpc/Cargo.toml | 2 +- crates/sage-wallet/Cargo.toml | 2 +- crates/sage-wallet/src/error.rs | 6 + crates/sage-wallet/src/wallet.rs | 6 +- crates/sage-wallet/src/wallet/xch.rs | 86 ++++++++++++- crates/sage/Cargo.toml | 2 +- crates/sage/src/endpoints/transactions.rs | 18 ++- migrations/0005_clawback_coin_fix.sql | 8 ++ src-tauri/Cargo.toml | 2 +- src-tauri/gen/apple/project.yml | 4 +- src-tauri/gen/apple/sage-tauri_iOS/Info.plist | 4 +- src-tauri/src/lib.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/bindings.ts | 19 +++ src/components/ClawbackCoinsCard.tsx | 120 +++++++++++++++++- .../confirmations/TokenConfirmation.tsx | 40 +++++- src/locales/de-DE/messages.po | 106 +++++++++++----- src/locales/en-US/messages.po | 108 +++++++++++----- src/locales/es-MX/messages.po | 106 +++++++++++----- src/locales/zh-CN/messages.po | 106 +++++++++++----- src/pages/Token.tsx | 12 ++ 36 files changed, 682 insertions(+), 227 deletions(-) delete mode 100644 .sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json create mode 100644 .sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json create mode 100644 migrations/0005_clawback_coin_fix.sql diff --git a/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json b/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json deleted file mode 100644 index f6bc5bea6..000000000 --- a/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds\n FROM p2_puzzles\n INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id\n INNER JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)\n OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)\n LIMIT 1\n )\n WHERE p2_puzzles.hash = ?\n ", - "describe": { - "columns": [ - { - "name": "key", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "sender_puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "receiver_puzzle_hash", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "expiration_seconds", - "ordinal": 3, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8" -} diff --git a/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json b/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json new file mode 100644 index 000000000..e1ffb8d0e --- /dev/null +++ b/.sqlx/query-ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT key AS 'key?', sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds\n FROM p2_puzzles\n INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id\n LEFT JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)\n OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)\n LIMIT 1\n )\n WHERE p2_puzzles.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "key?", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "sender_puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "receiver_puzzle_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "expiration_seconds", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "ee23959d467ab94655046179f08d2b4c930aaaa584d3760ad0f8823ac1d4bfe4" +} diff --git a/Cargo.lock b/Cargo.lock index dabf0eff9..be527604b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5793,7 +5793,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "sage" -version = "0.12.6" +version = "0.12.7" dependencies = [ "base64 0.22.1", "bech32", @@ -5828,7 +5828,7 @@ dependencies = [ [[package]] name = "sage-api" -version = "0.12.6" +version = "0.12.7" dependencies = [ "sage-api-macro", "sage-config", @@ -5840,7 +5840,7 @@ dependencies = [ [[package]] name = "sage-api-macro" -version = "0.12.6" +version = "0.12.7" dependencies = [ "convert_case 0.8.0", "indexmap 2.11.4", @@ -5852,7 +5852,7 @@ dependencies = [ [[package]] name = "sage-assets" -version = "0.12.6" +version = "0.12.7" dependencies = [ "base64 0.22.1", "chia", @@ -5872,7 +5872,7 @@ dependencies = [ [[package]] name = "sage-cli" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "clap", @@ -5890,7 +5890,7 @@ dependencies = [ [[package]] name = "sage-client" -version = "0.12.6" +version = "0.12.7" dependencies = [ "dirs 5.0.1", "reqwest", @@ -5905,7 +5905,7 @@ dependencies = [ [[package]] name = "sage-config" -version = "0.12.6" +version = "0.12.7" dependencies = [ "chia", "chia-wallet-sdk", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "sage-database" -version = "0.12.6" +version = "0.12.7" dependencies = [ "chia", "chia-wallet-sdk", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "sage-keychain" -version = "0.12.6" +version = "0.12.7" dependencies = [ "aes-gcm", "argon2", @@ -5949,7 +5949,7 @@ dependencies = [ [[package]] name = "sage-rpc" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "axum", @@ -5969,7 +5969,7 @@ dependencies = [ [[package]] name = "sage-tauri" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "aws-lc-rs", @@ -6007,7 +6007,7 @@ dependencies = [ [[package]] name = "sage-wallet" -version = "0.12.6" +version = "0.12.7" dependencies = [ "anyhow", "chia", diff --git a/crates/sage-api/Cargo.toml b/crates/sage-api/Cargo.toml index 34817fc00..7f7fd840f 100644 --- a/crates/sage-api/Cargo.toml +++ b/crates/sage-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-api" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "API definitions for the Sage wallet." diff --git a/crates/sage-api/endpoints.json b/crates/sage-api/endpoints.json index 277ef307a..57b4ccc92 100644 --- a/crates/sage-api/endpoints.json +++ b/crates/sage-api/endpoints.json @@ -58,6 +58,7 @@ "mint_option": true, "transfer_options": true, "exercise_options": true, + "finalize_clawback": true, "sign_coin_spends": true, "view_coin_spends": true, "submit_transaction": true, diff --git a/crates/sage-api/macro/Cargo.toml b/crates/sage-api/macro/Cargo.toml index cfcd675b4..d592d096a 100644 --- a/crates/sage-api/macro/Cargo.toml +++ b/crates/sage-api/macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-api-macro" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A macro for implementing things for each of the Sage API endpoints." diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index b16754840..0eac3b347 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -204,7 +204,7 @@ pub struct GetVersion {} #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct GetVersionResponse { /// Semantic version string - #[cfg_attr(feature = "openapi", schema(example = "0.12.6"))] + #[cfg_attr(feature = "openapi", schema(example = "0.12.7"))] pub version: String, } diff --git a/crates/sage-api/src/requests/transactions.rs b/crates/sage-api/src/requests/transactions.rs index ddc932e34..2f21a67ff 100644 --- a/crates/sage-api/src/requests/transactions.rs +++ b/crates/sage-api/src/requests/transactions.rs @@ -696,6 +696,29 @@ pub struct TransferOptions { pub auto_submit: bool, } +/// Send CAT tokens to an address +#[cfg_attr( + feature = "openapi", + crate::openapi_attr( + tag = "XCH Transactions", + description = "Finalize the clawback for a set of coins.", + response_type = "TransactionResponse" + ) +)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct FinalizeClawback { + /// The coins to finalize the clawback for + pub coin_ids: Vec, + /// Transaction fee + pub fee: Amount, + /// Whether to automatically submit the transaction + #[serde(default)] + #[cfg_attr(feature = "openapi", schema(default = false))] + pub auto_submit: bool, +} + /// Sign coin spends to create a transaction #[cfg_attr( feature = "openapi", @@ -807,3 +830,4 @@ pub type TransferDidsResponse = TransactionResponse; pub type NormalizeDidsResponse = TransactionResponse; pub type TransferOptionsResponse = TransactionResponse; pub type ExerciseOptionsResponse = TransactionResponse; +pub type FinalizeClawbackResponse = TransactionResponse; diff --git a/crates/sage-assets/Cargo.toml b/crates/sage-assets/Cargo.toml index 1ea0acdbd..eb1b069bb 100644 --- a/crates/sage-assets/Cargo.toml +++ b/crates/sage-assets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-assets" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "Fetches non-critical data from various APIs for use in Sage wallet." diff --git a/crates/sage-cli/Cargo.toml b/crates/sage-cli/Cargo.toml index 952b4ed62..181e8b922 100644 --- a/crates/sage-cli/Cargo.toml +++ b/crates/sage-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-cli" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A CLI and RPC for Sage wallet." diff --git a/crates/sage-client/Cargo.toml b/crates/sage-client/Cargo.toml index 71daaf740..bdf7c8ec3 100644 --- a/crates/sage-client/Cargo.toml +++ b/crates/sage-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-client" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "An RPC client for Sage wallet." diff --git a/crates/sage-config/Cargo.toml b/crates/sage-config/Cargo.toml index ed0582e75..03e2f8e62 100644 --- a/crates/sage-config/Cargo.toml +++ b/crates/sage-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-config" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "Configuration for the Sage wallet." diff --git a/crates/sage-database/Cargo.toml b/crates/sage-database/Cargo.toml index 76b9b715c..f3e00a36d 100644 --- a/crates/sage-database/Cargo.toml +++ b/crates/sage-database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-database" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "The SQLite database for Sage." diff --git a/crates/sage-database/src/tables/p2_puzzles.rs b/crates/sage-database/src/tables/p2_puzzles.rs index 0ba66e16e..7c3f70896 100644 --- a/crates/sage-database/src/tables/p2_puzzles.rs +++ b/crates/sage-database/src/tables/p2_puzzles.rs @@ -25,7 +25,7 @@ pub enum P2Puzzle { #[derive(Debug, Clone, Copy)] pub struct Clawback { - pub public_key: PublicKey, + pub public_key: Option, pub sender_puzzle_hash: Bytes32, pub receiver_puzzle_hash: Bytes32, pub seconds: u64, @@ -490,10 +490,10 @@ async fn clawback(conn: impl SqliteExecutor<'_>, p2_puzzle_hash: Bytes32) -> Res let row = query!( " - SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds + SELECT key AS 'key?', sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds FROM p2_puzzles INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id - INNER JOIN public_keys ON public_keys.p2_puzzle_id IN ( + LEFT JOIN public_keys ON public_keys.p2_puzzle_id IN ( SELECT id FROM p2_puzzles WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds) OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds) diff --git a/crates/sage-keychain/Cargo.toml b/crates/sage-keychain/Cargo.toml index 47da88e79..ad8ee26c3 100644 --- a/crates/sage-keychain/Cargo.toml +++ b/crates/sage-keychain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-keychain" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A simple password based keychain implementation for Sage." diff --git a/crates/sage-rpc/Cargo.toml b/crates/sage-rpc/Cargo.toml index 8e8a40969..25f4a1851 100644 --- a/crates/sage-rpc/Cargo.toml +++ b/crates/sage-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-rpc" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "An RPC server for Sage wallet." diff --git a/crates/sage-wallet/Cargo.toml b/crates/sage-wallet/Cargo.toml index d62890fda..e38a244d1 100644 --- a/crates/sage-wallet/Cargo.toml +++ b/crates/sage-wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-wallet" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "The driver code and sync logic for Sage wallet." diff --git a/crates/sage-wallet/src/error.rs b/crates/sage-wallet/src/error.rs index 3c6db9309..290e772f7 100644 --- a/crates/sage-wallet/src/error.rs +++ b/crates/sage-wallet/src/error.rs @@ -117,6 +117,12 @@ pub enum WalletError { #[error("Unsupported underlying coin kind: {0:?}")] UnsupportedUnderlyingCoinKind(CoinKind), + #[error("Unsupported clawback coin kind: {0:?}")] + UnsupportedClawbackCoinKind(CoinKind), + + #[error("Cannot find clawback info for coin with id {0}")] + MissingClawbackInfo(Bytes32), + #[error("Try from int error: {0}")] TryFromInt(#[from] TryFromIntError), } diff --git a/crates/sage-wallet/src/wallet.rs b/crates/sage-wallet/src/wallet.rs index 733a95725..ef5a33e84 100644 --- a/crates/sage-wallet/src/wallet.rs +++ b/crates/sage-wallet/src/wallet.rs @@ -309,7 +309,11 @@ impl Wallet { P2Puzzle::PublicKey(public_key) => StandardLayer::new(*public_key) .spend_with_conditions(ctx, spend.finish()), P2Puzzle::Clawback(clawback) => { - let custody = StandardLayer::new(clawback.public_key); + let Some(public_key) = clawback.public_key else { + return Err(DriverError::MissingKey); + }; + + let custody = StandardLayer::new(public_key); let spend = custody.spend_with_conditions(ctx, spend.finish())?; let clawback = ClawbackV2::new( diff --git a/crates/sage-wallet/src/wallet/xch.rs b/crates/sage-wallet/src/wallet/xch.rs index 11b346d77..17b27dc4f 100644 --- a/crates/sage-wallet/src/wallet/xch.rs +++ b/crates/sage-wallet/src/wallet/xch.rs @@ -2,7 +2,11 @@ use chia::{ clvm_utils::ToTreeHash, protocol::{Bytes, Bytes32, CoinSpend}, }; -use chia_wallet_sdk::driver::{Action, ClawbackV2, Id, SpendContext}; +use chia_wallet_sdk::{ + driver::{Action, Cat, CatSpend, ClawbackV2, Id, SpendContext}, + prelude::AssertConcurrentSpend, +}; +use sage_database::{CoinKind, P2Puzzle}; use crate::{ wallet::memos::{calculate_memos, Hint}, @@ -53,6 +57,86 @@ impl Wallet { Ok(ctx.take()) } + + pub async fn finalize_clawback( + &self, + coin_ids: Vec, + fee: u64, + ) -> Result, WalletError> { + let mut ctx = SpendContext::new(); + + for &coin_id in &coin_ids { + let Some(coin_kind) = self.db.coin_kind(coin_id).await? else { + return Err(WalletError::MissingCoin(coin_id)); + }; + + match coin_kind { + CoinKind::Xch => { + let Some(coin) = self.db.xch_coin(coin_id).await? else { + return Err(WalletError::MissingXchCoin(coin_id)); + }; + + let P2Puzzle::Clawback(clawback) = self.db.p2_puzzle(coin.puzzle_hash).await? + else { + return Err(WalletError::MissingClawbackInfo(coin_id)); + }; + + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + coin.amount, + false, + ); + + clawback.push_through_coin_spend(&mut ctx, coin)?; + } + CoinKind::Cat => { + let Some(cat) = self.db.cat_coin(coin_id).await? else { + return Err(WalletError::MissingCatCoin(coin_id)); + }; + + let P2Puzzle::Clawback(clawback) = + self.db.p2_puzzle(cat.info.p2_puzzle_hash).await? + else { + return Err(WalletError::MissingClawbackInfo(coin_id)); + }; + + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + cat.coin.amount, + true, + ); + + let spend = clawback.push_through_spend(&mut ctx)?; + Cat::spend_all(&mut ctx, &[CatSpend::new(cat, spend)])?; + } + _ => { + return Err(WalletError::UnsupportedClawbackCoinKind(coin_kind)); + } + } + } + + if fee > 0 { + let actions = [Action::fee(fee)]; + + let mut spends = self.prepare_spends(&mut ctx, vec![], &actions).await?; + + for &coin_id in &coin_ids { + spends + .conditions + .required + .push(AssertConcurrentSpend::new(coin_id)); + } + + let deltas = spends.apply(&mut ctx, &actions)?; + self.complete_spends(&mut ctx, &deltas, spends).await?; + } + + Ok(ctx.take()) + } } #[cfg(test)] diff --git a/crates/sage/Cargo.toml b/crates/sage/Cargo.toml index a9c06a46f..2774300e4 100644 --- a/crates/sage/Cargo.toml +++ b/crates/sage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage" -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "Apache-2.0" description = "A high level abstraction for running Sage wallet." diff --git a/crates/sage/src/endpoints/transactions.rs b/crates/sage/src/endpoints/transactions.rs index 7ce47830f..25f0ae048 100644 --- a/crates/sage/src/endpoints/transactions.rs +++ b/crates/sage/src/endpoints/transactions.rs @@ -13,10 +13,11 @@ use itertools::Itertools; use sage_api::{ AddNftUri, AssignNftsToDid, AutoCombineCat, AutoCombineCatResponse, AutoCombineXch, AutoCombineXchResponse, BulkMintNfts, BulkMintNftsResponse, BulkSendCat, BulkSendXch, Combine, - CreateDid, ExerciseOptions, IssueCat, MintOption, MintOptionResponse, MultiSend, NftUriKind, - NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends, SignCoinSpendsResponse, Split, - SubmitTransaction, SubmitTransactionResponse, TransactionResponse, TransferDids, TransferNfts, - TransferOptions, ViewCoinSpends, ViewCoinSpendsResponse, + CreateDid, ExerciseOptions, FinalizeClawback, IssueCat, MintOption, MintOptionResponse, + MultiSend, NftUriKind, NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends, + SignCoinSpendsResponse, Split, SubmitTransaction, SubmitTransactionResponse, + TransactionResponse, TransferDids, TransferNfts, TransferOptions, ViewCoinSpends, + ViewCoinSpendsResponse, }; use sage_assets::fetch_uris_without_hash; use sage_database::{Asset, AssetKind}; @@ -552,6 +553,15 @@ impl Sage { self.transact(coin_spends, req.auto_submit).await } + pub async fn finalize_clawback(&self, req: FinalizeClawback) -> Result { + let wallet = self.wallet()?; + let coin_ids = parse_coin_ids(req.coin_ids)?; + let fee = parse_amount(req.fee)?; + + let coin_spends = wallet.finalize_clawback(coin_ids, fee).await?; + self.transact(coin_spends, req.auto_submit).await + } + pub async fn sign_coin_spends(&self, req: SignCoinSpends) -> Result { let coin_spends = req .coin_spends diff --git a/migrations/0005_clawback_coin_fix.sql b/migrations/0005_clawback_coin_fix.sql new file mode 100644 index 000000000..6ec11f6e5 --- /dev/null +++ b/migrations/0005_clawback_coin_fix.sql @@ -0,0 +1,8 @@ +DROP VIEW clawback_coins; + +CREATE VIEW clawback_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND spent_height IS NULL + AND clawback_expiration_seconds IS NOT NULL; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 66af530c8..8a33e1e0d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sage-tauri" -version = "0.12.6" +version = "0.12.7" description = "A next generation Chia wallet." authors = ["Rigidity "] license = "Apache-2.0" diff --git a/src-tauri/gen/apple/project.yml b/src-tauri/gen/apple/project.yml index d0548e523..936b1261a 100644 --- a/src-tauri/gen/apple/project.yml +++ b/src-tauri/gen/apple/project.yml @@ -52,8 +52,8 @@ targets: - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - CFBundleShortVersionString: 0.12.6 - CFBundleVersion: 0.12.6 + CFBundleShortVersionString: 0.12.7 + CFBundleVersion: 0.12.7 entitlements: path: sage-tauri_iOS/sage-tauri_iOS.entitlements scheme: diff --git a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist index 44f9e6010..8565587cd 100644 --- a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist +++ b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.12.6 + 0.12.7 CFBundleVersion - 0.12.6 + 0.12.7 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index abc653fcf..0b07633b4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,6 +55,7 @@ pub fn run() { commands::exercise_options, commands::add_nft_uri, commands::assign_nfts_to_did, + commands::finalize_clawback, commands::sign_coin_spends, commands::view_coin_spends, commands::submit_transaction, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 109a6bd38..910c1372b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Sage", - "version": "0.12.6", + "version": "0.12.7", "identifier": "com.rigidnetwork.sage", "build": { "frontendDist": "../dist", diff --git a/src/bindings.ts b/src/bindings.ts index e7a3dcd19..836353790 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -101,6 +101,9 @@ async addNftUri(req: AddNftUri) : Promise { async assignNftsToDid(req: AssignNftsToDid) : Promise { return await TAURI_INVOKE("assign_nfts_to_did", { req }); }, +async finalizeClawback(req: FinalizeClawback) : Promise { + return await TAURI_INVOKE("finalize_clawback", { req }); +}, async signCoinSpends(req: SignCoinSpends) : Promise { return await TAURI_INVOKE("sign_coin_spends", { req }); }, @@ -817,6 +820,22 @@ export type FilterUnlockedCoinsResponse = { * List of unlocked coin IDs */ coin_ids: string[] } +/** + * Send CAT tokens to an address + */ +export type FinalizeClawback = { +/** + * The coins to finalize the clawback for + */ +coin_ids: string[]; +/** + * Transaction fee + */ +fee: Amount; +/** + * Whether to automatically submit the transaction + */ +auto_submit?: boolean } /** * Generate a new mnemonic phrase for wallet creation */ diff --git a/src/components/ClawbackCoinsCard.tsx b/src/components/ClawbackCoinsCard.tsx index 166f1dfac..08e43df2d 100644 --- a/src/components/ClawbackCoinsCard.tsx +++ b/src/components/ClawbackCoinsCard.tsx @@ -26,7 +26,7 @@ import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { RowSelectionState } from '@tanstack/react-table'; import BigNumber from 'bignumber.js'; -import { UndoIcon, XIcon } from 'lucide-react'; +import { CheckIcon, UndoIcon, XIcon } from 'lucide-react'; import { Dispatch, SetStateAction, @@ -73,6 +73,10 @@ export function ClawbackCoinsCard({ const [sortMode, setSortMode] = useState('created_height'); const [sortDirection, setSortDirection] = useState(false); // false = descending, true = ascending const [includeSpentCoins, setIncludeSpentCoins] = useState(false); + const [canClawBack, setCanClawBack] = useState(false); + const [clawBackOpen, setClawBackOpen] = useState(false); + const [finalizeOpen, setFinalizeOpen] = useState(false); + const pageSize = 10; // Use ref to track current page to avoid dependency issues @@ -104,8 +108,6 @@ export function ClawbackCoinsCard({ }); }, [selectedCoinIds, coins]); - const [canClawBack, setCanClawBack] = useState(false); - useEffect(() => { let isMounted = true; @@ -123,7 +125,7 @@ export function ClawbackCoinsCard({ }); if (isMounted) { - setCanClawBack(selectedCoinIds.length > 0 && isSpendable.spendable); + setCanClawBack(isSpendable.spendable); } } catch (error) { console.error('Error checking if coins are spendable:', error); @@ -189,8 +191,6 @@ export function ClawbackCoinsCard({ updateCoins(currentPage); }, [currentPage, updateCoins]); - const [clawBackOpen, setClawBackOpen] = useState(false); - const clawBackFormSchema = z.object({ clawBackFee: amount(walletState.sync.unit.precision).refine( (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), @@ -219,7 +219,7 @@ export function ClawbackCoinsCard({ // Add confirmation data to the response const resultWithDetails = Object.assign({}, result, { additionalData: { - title: t`Claw back Details`, + title: t`Claw Back Details`, content: { type: 'clawback', coins: selectedCoinRecords, @@ -235,6 +235,50 @@ export function ClawbackCoinsCard({ .finally(() => setClawBackOpen(false)); }; + const finalizeFormSchema = z.object({ + finalizeFee: amount(walletState.sync.unit.precision).refine( + (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), + t`Not enough funds to cover the fee`, + ), + }); + + const finalizeForm = useForm>({ + resolver: zodResolver(finalizeFormSchema), + }); + + const onFinalizeSubmit = (values: z.infer) => { + const fee = toMojos(values.finalizeFee, walletState.sync.unit.precision); + + // Get IDs from the selected coin records + const coinIdsForRequest = selectedCoinRecords.map( + (record) => record.coin_id, + ); + + commands + .finalizeClawback({ + coin_ids: coinIdsForRequest, + fee, + }) + .then((result) => { + // Add confirmation data to the response + const resultWithDetails = Object.assign({}, result, { + additionalData: { + title: t`Finalize Clawback Details`, + content: { + type: 'finalize_clawback', + coins: selectedCoinRecords, + ticker: asset.ticker, + precision: asset.precision, + }, + }, + }); + + setResponse(resultWithDetails); + }) + .catch(addError) + .finally(() => setFinalizeOpen(false)); + }; + const pageCount = Math.ceil(totalCoins / pageSize); const selectedCoinCount = selectedCoinIds.length; const selectedCoinLabel = selectedCoinCount === 1 ? t`coin` : t`coins`; @@ -277,6 +321,17 @@ export function ClawbackCoinsCard({ Claw Back + + } /> @@ -342,6 +397,57 @@ export function ClawbackCoinsCard({ + + + + + + Finalize {asset.ticker} Clawback + + + + This will complete the clawback for all of the selected coins, + and send the funds to the original recipient (even if the + recipient wallet does not support clawbacks). + + + +
+ + ( + + + Network Fee + + + + + + + )} + /> + + + + + + +
+
); } diff --git a/src/components/confirmations/TokenConfirmation.tsx b/src/components/confirmations/TokenConfirmation.tsx index c2b609540..07373cb20 100644 --- a/src/components/confirmations/TokenConfirmation.tsx +++ b/src/components/confirmations/TokenConfirmation.tsx @@ -9,7 +9,13 @@ import { formatNumber } from '../../i18n'; import { ConfirmationAlert } from './ConfirmationAlert'; import { ConfirmationCard } from './ConfirmationCard'; -type TokenOperationType = 'split' | 'combine' | 'issue' | 'send' | 'clawback'; +type TokenOperationType = + | 'split' + | 'combine' + | 'issue' + | 'send' + | 'clawback' + | 'finalize_clawback'; interface TokenConfirmationProps { type: TokenOperationType; @@ -83,6 +89,18 @@ export function TokenConfirmation({ variant: 'info' as const, message: null, }, + finalize_clawback: { + icon: CoinsIcon, + title: Finalize Clawback, + variant: 'info' as const, + message: ( + + You are about to finalize the clawback transaction. This will send the + funds to the original recipient (even if the recipient wallet does not + support clawbacks). + + ), + }, }; const { icon: Icon, title, variant, message } = config[type]; @@ -150,7 +168,10 @@ export function TokenConfirmation({
)} - {(type === 'split' || type === 'combine' || type === 'clawback') && + {(type === 'split' || + type === 'combine' || + type === 'clawback' || + type === 'finalize_clawback') && coins && ( <> } @@ -209,12 +232,17 @@ export function TokenConfirmation({ {outputCount} coins ) : type === 'combine' ? ( 1 combined coin - ) : ( + ) : type === 'clawback' ? ( - {coins.length} clawed back coin {coins.length} coin + {coins.length} clawed back coin {coins.length === 1 ? '' : 's'} - )} + ) : type === 'finalize_clawback' ? ( + + {coins.length} finalized clawback + {coins.length === 1 ? '' : 's'} + + ) : null} diff --git a/src/locales/de-DE/messages.po b/src/locales/de-DE/messages.po index 3aedd963e..169c3ce55 100644 --- a/src/locales/de-DE/messages.po +++ b/src/locales/de-DE/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {Dadurch wird der Peer aus Ihrer Verbin msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Verhindert vorübergehend die Verbindung zum Peer.} other {Verhindert vorübergehend die Verbindung zu den ausgewählten Peers.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "" @@ -150,7 +158,7 @@ msgstr "{selectedCount} ausgewählt" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Betrag" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "Wenn Sie diese Funktion deaktivieren, erstellen Sie ein Cold Wallet, in #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "" @@ -812,7 +822,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "{ticker} zusammenfügen" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "" @@ -1185,7 +1195,7 @@ msgstr "Daten" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Neue Addresse" @@ -2409,7 +2437,8 @@ msgstr "Netzwerk" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "Nicht genügend Guthaben, um die Gebühr zu decken" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "" @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Output Anzahl" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "" @@ -3538,7 +3568,7 @@ msgstr "{ticker} senden" msgid "Send in bulk (airdrop)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "{ticker} splitten" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "" @@ -4215,7 +4245,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "" -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4227,6 +4257,10 @@ msgstr "Dadurch werden alle ausgewählten Coins zu einem einzigen kombiniert." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "" +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4289,7 +4323,7 @@ msgstr "Dadurch werden alle ausgewählten Coins aufgeteilt." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "" msgid "Token Icon" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "" @@ -4373,7 +4407,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "" @@ -4954,7 +4988,7 @@ msgstr "" msgid "You" msgstr "Sie" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4966,6 +5000,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "" @@ -4978,7 +5016,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "" @@ -4986,7 +5024,7 @@ msgstr "" msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "" @@ -5002,7 +5040,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "" diff --git a/src/locales/en-US/messages.po b/src/locales/en-US/messages.po index 1fb7d122d..31ab531d0 100644 --- a/src/locales/en-US/messages.po +++ b/src/locales/en-US/messages.po @@ -41,13 +41,21 @@ msgstr "{0, plural, one {You do not currently have any option contracts. Would y msgid "{0}" msgstr "{0}" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "{0} {1} coin{2}" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" -msgstr "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "{0} clawed back coin {1} coin{2}" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "{0} clawed back coin{1}" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" +msgstr "{0} finalized clawback{1}" #: src/components/MarketplaceCard.tsx:107 msgid "{0} link" @@ -89,7 +97,7 @@ msgstr "{label}: {content} (navigate within app)" msgid "{label}: {content} (opens in external application)" msgstr "{label}: {content} (opens in external application)" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "{outputCount} coins" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {This will remove the peer from your co msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "{selectedCoinCount} {selectedCoinLabel} selected" @@ -150,7 +158,7 @@ msgstr "{selectedCount} selected" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "1 combined coin" @@ -340,7 +348,7 @@ msgstr "All Levels" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Amount" @@ -412,7 +420,7 @@ msgstr "Asset can be revoked by the issuer" msgid "Asset ID" msgstr "Asset ID" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "By disabling this you are creating a cold wallet, with no ability to sig #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "Choose your preferred theme" msgid "Choose Your Theme" msgstr "Choose Your Theme" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "Claw Back" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "Claw Back {0}" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "Claw back Details" +#~ msgid "Claw back Details" +#~ msgstr "Claw back Details" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "Claw Back Details" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "Clawback Coins" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "Clear search" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "Clear Selection" @@ -781,7 +791,7 @@ msgstr "Close" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "coin" @@ -812,7 +822,7 @@ msgstr "Coin ID copied to clipboard" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "coins" @@ -897,7 +907,7 @@ msgstr "Combine {0}" #~ msgstr "Combine {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "Combine Coins" @@ -1185,7 +1195,7 @@ msgstr "Data" msgid "Data and License Details" msgstr "Data and License Details" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "Data copied to clipboard" @@ -1775,6 +1785,24 @@ msgstr "Fetching transactions..." msgid "Files Processed" msgstr "Files Processed" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "Finalize" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "Finalize {0} Clawback" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "Finalize Clawback" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "Finalize Clawback Details" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Fresh Address" @@ -2409,7 +2437,8 @@ msgstr "Network" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "Normalize Profiles" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "Not enough funds to cover the fee" @@ -2732,7 +2762,7 @@ msgstr "Offers Requesting This NFT" msgid "OK" msgstr "OK" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "Outline" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Output Count" @@ -3238,7 +3268,7 @@ msgstr "Requesting in exchange:" msgid "Require biometrics for sensitive actions" msgstr "Require biometrics for sensitive actions" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "Result" @@ -3538,7 +3568,7 @@ msgstr "Send {ticker}" msgid "Send in bulk (airdrop)" msgstr "Send in bulk (airdrop)" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "Send Token" @@ -3775,7 +3805,7 @@ msgstr "Split {0}" #~ msgstr "Split {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "Split Coins" @@ -4215,7 +4245,7 @@ msgstr "This will cancel all {0} active offers on-chain with transactions, preve msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "This will claw back all of the selected coins." @@ -4227,6 +4257,10 @@ msgstr "This will combine all of the selected coins into one." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "This will combine small enough coins automatically, so you don't have to manually select them." +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." @@ -4289,7 +4323,7 @@ msgstr "This will split all of the selected coins." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "Token Grid" msgid "Token Icon" msgstr "Token Icon" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "Token Issuance" @@ -4373,7 +4407,7 @@ msgstr "Tokens exported successfully" msgid "Tokens must have a positive amount." msgstr "Tokens must have a positive amount." -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "Total Amount" @@ -4954,7 +4988,7 @@ msgstr "with {syncedCoins} / {totalCoins} coins synced" msgid "You" msgstr "You" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "You are about to claw back coins. This will return them to your wallet." @@ -4966,6 +5000,10 @@ msgstr "You are about to create <0>{numberOfOffers} individual offers. Each msgid "You are about to create <0>1 offer." msgstr "You are about to create <0>1 offer." +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." @@ -4978,7 +5016,7 @@ msgstr "You are canceling {offerCount} offers on-chain. This will prevent them f msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." @@ -4986,7 +5024,7 @@ msgstr "You are combining multiple coins into a single coin. This can help reduc msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "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." @@ -5002,7 +5040,7 @@ msgstr "You Are Offering" msgid "You Are Requesting" msgstr "You Are Requesting" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." diff --git a/src/locales/es-MX/messages.po b/src/locales/es-MX/messages.po index 5fa1ffa17..92d06fd5b 100644 --- a/src/locales/es-MX/messages.po +++ b/src/locales/es-MX/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "{outputCount} monedas" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {Esto eliminará al nodo de tu conexió msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {Evitará temporalmente que el nodo se conecte.} other {Evitará temporalmente que los nodos se conecten.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "{selectedCoinCount} {selectedCoinLabel} seleccionados" @@ -150,7 +158,7 @@ msgstr "{selectedCount} seleccionados" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "{transactionSpentCount} entradas, {transactionCreatedCount} salidas" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "1 moneda combinada" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "Cantidad" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "ID de activo" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "Al deshabilitar esto, estás creando una billetera fría, sin capacidad #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "Limpiar búsqueda" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "Limpiar selección" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "moneda" @@ -812,7 +822,7 @@ msgstr "ID de moneda copiado al portapapeles" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "monedas" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "Combinar {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "Combinar Monedas" @@ -1185,7 +1195,7 @@ msgstr "Datos" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "Datos copiados al portapapeles" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "Dirección nueva" @@ -2409,7 +2437,8 @@ msgstr "Red" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "Normalizar perfiles" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "No hay fondos suficientes para cubrir la tarifa" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "Una vez emitido, este token aparecerá en tu billetera. Luego, puedes enviarlo a otras direcciones o crear ofertas para intercambiarlo." @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "Cantidad de salidas" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "Resultado" @@ -3538,7 +3568,7 @@ msgstr "Enviar {ticker}" msgid "Send in bulk (airdrop)" msgstr "Enviar en masa (airdrop)" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "Enviar Token" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "Dividir {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "Dividir Monedas" @@ -4215,7 +4245,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "Esto cancelará la oferta en la cadena con una transacción, evitando que sea aceptada incluso si alguien tiene el archivo de oferta original." -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4227,6 +4257,10 @@ msgstr "Esto combinará todas las monedas seleccionadas en una." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "Esto combinará automáticamente las monedas lo suficientemente pequeñas, así que no tienes que seleccionarlas manualmente." +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4289,7 +4323,7 @@ msgstr "Esto dividirá todas las monedas seleccionadas." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "Ticker" @@ -4339,7 +4373,7 @@ msgstr "Cuadrícula de tokens" msgid "Token Icon" msgstr "Icono de Token" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "Emisión de tokens" @@ -4373,7 +4407,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "Cantidad total" @@ -4954,7 +4988,7 @@ msgstr "con {syncedCoins} / {totalCoins} monedas sincronizadas" msgid "You" msgstr "Tú" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4966,6 +5000,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "Estás agregando una URL a este NFT. Esto se almacenará en la cadena de bloques y se puede utilizar para vincularse a contenido externo." @@ -4978,7 +5016,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "Estás cancelando esta oferta en la cadena de bloques. Esto evitará que sea aceptada, incluso si alguien tiene el archivo de oferta original." -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "Estás combinando varias monedas en una sola moneda. Esto puede ayudar a reducir la cantidad de monedas en tu billetera." @@ -4986,7 +5024,7 @@ msgstr "Estás combinando varias monedas en una sola moneda. Esto puede ayudar a msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "Estás creando un nuevo perfil. Esto generará un identificador descentralizado (DID) que se puede utilizar para asociar NFT y otros activos digitales con tu identidad." -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "Estás emitiendo un nuevo token. Esto creará un CAT (Token de Activos Chia) que se puede enviar a otros usuarios y negociar en bolsas de valores." @@ -5002,7 +5040,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "Estás dividiendo monedas en múltiples monedas de igual valor. Esto puede ayudar con transacciones paralelas y la creación de ofertas." diff --git a/src/locales/zh-CN/messages.po b/src/locales/zh-CN/messages.po index ff170aa5e..85e177c82 100644 --- a/src/locales/zh-CN/messages.po +++ b/src/locales/zh-CN/messages.po @@ -41,12 +41,20 @@ msgstr "" msgid "{0}" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:158 +#: src/components/confirmations/TokenConfirmation.tsx:179 msgid "{0} {1} coin{2}" msgstr "" #: src/components/confirmations/TokenConfirmation.tsx:213 -msgid "{0} clawed back coin {1} coin{2}" +#~ msgid "{0} clawed back coin {1} coin{2}" +#~ msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:236 +msgid "{0} clawed back coin{1}" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:241 +msgid "{0} finalized clawback{1}" msgstr "" #: src/components/MarketplaceCard.tsx:107 @@ -89,7 +97,7 @@ msgstr "" msgid "{label}: {content} (opens in external application)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:209 +#: src/components/confirmations/TokenConfirmation.tsx:232 msgid "{outputCount} coins" msgstr "" @@ -122,7 +130,7 @@ msgstr "{peersToDeleteCount, plural, one {这将移除与该节点的连接. 如 msgid "{peersToDeleteCount, plural, one {Will temporarily prevent the peer from being connected to.} other {Will temporarily prevent the peers from being connected to.}}" msgstr "{peersToDeleteCount, plural, one {将暂时阻止与该节点的连接.} other {将暂时阻止与该节点的连接.}}" -#: src/components/ClawbackCoinsCard.tsx:291 +#: src/components/ClawbackCoinsCard.tsx:346 msgid "{selectedCoinCount} {selectedCoinLabel} selected" msgstr "" @@ -150,7 +158,7 @@ msgstr "已选择 {selectedCount}" #~ msgid "{transactionSpentCount} inputs, {transactionCreatedCount} outputs" #~ msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:211 +#: src/components/confirmations/TokenConfirmation.tsx:234 msgid "1 combined coin" msgstr "" @@ -340,7 +348,7 @@ msgstr "" #: src/components/TransactionColumns.tsx:104 #: src/components/CoinList.tsx:133 #: src/components/selectors/AssetSelector.tsx:264 -#: src/components/confirmations/TokenConfirmation.tsx:137 +#: src/components/confirmations/TokenConfirmation.tsx:155 msgid "Amount" msgstr "金额" @@ -412,7 +420,7 @@ msgstr "" msgid "Asset ID" msgstr "" -#: src/pages/Token.tsx:190 +#: src/pages/Token.tsx:202 #: src/components/TokenGridView.tsx:81 #: src/components/TokenColumns.tsx:219 #: src/components/AssetCoin.tsx:77 @@ -619,7 +627,8 @@ msgstr "禁用此功能后, 将创建一个冷钱包, 该钱包无法签署交 #: src/components/NftCard.tsx:668 #: src/components/FeeOnlyDialog.tsx:92 #: src/components/ConfirmationDialog.tsx:607 -#: src/components/ClawbackCoinsCard.tsx:335 +#: src/components/ClawbackCoinsCard.tsx:390 +#: src/components/ClawbackCoinsCard.tsx:441 #: src/components/AssignNftDialog.tsx:144 #: src/components/dialogs/ResyncDialog.tsx:92 #: src/components/dialogs/OfferCreationProgressDialog.tsx:317 @@ -728,25 +737,26 @@ msgstr "" msgid "Choose Your Theme" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:278 -#: src/components/ClawbackCoinsCard.tsx:338 -#: src/components/confirmations/TokenConfirmation.tsx:71 +#: src/components/ClawbackCoinsCard.tsx:322 +#: src/components/ClawbackCoinsCard.tsx:393 +#: src/components/confirmations/TokenConfirmation.tsx:77 msgid "Claw Back" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:303 +#: src/components/ClawbackCoinsCard.tsx:358 msgid "Claw Back {0}" msgstr "" #: src/components/ClawbackCoinsCard.tsx:222 -msgid "Claw back Details" -msgstr "" +#~ msgid "Claw back Details" +#~ msgstr "" #: src/pages/Token.tsx:155 +#: src/components/ClawbackCoinsCard.tsx:222 msgid "Claw Back Details" msgstr "" -#: src/components/ClawbackCoinsCard.tsx:248 +#: src/components/ClawbackCoinsCard.tsx:292 msgid "Clawback Coins" msgstr "" @@ -766,7 +776,7 @@ msgid "Clear search" msgstr "" #: src/components/OwnedCoinsCard.tsx:470 -#: src/components/ClawbackCoinsCard.tsx:287 +#: src/components/ClawbackCoinsCard.tsx:342 msgid "Clear Selection" msgstr "" @@ -781,7 +791,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:526 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coin" msgstr "" @@ -812,7 +822,7 @@ msgstr "" #: src/components/OwnedCoinsCard.tsx:398 #: src/components/CoinList.tsx:527 -#: src/components/ClawbackCoinsCard.tsx:240 +#: src/components/ClawbackCoinsCard.tsx:284 msgid "coins" msgstr "" @@ -897,7 +907,7 @@ msgstr "" #~ msgstr "合并 {ticker}" #: src/pages/Token.tsx:143 -#: src/components/confirmations/TokenConfirmation.tsx:49 +#: src/components/confirmations/TokenConfirmation.tsx:55 msgid "Combine Coins" msgstr "" @@ -1185,7 +1195,7 @@ msgstr "数据" msgid "Data and License Details" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:116 +#: src/components/confirmations/TokenConfirmation.tsx:134 msgid "Data copied to clipboard" msgstr "" @@ -1775,6 +1785,24 @@ msgstr "" msgid "Files Processed" msgstr "" +#: src/components/ClawbackCoinsCard.tsx:333 +#: src/components/ClawbackCoinsCard.tsx:444 +msgid "Finalize" +msgstr "" + +#: src/components/ClawbackCoinsCard.tsx:405 +msgid "Finalize {0} Clawback" +msgstr "" + +#: src/components/confirmations/TokenConfirmation.tsx:94 +msgid "Finalize Clawback" +msgstr "" + +#: src/pages/Token.tsx:167 +#: src/components/ClawbackCoinsCard.tsx:266 +msgid "Finalize Clawback Details" +msgstr "" + #: src/pages/Addresses.tsx:75 msgid "Fresh Address" msgstr "刷新地址" @@ -2409,7 +2437,8 @@ msgstr "网络" #: src/components/OwnedCoinsCard.tsx:624 #: src/components/NftCard.tsx:652 #: src/components/FeeOnlyDialog.tsx:77 -#: src/components/ClawbackCoinsCard.tsx:320 +#: src/components/ClawbackCoinsCard.tsx:375 +#: src/components/ClawbackCoinsCard.tsx:426 #: src/components/AssignNftDialog.tsx:129 #: src/components/dialogs/CancelOfferDialog.tsx:64 msgid "Network Fee" @@ -2633,6 +2662,7 @@ msgstr "" #: src/components/NftCard.tsx:218 #: src/components/FeeOnlyDialog.tsx:50 #: src/components/ClawbackCoinsCard.tsx:197 +#: src/components/ClawbackCoinsCard.tsx:241 #: src/components/AssignNftDialog.tsx:58 msgid "Not enough funds to cover the fee" msgstr "余额不足以支付手续费" @@ -2732,7 +2762,7 @@ msgstr "" msgid "OK" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:227 +#: src/components/confirmations/TokenConfirmation.tsx:255 msgid "Once issued, this token will appear in your wallet. You can then send it to other addresses or create offers to trade it." msgstr "" @@ -2854,7 +2884,7 @@ msgid "Outline" msgstr "" #: src/components/OwnedCoinsCard.tsx:553 -#: src/components/confirmations/TokenConfirmation.tsx:202 +#: src/components/confirmations/TokenConfirmation.tsx:225 msgid "Output Count" msgstr "转出数量" @@ -3238,7 +3268,7 @@ msgstr "" msgid "Require biometrics for sensitive actions" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:204 +#: src/components/confirmations/TokenConfirmation.tsx:227 msgid "Result" msgstr "" @@ -3538,7 +3568,7 @@ msgstr "发送 {ticker}" msgid "Send in bulk (airdrop)" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:82 +#: src/components/confirmations/TokenConfirmation.tsx:88 msgid "Send Token" msgstr "" @@ -3775,7 +3805,7 @@ msgstr "" #~ msgstr "拆分 {ticker}" #: src/pages/Token.tsx:130 -#: src/components/confirmations/TokenConfirmation.tsx:38 +#: src/components/confirmations/TokenConfirmation.tsx:44 msgid "Split Coins" msgstr "" @@ -4214,7 +4244,7 @@ msgstr "" msgid "This will cancel the offer on-chain with a transaction, preventing it from being taken even if someone has the original offer file." msgstr "" -#: src/components/ClawbackCoinsCard.tsx:306 +#: src/components/ClawbackCoinsCard.tsx:361 msgid "This will claw back all of the selected coins." msgstr "" @@ -4226,6 +4256,10 @@ msgstr "这将把所有选中的币合并为一个." msgid "This will combine small enough coins automatically, so you don't have to manually select them." msgstr "" +#: src/components/ClawbackCoinsCard.tsx:408 +msgid "This will complete the clawback for all of the selected coins, and send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/dialogs/DeleteOfferDialog.tsx:40 msgid "This will delete {offerCount} offers from the wallet, but if they're shared externally they can still be accepted. The only way to truly cancel public offers is by spending one or more of their coins." msgstr "" @@ -4288,7 +4322,7 @@ msgstr "这将拆分所有选定的硬币." #: src/pages/IssueToken.tsx:91 #: src/components/TokenCard.tsx:237 -#: src/components/confirmations/TokenConfirmation.tsx:130 +#: src/components/confirmations/TokenConfirmation.tsx:148 msgid "Ticker" msgstr "简称" @@ -4338,7 +4372,7 @@ msgstr "" msgid "Token Icon" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:60 +#: src/components/confirmations/TokenConfirmation.tsx:66 msgid "Token Issuance" msgstr "" @@ -4372,7 +4406,7 @@ msgstr "" msgid "Tokens must have a positive amount." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:193 +#: src/components/confirmations/TokenConfirmation.tsx:216 msgid "Total Amount" msgstr "" @@ -4952,7 +4986,7 @@ msgstr "" msgid "You" msgstr "我" -#: src/components/confirmations/TokenConfirmation.tsx:74 +#: src/components/confirmations/TokenConfirmation.tsx:80 msgid "You are about to claw back coins. This will return them to your wallet." msgstr "" @@ -4964,6 +4998,10 @@ msgstr "" msgid "You are about to create <0>1 offer." msgstr "" +#: src/components/confirmations/TokenConfirmation.tsx:97 +msgid "You are about to finalize the clawback transaction. This will send the funds to the original recipient (even if the recipient wallet does not support clawbacks)." +msgstr "" + #: src/components/confirmations/AddUrlConfirmation.tsx:34 msgid "You are adding a URL to this NFT. This will be stored on-chain and can be used to link to external content." msgstr "" @@ -4976,7 +5014,7 @@ msgstr "" msgid "You are canceling this offer on-chain. This will prevent it from being taken even if someone has the original offer file." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:52 +#: src/components/confirmations/TokenConfirmation.tsx:58 msgid "You are combining multiple coins into a single coin. This can help reduce the number of coins in your wallet." msgstr "" @@ -4984,7 +5022,7 @@ msgstr "" msgid "You are creating a new profile. This will generate a decentralized identifier (DID) that can be used to associate NFTs and other digital assets with your identity." msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:63 +#: src/components/confirmations/TokenConfirmation.tsx:69 msgid "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." msgstr "" @@ -5000,7 +5038,7 @@ msgstr "" msgid "You Are Requesting" msgstr "" -#: src/components/confirmations/TokenConfirmation.tsx:41 +#: src/components/confirmations/TokenConfirmation.tsx:47 msgid "You are splitting coins into multiple coins of equal value. This can help with parallel transactions and offer creation." msgstr "" diff --git a/src/pages/Token.tsx b/src/pages/Token.tsx index 2f770bd0e..d932475ca 100644 --- a/src/pages/Token.tsx +++ b/src/pages/Token.tsx @@ -162,6 +162,18 @@ export default function Token() { /> ), }; + } else if (content.type === 'finalize_clawback') { + return { + title: t`Finalize Clawback Details`, + content: ( + + ), + }; } return undefined; From 251e92a512e59b3f03d5f1f60b283fd091824bf6 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 15 Jan 2026 19:31:46 -0600 Subject: [PATCH 03/14] add all supported os's --- Cargo.lock | 97 +++++++++ Cargo.toml | 1 + docs/DEEP_LINKING.md | 305 ++++++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 10 + src-tauri/Cargo.toml | 2 + src-tauri/capabilities/desktop.json | 3 +- src-tauri/capabilities/mobile.json | 3 +- src-tauri/src/lib.rs | 3 +- src-tauri/tauri.conf.json | 13 ++ src/App.tsx | 14 +- src/hooks/useDeepLink.ts | 99 +++++++++ 12 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 docs/DEEP_LINKING.md create mode 100644 src/hooks/useDeepLink.ts diff --git a/Cargo.lock b/Cargo.lock index c8b1475e3..4b7b77f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1675,6 +1675,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -2161,6 +2181,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "do-notation" version = "0.1.3" @@ -3099,6 +3128,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -4673,6 +4708,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -5763,6 +5808,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -6073,6 +6128,7 @@ dependencies = [ "tauri-plugin-barcode-scanner", "tauri-plugin-biometric", "tauri-plugin-clipboard-manager", + "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", @@ -7223,6 +7279,27 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b091f24f2f6bdb4a305b54d3961f629c11861c685aceeea9a1972f89e43d5" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "tracing", + "url", + "windows-registry", + "windows-result 0.3.4", +] + [[package]] name = "tauri-plugin-dialog" version = "2.4.2" @@ -7644,6 +7721,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -8784,6 +8870,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index 0d7530abd..1c614c8f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ tauri-plugin-barcode-scanner = "2.4.2" tauri-plugin-biometric = "2.3.2" tauri-plugin-safe-area-insets = "0.1.0" tauri-plugin-sage = { path = "./tauri-plugin-sage" } +tauri-plugin-deep-link = "2.4" tauri-build = "2.5.3" tauri-plugin = "2.5.2" diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md new file mode 100644 index 000000000..e3049e3f7 --- /dev/null +++ b/docs/DEEP_LINKING.md @@ -0,0 +1,305 @@ +# Deep Linking in Sage + +Sage supports custom URL scheme deep linking via the `chia-offer://` protocol. When a `chia-offer://` URL is opened, the app will launch (or come to focus if already running) and navigate to the appropriate screen. + +## URL Format + +```html +chia-offer:// +``` + +Where `` is a valid Chia offer string starting with `offer1`. + +### Example + +```html +chia-offer://offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0ygjusmd7wlcdkuk3swhc8d6nga5l447u06ml28d0ldrkn7khmlu063ksltjuz7vxl7uu7zhtt50cmdwv32yfh9r7yx2hq7vylhvmrqmn8vrt7uxje45423vjltcf9ep74nm2jm6kuj8ua3fffandh443zlxdf7f48vuewuk4k0hj4c6z4x8d2yg9zl08s3y2ewpaqna7nfa4agfddd069vpx2glkrvzuuh3xvxa97u00hel344vva6lcrjky2ez53p6yh7uh54rlkxtawmgah0v6v3h36wnw6z3uazgpa5afvmmwelunfzp6y9zpas4ea0hmd8mu30v9t60p7470ntl7djjkrufar4u72yv489hpzx3gknypm8lqzzefu20n36hjz0km5y4wl595u38n8d8a4hnjtmx4lm79la3788yflaq28j5yzhq7cul742jxlcs67f2848k7a60vhkclmaxxqwhxlqu8t6t4kw8kejjmm4nsz9tvj88m87tak3k99efxc7f82kk9s4mu8wz48my300x2t6j8g0ptasnnqqhznpycgvksqph04cd4g72zmwre95sa74dth2h4fpx03fx9pl7t8kmuye7cev4cf0wx7kdqymlz8knj4ej94zma287vtmspkcfgg9fml32229z0l94h5872tjqnf56xmdq3kmdy3xmdysxmd5jxnd5kqv23c6drcurlplmydk366yejl6vfeu99wd47h2fv7u9dv8lee579808p3v8040 +``` + +## Platform-Specific Information + +### macOS + +#### Registration + +The `chia-offer://` URL scheme is automatically registered in the app's `Info.plist` during the build process. The Tauri deep-link plugin handles the `CFBundleURLTypes` entries automatically based on the configuration in `tauri.conf.json`. + +#### Testing + +1. **Build the app:** + + ```bash + pnpm tauri build + ``` + +2. **Install the app:** + + - Copy `src-tauri/target/release/bundle/macos/Sage.app` to `/Applications` + - Or open the `.dmg` installer and drag to Applications + +3. **Test the deep link:** + + ```bash + open "chia-offer://offer1qqr83wcuu..." + ``` + +#### Development Limitations + +Deep links do **not** work during development with `pnpm tauri dev` on macOS. The app must be bundled and installed in `/Applications` for deep links to be recognized by the system. + +--- + +### Windows + +#### Registration + +The URL scheme is registered in the Windows Registry during app installation. The Tauri installer (`.msi` or `.exe`) handles this automatically. + +Registry entries are created at: + +- `HKEY_CURRENT_USER\Software\Classes\chia-offer` +- Or `HKEY_LOCAL_MACHINE\Software\Classes\chia-offer` (for all users) + +#### Testing + +1. **Build the app:** + + ```bash + pnpm tauri build + ``` + +2. **Install the app:** + + - Run the generated installer from `src-tauri/target/release/bundle/msi/` or `src-tauri/target/release/bundle/nsis/` + +3. **Test the deep link:** + + ```cmd + start chia-offer://offer1qqr83wcuu... + ``` + + Or open the URL in a web browser. + +#### Development Testing + +On Windows, you can use `register_all()` in Rust to register the URL scheme during development without installing the app. However, this requires running with elevated permissions. + +--- + +### Linux + +#### Registration + +On Linux, the URL scheme is registered via a `.desktop` file that includes `MimeType=x-scheme-handler/chia-offer`. This is handled automatically when: + +- Installing the `.deb` package +- Using an AppImage with an AppImage launcher + +#### Testing + +1. **Build the app:** + + ```bash + pnpm tauri build + ``` + +2. **Install the app:** + + - For `.deb`: `sudo dpkg -i src-tauri/target/release/bundle/deb/sage_*.deb` + - For AppImage: Use an AppImage launcher like [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) + +3. **Test the deep link:** + + ```bash + xdg-open "chia-offer://offer1qqr83wcuu..." + ``` + +#### Development Testing + +During development, you can manually create a `.desktop` file or use `xdg-mime` to register the scheme handler: + +```bash +# Create a desktop entry (replace paths appropriately) +cat > ~/.local/share/applications/sage-dev.desktop << EOF +[Desktop Entry] +Name=Sage (Dev) +Exec=/path/to/sage %u +Type=Application +MimeType=x-scheme-handler/chia-offer; +EOF + +# Register the handler +xdg-mime default sage-dev.desktop x-scheme-handler/chia-offer +``` + +--- + +### iOS + +#### Registration + +The URL scheme is automatically configured in the app's `Info.plist` during the build process. The Tauri plugin generates the necessary `CFBundleURLTypes` entries. + +#### Testing + +1. **Build for iOS:** + + ```bash + pnpm tauri ios build + ``` + +2. **Install on device/simulator:** + + - Use Xcode to install on a physical device or simulator + - Or use TestFlight for distribution + +3. **Test the deep link:** + - Open Safari and navigate to `chia-offer://offer1qqr83wcuu...` + - Or use the command line on a simulator: + + ```bash + xcrun simctl openurl booted "chia-offer://offer1qqr83wcuu..." + ``` + +#### Development Testing + +For iOS development, you can test on the simulator or a physical device connected via Xcode. Deep links work in development builds but require the app to be properly signed. + +--- + +### Android + +#### Registration + +The URL scheme is automatically registered in the app's `AndroidManifest.xml` during the build process. The Tauri plugin adds the necessary `` with the `chia-offer` scheme. + +The generated manifest includes: + +```xml + + + + + + +``` + +#### Testing + +1. **Build for Android:** + + ```bash + pnpm tauri android build + ``` + +2. **Install on device/emulator:** + + ```bash + adb install src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk + ``` + +3. **Test the deep link:** + + ```bash + adb shell am start -a android.intent.action.VIEW -d "chia-offer://offer1qqr83wcuu..." + ``` + +#### Development Testing + +For Android development, you can test on an emulator or physical device: + +```bash +# Start the dev server and build +pnpm tauri android dev + +# In another terminal, trigger the deep link +adb shell am start -a android.intent.action.VIEW -d "chia-offer://offer1qqr83wcuu..." +``` + +--- + +## Configuration + +The deep link configuration is located in `src-tauri/tauri.conf.json`: + +```json +{ + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["chia-offer"] + }, + "mobile": [ + { + "scheme": ["chia-offer"], + "appLink": false + } + ] + } + } +} +``` + +- **desktop.schemes**: List of URL schemes for desktop platforms (macOS, Windows, Linux) +- **mobile**: Configuration for mobile platforms (iOS, Android) + - **scheme**: List of URL schemes + - **appLink**: Set to `false` for custom schemes (no domain verification required) + +## Permissions + +The following capabilities are required: + +### Desktop (`src-tauri/capabilities/desktop.json`) + +```json +{ + "permissions": ["deep-link:default"] +} +``` + +### Mobile (`src-tauri/capabilities/mobile.json`) + +```json +{ + "permissions": ["deep-link:default"] +} +``` + +## Troubleshooting + +### Deep link not working on macOS + +- Ensure the app is installed in `/Applications` +- Verify the app was built with `pnpm tauri build`, not running in dev mode +- Check Console.app for any launch services errors + +### Deep link not working on Windows + +- Verify the app was installed via the MSI or NSIS installer +- Check the Windows Registry for the `chia-offer` scheme under `HKEY_CURRENT_USER\Software\Classes` +- Try restarting Windows Explorer + +### Deep link not working on Linux + +- Ensure you're using an AppImage launcher or installed the `.deb` package +- Verify the MIME type is registered: `xdg-mime query default x-scheme-handler/chia-offer` +- Check that the `.desktop` file exists in `~/.local/share/applications/` + +### Deep link not working on iOS + +- Verify the app is properly signed +- Check that the Info.plist contains the URL scheme +- Review device logs in Xcode for any errors + +### Deep link not working on Android + +- Verify the AndroidManifest.xml contains the intent filter +- Check `adb logcat` for any activity resolution errors +- Ensure no other app has registered the same scheme + +## References + +- [Tauri Deep Linking Plugin Documentation](https://v2.tauri.app/plugin/deep-linking/) +- [Tauri Deep Link Plugin API Reference](https://v2.tauri.app/reference/javascript/deep-link/) +- [Apple URL Scheme Documentation](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) +- [Android Deep Links Documentation](https://developer.android.com/training/app-links/deep-linking) diff --git a/package.json b/package.json index a74f9bffe..2ff6dba95 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@tauri-apps/plugin-barcode-scanner": "^2.4.2", "@tauri-apps/plugin-biometric": "^2.3.2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", + "@tauri-apps/plugin-deep-link": "^2.4.6", "@tauri-apps/plugin-dialog": "~2.4.2", "@tauri-apps/plugin-fs": "~2.4.4", "@tauri-apps/plugin-opener": "^2.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63023f1ce..c9f503c69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: '@tauri-apps/plugin-clipboard-manager': specifier: ^2.3.2 version: 2.3.2 + '@tauri-apps/plugin-deep-link': + specifier: ^2.4.6 + version: 2.4.6 '@tauri-apps/plugin-dialog': specifier: ~2.4.2 version: 2.4.2 @@ -1797,6 +1800,9 @@ packages: '@tauri-apps/plugin-clipboard-manager@2.3.2': resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + '@tauri-apps/plugin-deep-link@2.4.6': + resolution: {integrity: sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA==} + '@tauri-apps/plugin-dialog@2.4.2': resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} @@ -5772,6 +5778,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-deep-link@2.4.6': + dependencies: + '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-dialog@2.4.2': dependencies: '@tauri-apps/api': 2.9.1 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8a33e1e0d..988896e48 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -46,12 +46,14 @@ tauri-plugin-sharesheet = "0.0.1" tauri-plugin-window-state = { workspace = true } tauri-plugin-dialog = "2" tauri-plugin-fs = "2" +tauri-plugin-deep-link = { workspace = true } [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-biometric = { workspace = true } tauri-plugin-barcode-scanner = { workspace = true } tauri-plugin-safe-area-insets = { workspace = true } tauri-plugin-sage = { workspace = true } +tauri-plugin-deep-link = { workspace = true } [build-dependencies] tauri-build = { workspace = true, features = [] } diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index bdb030db1..b23505168 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -9,6 +9,7 @@ "core:tray:default", "core:window:allow-request-user-attention", "fs:allow-write-text-file", - "dialog:default" + "dialog:default", + "deep-link:default" ] } diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json index 07f0f17b4..35c3cdc48 100644 --- a/src-tauri/capabilities/mobile.json +++ b/src-tauri/capabilities/mobile.json @@ -8,6 +8,7 @@ "barcode-scanner:default", "biometric:default", "sage:default", - "sharesheet:allow-share-text" + "sharesheet:allow-share-text", + "deep-link:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0b07633b4..9b5c75669 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -155,7 +155,8 @@ pub fn run() { let mut tauri_builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_os::init()); + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_deep_link::init()); #[cfg(not(mobile))] { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 910c1372b..b36f9b0ec 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,6 +2,19 @@ "productName": "Sage", "version": "0.12.7", "identifier": "com.rigidnetwork.sage", + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["chia-offer"] + }, + "mobile": [ + { + "scheme": ["chia-offer"], + "appLink": false + } + ] + } + }, "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", diff --git a/src/App.tsx b/src/App.tsx index 6fefeeff2..de1e90c02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,11 @@ import { useEffect, useState } from 'react'; import { createHashRouter, createRoutesFromElements, + Outlet, Route, RouterProvider, } from 'react-router-dom'; +import { useDeepLink } from './hooks/useDeepLink'; import { Slide, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { ThemeProvider, useTheme } from 'theme-o-rama'; @@ -58,6 +60,14 @@ import Transaction from './pages/Transaction'; import { Transactions } from './pages/Transactions'; import Wallet from './pages/Wallet'; +// Root layout component that handles deep linking +function RootLayout() { + // Initialize deep link handler + useDeepLink(); + + return ; +} + // Theme-aware toast container component function ThemeAwareToastContainer() { const { currentTheme } = useTheme(); @@ -89,7 +99,7 @@ function ThemeAwareToastContainer() { const router = createHashRouter( createRoutesFromElements( - <> + }> } /> } /> } /> @@ -140,7 +150,7 @@ const router = createHashRouter( } /> } /> } /> - , + , ), ); diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts new file mode 100644 index 000000000..48afb11a7 --- /dev/null +++ b/src/hooks/useDeepLink.ts @@ -0,0 +1,99 @@ +import { useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const SCHEME_PREFIX = 'chia-offer://'; + +/** + * Parses a chia-offer:// URL and extracts the offer string. + * URL format: chia-offer:// + * Example: chia-offer://offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0yg... + */ +function parseDeepLinkUrl(url: string): string | null { + if (!url.startsWith(SCHEME_PREFIX)) { + return null; + } + + // Extract everything after the scheme + const offerString = url.slice(SCHEME_PREFIX.length); + + // Basic validation - offer strings should start with 'offer1' + if (!offerString || !offerString.startsWith('offer1')) { + console.warn('Invalid offer string in deep link:', offerString); + return null; + } + + return offerString; +} + +/** + * Hook to handle sage:// deep links on all platforms. + * When the app is opened via a deep link, it navigates to the offer view page. + * + * Platform-specific notes: + * - macOS: Deep links only work in the bundled app installed in /Applications. + * They will not work during development with `pnpm tauri dev`. + * - Windows: Deep links are registered during app installation. + * - Linux: Requires AppImage launcher for deep links to work, or use development mode + * with register_all() in Rust. + * - iOS/Android: Deep links are configured via the mobile section in tauri.conf.json + * and work after the app is installed. + */ +export function useDeepLink() { + const navigate = useNavigate(); + const processedUrls = useRef>(new Set()); + + useEffect(() => { + let cleanup: (() => void) | null = null; + + const handleDeepLinkUrls = (urls: string[]) => { + for (const url of urls) { + // Avoid processing the same URL twice + if (processedUrls.current.has(url)) { + continue; + } + processedUrls.current.add(url); + + console.log('Deep link received:', url); + + const offerString = parseDeepLinkUrl(url); + if (offerString) { + // Navigate to the offer view page + navigate(`/offers/view/${encodeURIComponent(offerString)}`); + // Only process the first valid URL + break; + } + } + }; + + const initDeepLink = async () => { + try { + // Dynamically import the deep-link plugin + const { getCurrent, onOpenUrl } = await import( + '@tauri-apps/plugin-deep-link' + ); + + // Check if app was launched via deep link + const initialUrls = await getCurrent(); + if (initialUrls && initialUrls.length > 0) { + handleDeepLinkUrls(initialUrls); + } + + // Listen for deep link events while the app is running + const unlisten = await onOpenUrl(handleDeepLinkUrls); + cleanup = unlisten; + } catch (error) { + // This can happen if the plugin isn't available on the current platform + // or if there's a configuration issue. Log but don't crash. + console.warn('Deep link handler not available:', error); + } + }; + + initDeepLink(); + + return () => { + if (cleanup) { + cleanup(); + } + }; + }, [navigate]); +} From d5dc9d90b28d91123afc7dbea2cb65d525fb10df Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 15 Jan 2026 19:46:34 -0600 Subject: [PATCH 04/14] add chia-offer scheme registration for ios --- docs/DEEP_LINKING.md | 3 ++- src-tauri/Info.plist | 11 +++++++++++ src-tauri/gen/apple/sage-tauri_iOS/Info.plist | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md index e3049e3f7..416ae3fe0 100644 --- a/docs/DEEP_LINKING.md +++ b/docs/DEEP_LINKING.md @@ -5,7 +5,7 @@ Sage supports custom URL scheme deep linking via the `chia-offer://` protocol. W ## URL Format ```html -chia-offer:// +chia-offer:// ``` Where `` is a valid Chia offer string starting with `offer1`. @@ -154,6 +154,7 @@ The URL scheme is automatically configured in the app's `Info.plist` during the - Or use TestFlight for distribution 3. **Test the deep link:** + - Open Safari and navigate to `chia-offer://offer1qqr83wcuu...` - Or use the command line on a simulator: diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist index 9cc665810..17fcc49d6 100644 --- a/src-tauri/Info.plist +++ b/src-tauri/Info.plist @@ -4,5 +4,16 @@ NSPhotoLibraryAddUsageDescription This app needs access to save images to your photo library. + CFBundleURLTypes + + + CFBundleURLName + com.rigidnetwork.sage + CFBundleURLSchemes + + chia-offer + + + diff --git a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist index bcc53b2f7..4ebea1a8d 100644 --- a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist +++ b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist @@ -48,5 +48,16 @@ Authenticate with biometric NSPhotoLibraryAddUsageDescription This app needs access to save images to your photo library. + CFBundleURLTypes + + + CFBundleURLName + com.rigidnetwork.sage + CFBundleURLSchemes + + chia-offer + + + \ No newline at end of file From e3fff09d32463538126393d3487a71494913389a Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Fri, 16 Jan 2026 18:11:21 -0600 Subject: [PATCH 05/14] revert to sage shcme for now --- docs/DEEP_LINKING.md | 48 +++---- src-tauri/Info.plist | 2 +- src-tauri/gen/apple/sage-tauri_iOS/Info.plist | 118 +++++++++--------- src-tauri/tauri.conf.json | 10 +- src/hooks/useDeepLink.ts | 18 +-- 5 files changed, 94 insertions(+), 102 deletions(-) diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md index 416ae3fe0..03a66a451 100644 --- a/docs/DEEP_LINKING.md +++ b/docs/DEEP_LINKING.md @@ -1,19 +1,19 @@ # Deep Linking in Sage -Sage supports custom URL scheme deep linking via the `chia-offer://` protocol. When a `chia-offer://` URL is opened, the app will launch (or come to focus if already running) and navigate to the appropriate screen. +Sage supports custom URL scheme deep linking via the `sage:` protocol. When a `sage:` URL is opened, the app will launch (or come to focus if already running) and navigate to the appropriate screen. ## URL Format -```html -chia-offer:// +``` +sage: ``` Where `` is a valid Chia offer string starting with `offer1`. ### Example -```html -chia-offer://offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0ygjusmd7wlcdkuk3swhc8d6nga5l447u06ml28d0ldrkn7khmlu063ksltjuz7vxl7uu7zhtt50cmdwv32yfh9r7yx2hq7vylhvmrqmn8vrt7uxje45423vjltcf9ep74nm2jm6kuj8ua3fffandh443zlxdf7f48vuewuk4k0hj4c6z4x8d2yg9zl08s3y2ewpaqna7nfa4agfddd069vpx2glkrvzuuh3xvxa97u00hel344vva6lcrjky2ez53p6yh7uh54rlkxtawmgah0v6v3h36wnw6z3uazgpa5afvmmwelunfzp6y9zpas4ea0hmd8mu30v9t60p7470ntl7djjkrufar4u72yv489hpzx3gknypm8lqzzefu20n36hjz0km5y4wl595u38n8d8a4hnjtmx4lm79la3788yflaq28j5yzhq7cul742jxlcs67f2848k7a60vhkclmaxxqwhxlqu8t6t4kw8kejjmm4nsz9tvj88m87tak3k99efxc7f82kk9s4mu8wz48my300x2t6j8g0ptasnnqqhznpycgvksqph04cd4g72zmwre95sa74dth2h4fpx03fx9pl7t8kmuye7cev4cf0wx7kdqymlz8knj4ej94zma287vtmspkcfgg9fml32229z0l94h5872tjqnf56xmdq3kmdy3xmdysxmd5jxnd5kqv23c6drcurlplmydk366yejl6vfeu99wd47h2fv7u9dv8lee579808p3v8040 +``` +sage:offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0ygjusmd7wlcdkuk3swhc8d6nga5l447u06ml28d0ldrkn7khmlu063ksltjuz7vxl7uu7zhtt50cmdwv32yfh9r7yx2hq7vylhvmrqmn8vrt7uxje45423vjltcf9ep74nm2jm6kuj8ua3fffandh443zlxdf7f48vuewuk4k0hj4c6z4x8d2yg9zl08s3y2ewpaqna7nfa4agfddd069vpx2glkrvzuuh3xvxa97u00hel344vva6lcrjky2ez53p6yh7uh54rlkxtawmgah0v6v3h36wnw6z3uazgpa5afvmmwelunfzp6y9zpas4ea0hmd8mu30v9t60p7470ntl7djjkrufar4u72yv489hpzx3gknypm8lqzzefu20n36hjz0km5y4wl595u38n8d8a4hnjtmx4lm79la3788yflaq28j5yzhq7cul742jxlcs67f2848k7a60vhkclmaxxqwhxlqu8t6t4kw8kejjmm4nsz9tvj88m87tak3k99efxc7f82kk9s4mu8wz48my300x2t6j8g0ptasnnqqhznpycgvksqph04cd4g72zmwre95sa74dth2h4fpx03fx9pl7t8kmuye7cev4cf0wx7kdqymlz8knj4ej94zma287vtmspkcfgg9fml32229z0l94h5872tjqnf56xmdq3kmdy3xmdysxmd5jxnd5kqv23c6drcurlplmydk366yejl6vfeu99wd47h2fv7u9dv8lee579808p3v8040 ``` ## Platform-Specific Information @@ -22,7 +22,7 @@ chia-offer://offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fu #### Registration -The `chia-offer://` URL scheme is automatically registered in the app's `Info.plist` during the build process. The Tauri deep-link plugin handles the `CFBundleURLTypes` entries automatically based on the configuration in `tauri.conf.json`. +The `sage:` URL scheme is automatically registered in the app's `Info.plist` during the build process. The Tauri deep-link plugin handles the `CFBundleURLTypes` entries automatically based on the configuration in `tauri.conf.json`. #### Testing @@ -40,7 +40,7 @@ The `chia-offer://` URL scheme is automatically registered in the app's `Info.pl 3. **Test the deep link:** ```bash - open "chia-offer://offer1qqr83wcuu..." + open "sage:offer1qqr83wcuu..." ``` #### Development Limitations @@ -57,8 +57,8 @@ The URL scheme is registered in the Windows Registry during app installation. Th Registry entries are created at: -- `HKEY_CURRENT_USER\Software\Classes\chia-offer` -- Or `HKEY_LOCAL_MACHINE\Software\Classes\chia-offer` (for all users) +- `HKEY_CURRENT_USER\Software\Classes\sage` +- Or `HKEY_LOCAL_MACHINE\Software\Classes\sage` (for all users) #### Testing @@ -75,7 +75,7 @@ Registry entries are created at: 3. **Test the deep link:** ```cmd - start chia-offer://offer1qqr83wcuu... + start sage:offer1qqr83wcuu... ``` Or open the URL in a web browser. @@ -90,7 +90,7 @@ On Windows, you can use `register_all()` in Rust to register the URL scheme duri #### Registration -On Linux, the URL scheme is registered via a `.desktop` file that includes `MimeType=x-scheme-handler/chia-offer`. This is handled automatically when: +On Linux, the URL scheme is registered via a `.desktop` file that includes `MimeType=x-scheme-handler/sage`. This is handled automatically when: - Installing the `.deb` package - Using an AppImage with an AppImage launcher @@ -111,7 +111,7 @@ On Linux, the URL scheme is registered via a `.desktop` file that includes `Mime 3. **Test the deep link:** ```bash - xdg-open "chia-offer://offer1qqr83wcuu..." + xdg-open "sage:offer1qqr83wcuu..." ``` #### Development Testing @@ -125,11 +125,11 @@ cat > ~/.local/share/applications/sage-dev.desktop << EOF Name=Sage (Dev) Exec=/path/to/sage %u Type=Application -MimeType=x-scheme-handler/chia-offer; +MimeType=x-scheme-handler/sage; EOF # Register the handler -xdg-mime default sage-dev.desktop x-scheme-handler/chia-offer +xdg-mime default sage-dev.desktop x-scheme-handler/sage ``` --- @@ -155,11 +155,11 @@ The URL scheme is automatically configured in the app's `Info.plist` during the 3. **Test the deep link:** - - Open Safari and navigate to `chia-offer://offer1qqr83wcuu...` + - Open Safari and navigate to `sage:offer1qqr83wcuu...` - Or use the command line on a simulator: ```bash - xcrun simctl openurl booted "chia-offer://offer1qqr83wcuu..." + xcrun simctl openurl booted "sage:offer1qqr83wcuu..." ``` #### Development Testing @@ -172,7 +172,7 @@ For iOS development, you can test on the simulator or a physical device connecte #### Registration -The URL scheme is automatically registered in the app's `AndroidManifest.xml` during the build process. The Tauri plugin adds the necessary `` with the `chia-offer` scheme. +The URL scheme is automatically registered in the app's `AndroidManifest.xml` during the build process. The Tauri plugin adds the necessary `` with the `sage` scheme. The generated manifest includes: @@ -181,7 +181,7 @@ The generated manifest includes: - + ``` @@ -202,7 +202,7 @@ The generated manifest includes: 3. **Test the deep link:** ```bash - adb shell am start -a android.intent.action.VIEW -d "chia-offer://offer1qqr83wcuu..." + adb shell am start -a android.intent.action.VIEW -d "sage:offer1qqr83wcuu..." ``` #### Development Testing @@ -214,7 +214,7 @@ For Android development, you can test on an emulator or physical device: pnpm tauri android dev # In another terminal, trigger the deep link -adb shell am start -a android.intent.action.VIEW -d "chia-offer://offer1qqr83wcuu..." +adb shell am start -a android.intent.action.VIEW -d "sage:offer1qqr83wcuu..." ``` --- @@ -228,11 +228,11 @@ The deep link configuration is located in `src-tauri/tauri.conf.json`: "plugins": { "deep-link": { "desktop": { - "schemes": ["chia-offer"] + "schemes": ["sage"] }, "mobile": [ { - "scheme": ["chia-offer"], + "scheme": ["sage"], "appLink": false } ] @@ -277,13 +277,13 @@ The following capabilities are required: ### Deep link not working on Windows - Verify the app was installed via the MSI or NSIS installer -- Check the Windows Registry for the `chia-offer` scheme under `HKEY_CURRENT_USER\Software\Classes` +- Check the Windows Registry for the `sage` scheme under `HKEY_CURRENT_USER\Software\Classes` - Try restarting Windows Explorer ### Deep link not working on Linux - Ensure you're using an AppImage launcher or installed the `.deb` package -- Verify the MIME type is registered: `xdg-mime query default x-scheme-handler/chia-offer` +- Verify the MIME type is registered: `xdg-mime query default x-scheme-handler/sage` - Check that the `.desktop` file exists in `~/.local/share/applications/` ### Deep link not working on iOS diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist index 17fcc49d6..ad0a8ea71 100644 --- a/src-tauri/Info.plist +++ b/src-tauri/Info.plist @@ -11,7 +11,7 @@ com.rigidnetwork.sage CFBundleURLSchemes - chia-offer + sage diff --git a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist index 4ebea1a8d..a0c77e1bb 100644 --- a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist +++ b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist @@ -1,63 +1,63 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.12.7 - CFBundleVersion - 0.12.7 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - arm64 - metal - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSCameraUsageDescription - Read QR codes - NFCReaderUsageDescription - Read NFC tags - NSFaceIDUsageDescription - Authenticate with biometric - NSPhotoLibraryAddUsageDescription - This app needs access to save images to your photo library. - CFBundleURLTypes - - - CFBundleURLName - com.rigidnetwork.sage - CFBundleURLSchemes - - chia-offer - - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.12.7 + CFBundleVersion + 0.12.7 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + Read QR codes + NFCReaderUsageDescription + Read NFC tags + NSFaceIDUsageDescription + Authenticate with biometric + NSPhotoLibraryAddUsageDescription + This app needs access to save images to your photo library. + CFBundleURLTypes + + + CFBundleURLName + com.rigidnetwork.sage + CFBundleURLSchemes + + chia + + + + \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b36f9b0ec..ca0384bb2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,11 +5,15 @@ "plugins": { "deep-link": { "desktop": { - "schemes": ["chia-offer"] + "schemes": [ + "sage" + ] }, "mobile": [ { - "scheme": ["chia-offer"], + "scheme": [ + "sage" + ], "appLink": false } ] @@ -51,4 +55,4 @@ } }, "$schema": "../node_modules/@tauri-apps/cli/config.schema.json" -} +} \ No newline at end of file diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 48afb11a7..4bee22f1e 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -1,19 +1,13 @@ import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; -const SCHEME_PREFIX = 'chia-offer://'; +const SCHEME_PREFIX = 'sage:'; -/** - * Parses a chia-offer:// URL and extracts the offer string. - * URL format: chia-offer:// - * Example: chia-offer://offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0yg... - */ function parseDeepLinkUrl(url: string): string | null { - if (!url.startsWith(SCHEME_PREFIX)) { + if (!url.toLowerCase().startsWith(SCHEME_PREFIX)) { return null; } - // Extract everything after the scheme const offerString = url.slice(SCHEME_PREFIX.length); // Basic validation - offer strings should start with 'offer1' @@ -26,9 +20,7 @@ function parseDeepLinkUrl(url: string): string | null { } /** - * Hook to handle sage:// deep links on all platforms. - * When the app is opened via a deep link, it navigates to the offer view page. - * + * Hook to handle sage: deep links on all platforms. * Platform-specific notes: * - macOS: Deep links only work in the bundled app installed in /Applications. * They will not work during development with `pnpm tauri dev`. @@ -47,19 +39,15 @@ export function useDeepLink() { const handleDeepLinkUrls = (urls: string[]) => { for (const url of urls) { - // Avoid processing the same URL twice if (processedUrls.current.has(url)) { continue; } processedUrls.current.add(url); - console.log('Deep link received:', url); - const offerString = parseDeepLinkUrl(url); if (offerString) { // Navigate to the offer view page navigate(`/offers/view/${encodeURIComponent(offerString)}`); - // Only process the first valid URL break; } } From 0a36a56808e7b9d608c40b9b3e676ebdeac8f15a Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Fri, 16 Jan 2026 18:16:15 -0600 Subject: [PATCH 06/14] handle not logged in state --- src/hooks/useDeepLink.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 4bee22f1e..2dc6e1324 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -1,5 +1,8 @@ +import { useWallet } from '@/contexts/WalletContext'; +import { t } from '@lingui/core/macro'; import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; const SCHEME_PREFIX = 'sage:'; @@ -32,6 +35,7 @@ function parseDeepLinkUrl(url: string): string | null { */ export function useDeepLink() { const navigate = useNavigate(); + const { wallet } = useWallet(); const processedUrls = useRef>(new Set()); useEffect(() => { @@ -46,6 +50,11 @@ export function useDeepLink() { const offerString = parseDeepLinkUrl(url); if (offerString) { + // Check if user is logged into a wallet + if (!wallet) { + toast.error(t`Please log into a wallet and try again`); + break; + } // Navigate to the offer view page navigate(`/offers/view/${encodeURIComponent(offerString)}`); break; @@ -83,5 +92,5 @@ export function useDeepLink() { cleanup(); } }; - }, [navigate]); + }, [navigate, wallet]); } From 944cf9318369e240f1eaa2bac975141acfcf0dce Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 17 Jan 2026 08:51:46 -0600 Subject: [PATCH 07/14] add send support --- src/hooks/useDeepLink.ts | 102 ++++++++++++++++++++++++++++++++------- src/pages/Offer.tsx | 15 +++++- src/pages/Send.tsx | 29 ++++++++++- 3 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 2dc6e1324..84e5edf81 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -1,4 +1,6 @@ import { useWallet } from '@/contexts/WalletContext'; +import { isValidAddress } from '@/lib/utils'; +import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -6,20 +8,65 @@ import { toast } from 'react-toastify'; const SCHEME_PREFIX = 'sage:'; -function parseDeepLinkUrl(url: string): string | null { +interface OfferDeepLink { + type: 'offer'; + offerString: string; + fee?: string; +} + +interface AddressDeepLink { + type: 'address'; + address: string; + amount?: string; + fee?: string; + memo?: string; +} + +type DeepLinkData = OfferDeepLink | AddressDeepLink | null; + +function parseDeepLinkUrl(url: string, prefix: string): DeepLinkData { if (!url.toLowerCase().startsWith(SCHEME_PREFIX)) { return null; } - const offerString = url.slice(SCHEME_PREFIX.length); + const payload = url.slice(SCHEME_PREFIX.length); - // Basic validation - offer strings should start with 'offer1' - if (!offerString || !offerString.startsWith('offer1')) { - console.warn('Invalid offer string in deep link:', offerString); - return null; + const [mainPart, queryString] = payload.split('?'); + + if (mainPart.startsWith('offer1')) { + const result: OfferDeepLink = { type: 'offer', offerString: mainPart }; + + if (queryString) { + const params = new URLSearchParams(queryString); + const fee = params.get('fee'); + if (fee) result.fee = fee; + } + + return result; } - return offerString; + if (isValidAddress(mainPart, prefix)) { + const result: AddressDeepLink = { + type: 'address', + address: mainPart, + }; + + if (queryString) { + const params = new URLSearchParams(queryString); + const amount = params.get('amount'); + const fee = params.get('fee'); + const memo = params.get('memos'); + + if (amount) result.amount = amount; + if (fee) result.fee = fee; + if (memo) result.memo = memo; + } + + return result; + } + + console.warn('Unrecognized deep link payload:', payload); + return null; } /** @@ -36,27 +83,49 @@ function parseDeepLinkUrl(url: string): string | null { export function useDeepLink() { const navigate = useNavigate(); const { wallet } = useWallet(); + const walletState = useWalletState(); const processedUrls = useRef>(new Set()); useEffect(() => { let cleanup: (() => void) | null = null; const handleDeepLinkUrls = (urls: string[]) => { + // Check if user is logged into a wallet + if (!wallet) { + toast.error(t`Please log into a wallet and try again`); + return; + } + + const prefix = walletState.sync.unit.ticker.toLowerCase(); + for (const url of urls) { if (processedUrls.current.has(url)) { continue; } processedUrls.current.add(url); - const offerString = parseDeepLinkUrl(url); - if (offerString) { - // Check if user is logged into a wallet - if (!wallet) { - toast.error(t`Please log into a wallet and try again`); - break; + const deepLinkData = parseDeepLinkUrl(url, prefix); + if (!deepLinkData) { + continue; + } + + if (deepLinkData.type === 'offer') { + let offerUrl = `/offers/view/${encodeURIComponent(deepLinkData.offerString)}`; + if (deepLinkData.fee) { + offerUrl += `?fee=${encodeURIComponent(deepLinkData.fee)}`; } - // Navigate to the offer view page - navigate(`/offers/view/${encodeURIComponent(offerString)}`); + navigate(offerUrl); + break; + } + + if (deepLinkData.type === 'address') { + const params = new URLSearchParams(); + params.set('address', deepLinkData.address); + if (deepLinkData.amount) params.set('amount', deepLinkData.amount); + if (deepLinkData.fee) params.set('fee', deepLinkData.fee); + if (deepLinkData.memo) params.set('memo', deepLinkData.memo); + + navigate(`/wallet/send/xch?${params.toString()}`); break; } } @@ -64,7 +133,6 @@ export function useDeepLink() { const initDeepLink = async () => { try { - // Dynamically import the deep-link plugin const { getCurrent, onOpenUrl } = await import( '@tauri-apps/plugin-deep-link' ); @@ -92,5 +160,5 @@ export function useDeepLink() { cleanup(); } }; - }, [navigate, wallet]); + }, [navigate, wallet, walletState]); } diff --git a/src/pages/Offer.tsx b/src/pages/Offer.tsx index f71ae8f65..28e3b65e4 100644 --- a/src/pages/Offer.tsx +++ b/src/pages/Offer.tsx @@ -16,18 +16,19 @@ import { FeeAmountInput } from '@/components/ui/masked-input'; import { CustomError } from '@/contexts/ErrorContext'; import { useErrors } from '@/hooks/useErrors'; import { resolveOfferData } from '@/lib/offerData'; -import { toMojos } from '@/lib/utils'; +import { fromMojos, toMojos } from '@/lib/utils'; import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { useCallback, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; export function Offer() { const { offer } = useParams(); const { addError } = useErrors(); const walletState = useWalletState(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [isLoading, setIsLoading] = useState(true); const [loadingStatus, setLoadingStatus] = useState(t`Initializing...`); @@ -37,6 +38,15 @@ export function Offer() { const [fee, setFee] = useState(''); const [resolvedOffer, setResolvedOffer] = useState(null); + // Populate fee from URL query parameters (e.g., from deep links) + useEffect(() => { + const feeMojos = searchParams.get('fee'); + if (feeMojos) { + const feeDecimal = fromMojos(feeMojos, walletState.sync.unit.precision); + setFee(feeDecimal.toString()); + } + }, [searchParams, walletState.sync.unit.precision]); + const resolveOffer = useCallback(async () => { if (!offer) return; @@ -127,6 +137,7 @@ export function Offer() { setFee(values.value)} onKeyDown={(event) => { if (event.key === 'Enter') { diff --git a/src/pages/Send.tsx b/src/pages/Send.tsx index 49dce50b1..4c0fad05d 100644 --- a/src/pages/Send.tsx +++ b/src/pages/Send.tsx @@ -43,7 +43,7 @@ import BigNumber from 'bignumber.js'; import { AlertCircleIcon, ArrowUpToLine } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import * as z from 'zod'; import { commands, @@ -65,6 +65,7 @@ export default function Send() { const navigate = useNavigate(); const walletState = useWalletState(); + const [searchParams] = useSearchParams(); const [asset, setAsset] = useState(null); const [response, setResponse] = useState(null); @@ -131,7 +132,7 @@ export default function Send() { asset ? BigNumber(amount).lte(toDecimal(asset.balance, asset.precision)) : true, - 'Amount exceeds balance', + t`Amount exceeds balance`, ), fee: amount(walletState.sync.unit.precision).optional(), memo: z.string().optional(), @@ -149,6 +150,30 @@ export default function Send() { resolver: zodResolver(formSchema), }); + // Populate form from URL query parameters (e.g., from deep links) + useEffect(() => { + const address = searchParams.get('address'); + const amountMojos = searchParams.get('amount'); + const feeMojos = searchParams.get('fee'); + const memo = searchParams.get('memo'); + + if (address) { + form.setValue('address', address); + } + if (amountMojos) { + const precision = asset?.precision ?? 12; + const amountDecimal = fromMojos(amountMojos, precision); + form.setValue('amount', amountDecimal.toString()); + } + if (feeMojos) { + const feeDecimal = fromMojos(feeMojos, walletState.sync.unit.precision); + form.setValue('fee', feeDecimal.toString()); + } + if (memo) { + form.setValue('memo', memo); + } + }, [searchParams, asset?.precision, walletState.sync.unit.precision, form]); + const { handleScanOrPaste } = useScannerOrClipboard((scanResValue) => { form.setValue('address', scanResValue); }); From 17ae6d7890d32421c5eff917a6680e17c1eb2d85 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 18 Jan 2026 11:29:03 -0600 Subject: [PATCH 08/14] add windows and linux support for deep-links --- .claude/settings.local.json | 5 ++ Cargo.lock | 17 +++++++ Cargo.toml | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 34 +++++++++++--- src-tauri/src/main.rs | 4 +- src-tauri/tauri.conf.json | 10 ++-- src/hooks/useDeepLink.ts | 91 +++++++++++++++++++++++++------------ 8 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..b99dd9d78 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "permissions": { + "allow": ["Bash(cd:*)", "WebFetch(domain:v2.tauri.app)"] + } +} diff --git a/Cargo.lock b/Cargo.lock index 4b7b77f5e..d708f5506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6136,6 +6136,7 @@ dependencies = [ "tauri-plugin-safe-area-insets", "tauri-plugin-sage", "tauri-plugin-sharesheet", + "tauri-plugin-single-instance", "tauri-plugin-window-state", "tauri-specta", "tokio", @@ -7417,6 +7418,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acba6b5ca527a96cdfcc96ae09b09ccb91ddff5e33978ca6873b96ea16bb404c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-plugin-deep-link", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-window-state" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 1c614c8f0..6399a2013 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ tauri-plugin-biometric = "2.3.2" tauri-plugin-safe-area-insets = "0.1.0" tauri-plugin-sage = { path = "./tauri-plugin-sage" } tauri-plugin-deep-link = "2.4" +tauri-plugin-single-instance = "2" tauri-build = "2.5.3" tauri-plugin = "2.5.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 988896e48..b72abec8b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -47,6 +47,7 @@ tauri-plugin-window-state = { workspace = true } tauri-plugin-dialog = "2" tauri-plugin-fs = "2" tauri-plugin-deep-link = { workspace = true } +tauri-plugin-single-instance = { workspace = true, features = ["deep-link"] } [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-biometric = { workspace = true } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9b5c75669..660bd57e9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -145,12 +145,13 @@ 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().bigint(BigIntExportBehavior::Number), - "../src/bindings.ts", - ) - .expect("Failed to export TypeScript bindings"); + if let Err(e) = builder.export( + Typescript::default().bigint(BigIntExportBehavior::Number), + "../src/bindings.ts", + ) { + // Don't panic - this can fail when a second instance is launched from a different directory + eprintln!("Failed to export TypeScript bindings: {e}"); + } let mut tauri_builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) @@ -163,7 +164,14 @@ pub fn run() { tauri_builder = tauri_builder .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_dialog::init()); + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + // Focus the main window when another instance is launched + // Deep link URLs are automatically forwarded by the plugin's "deep-link" feature + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_focus(); + } + })); } #[cfg(mobile)] @@ -180,6 +188,18 @@ pub fn run() { .invoke_handler(builder.invoke_handler()) .setup(move |app| { builder.mount_events(app); + + // Register deep link schemes at runtime for Linux and Windows dev mode + // Linux: Always needed since schemes aren't registered via installer during dev + // Windows: Only in debug mode, requires running as Administrator first time + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] + { + use tauri_plugin_deep_link::DeepLinkExt; + if let Err(e) = app.deep_link().register_all() { + eprintln!("Failed to register deep link: {e}"); + } + } + let path = app.path().app_data_dir()?; let app_state = AppState::new(Mutex::new(Sage::new(&path))); app.manage(Initialized(Mutex::new(false))); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9357357d1..be959d075 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,5 +1,5 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +// Prevents console window on Windows +#![windows_subsystem = "windows"] fn main() { sage_lib::run(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ca0384bb2..e456875ac 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,15 +5,11 @@ "plugins": { "deep-link": { "desktop": { - "schemes": [ - "sage" - ] + "schemes": ["sage"] }, "mobile": [ { - "scheme": [ - "sage" - ], + "scheme": ["sage"], "appLink": false } ] @@ -55,4 +51,4 @@ } }, "$schema": "../node_modules/@tauri-apps/cli/config.schema.json" -} \ No newline at end of file +} diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 84e5edf81..60e5e1f50 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -24,25 +24,42 @@ interface AddressDeepLink { type DeepLinkData = OfferDeepLink | AddressDeepLink | null; -function parseDeepLinkUrl(url: string, prefix: string): DeepLinkData { +interface ParseResult { + data: DeepLinkData; + error?: string; +} + +function parseDeepLinkUrl(url: string, prefix: string): ParseResult { if (!url.toLowerCase().startsWith(SCHEME_PREFIX)) { - return null; + return { data: null, error: 'invalid_scheme' }; } const payload = url.slice(SCHEME_PREFIX.length); + if (!payload) { + return { data: null, error: 'empty_payload' }; + } + const [mainPart, queryString] = payload.split('?'); - if (mainPart.startsWith('offer1')) { + // Validate offer string: must start with offer1, be alphanumeric, and reasonable length + // Chia offers are bech32m encoded, max ~10KB when compressed + const MAX_OFFER_LENGTH = 15000; + if ( + mainPart.startsWith('offer1') && + mainPart.length <= MAX_OFFER_LENGTH && + /^[a-z0-9]+$/.test(mainPart) + ) { const result: OfferDeepLink = { type: 'offer', offerString: mainPart }; if (queryString) { const params = new URLSearchParams(queryString); const fee = params.get('fee'); - if (fee) result.fee = fee; + // Validate fee is a positive integer (mojos) + if (fee && /^\d+$/.test(fee)) result.fee = fee; } - return result; + return { data: result }; } if (isValidAddress(mainPart, prefix)) { @@ -57,16 +74,18 @@ function parseDeepLinkUrl(url: string, prefix: string): DeepLinkData { const fee = params.get('fee'); const memo = params.get('memos'); - if (amount) result.amount = amount; - if (fee) result.fee = fee; - if (memo) result.memo = memo; + // Validate amount and fee are positive integers (mojos) + if (amount && /^\d+$/.test(amount)) result.amount = amount; + if (fee && /^\d+$/.test(fee)) result.fee = fee; + // Memo is freeform text but limit length to prevent abuse + if (memo && memo.length <= 1000) result.memo = memo; } - return result; + return { data: result }; } console.warn('Unrecognized deep link payload:', payload); - return null; + return { data: null, error: 'unrecognized_payload' }; } /** @@ -84,29 +103,40 @@ export function useDeepLink() { const navigate = useNavigate(); const { wallet } = useWallet(); const walletState = useWalletState(); - const processedUrls = useRef>(new Set()); + + // Use refs so the effect doesn't re-run when these change + const walletRef = useRef(wallet); + const walletStateRef = useRef(walletState); + const navigateRef = useRef(navigate); + + // Keep refs up to date + useEffect(() => { + walletRef.current = wallet; + walletStateRef.current = walletState; + navigateRef.current = navigate; + }, [wallet, walletState, navigate]); useEffect(() => { let cleanup: (() => void) | null = null; + let isMounted = true; const handleDeepLinkUrls = (urls: string[]) => { - // Check if user is logged into a wallet - if (!wallet) { - toast.error(t`Please log into a wallet and try again`); - return; - } - - const prefix = walletState.sync.unit.ticker.toLowerCase(); + const prefix = walletStateRef.current.sync.unit.ticker.toLowerCase(); for (const url of urls) { - if (processedUrls.current.has(url)) { + // Parse and validate URL first before checking wallet + const { data: deepLinkData, error } = parseDeepLinkUrl(url, prefix); + if (!deepLinkData) { + if (error) { + toast.error(t`Invalid deep link`); + } continue; } - processedUrls.current.add(url); - const deepLinkData = parseDeepLinkUrl(url, prefix); - if (!deepLinkData) { - continue; + // Only check wallet for valid deep links + if (!walletRef.current) { + toast.error(t`Please log into a wallet first`); + return; } if (deepLinkData.type === 'offer') { @@ -114,7 +144,7 @@ export function useDeepLink() { if (deepLinkData.fee) { offerUrl += `?fee=${encodeURIComponent(deepLinkData.fee)}`; } - navigate(offerUrl); + navigateRef.current(offerUrl); break; } @@ -125,7 +155,7 @@ export function useDeepLink() { if (deepLinkData.fee) params.set('fee', deepLinkData.fee); if (deepLinkData.memo) params.set('memo', deepLinkData.memo); - navigate(`/wallet/send/xch?${params.toString()}`); + navigateRef.current(`/wallet/send/xch?${params.toString()}`); break; } } @@ -137,15 +167,19 @@ export function useDeepLink() { '@tauri-apps/plugin-deep-link' ); + if (!isMounted) return; + // Check if app was launched via deep link const initialUrls = await getCurrent(); if (initialUrls && initialUrls.length > 0) { handleDeepLinkUrls(initialUrls); } + if (!isMounted) return; + // Listen for deep link events while the app is running - const unlisten = await onOpenUrl(handleDeepLinkUrls); - cleanup = unlisten; + // The single-instance plugin with "deep-link" feature automatically forwards URLs here + cleanup = await onOpenUrl(handleDeepLinkUrls); } catch (error) { // This can happen if the plugin isn't available on the current platform // or if there's a configuration issue. Log but don't crash. @@ -156,9 +190,10 @@ export function useDeepLink() { initDeepLink(); return () => { + isMounted = false; if (cleanup) { cleanup(); } }; - }, [navigate, wallet, walletState]); + }, []); // Empty deps - only run once } From b42acf67ff880b4d434126bb7f8944c762311fc9 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Tue, 20 Jan 2026 08:44:44 -0600 Subject: [PATCH 09/14] deeplink intents --- .../gen/android/app/src/main/AndroidManifest.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index d9d5222e2..359be853c 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,19 @@ + + + + + + + + + + + + + Date: Tue, 20 Jan 2026 14:27:25 -0600 Subject: [PATCH 10/14] get android working --- docs/DEEP_LINKING.md | 79 +++++++++++- src-tauri/gen/apple/sage-tauri_iOS/Info.plist | 118 +++++++++--------- src/hooks/useDeepLink.ts | 14 ++- 3 files changed, 147 insertions(+), 64 deletions(-) diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md index 03a66a451..4ecb9f4ad 100644 --- a/docs/DEEP_LINKING.md +++ b/docs/DEEP_LINKING.md @@ -2,20 +2,71 @@ Sage supports custom URL scheme deep linking via the `sage:` protocol. When a `sage:` URL is opened, the app will launch (or come to focus if already running) and navigate to the appropriate screen. -## URL Format +## URL Formats + +### Offer Links ``` -sage: +sage:[?fee=] ``` Where `` is a valid Chia offer string starting with `offer1`. -### Example +**Example:** + +``` +sage:offer1qqr83wcuu2rykcmqvpsxvgqq... +``` + +### Address Links (Send XCH) ``` -sage:offer1qqr83wcuu2rykcmqvpsxvgqqd2h6fv0lt2sn8ntyc6t52p2dxeem089j2x7fua0ygjusmd7wlcdkuk3swhc8d6nga5l447u06ml28d0ldrkn7khmlu063ksltjuz7vxl7uu7zhtt50cmdwv32yfh9r7yx2hq7vylhvmrqmn8vrt7uxje45423vjltcf9ep74nm2jm6kuj8ua3fffandh443zlxdf7f48vuewuk4k0hj4c6z4x8d2yg9zl08s3y2ewpaqna7nfa4agfddd069vpx2glkrvzuuh3xvxa97u00hel344vva6lcrjky2ez53p6yh7uh54rlkxtawmgah0v6v3h36wnw6z3uazgpa5afvmmwelunfzp6y9zpas4ea0hmd8mu30v9t60p7470ntl7djjkrufar4u72yv489hpzx3gknypm8lqzzefu20n36hjz0km5y4wl595u38n8d8a4hnjtmx4lm79la3788yflaq28j5yzhq7cul742jxlcs67f2848k7a60vhkclmaxxqwhxlqu8t6t4kw8kejjmm4nsz9tvj88m87tak3k99efxc7f82kk9s4mu8wz48my300x2t6j8g0ptasnnqqhznpycgvksqph04cd4g72zmwre95sa74dth2h4fpx03fx9pl7t8kmuye7cev4cf0wx7kdqymlz8knj4ej94zma287vtmspkcfgg9fml32229z0l94h5872tjqnf56xmdq3kmdy3xmdysxmd5jxnd5kqv23c6drcurlplmydk366yejl6vfeu99wd47h2fv7u9dv8lee579808p3v8040 +sage:
?amount=[&fee=][&memos=] ``` +Opens the send screen with pre-filled values. + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `address` | Yes | The destination address (xch1... or txch1...) | +| `amount` | No | Amount to send in mojos (1 XCH = 1,000,000,000,000 mojos) | +| `fee` | No | Transaction fee in mojos | +| `memos` | No | Memo text to attach to the transaction | + +**Example:** + +``` +sage:xch1abc123...?amount=1000000000000&fee=1000000&memos=Payment%20for%20services +``` + +## Android URL Encoding Requirement + +**Important:** On Android, the `&` character in query parameters must be URL-encoded as `%26`. Android's Intent system interprets literal `&` as a command separator, which causes the URL to be truncated at the first `&`. + +| Platform | `&` (literal) | `%26` (encoded) | +|----------|---------------|-----------------| +| Android | URL truncated | Works | +| iOS | Works | Works | +| macOS | Works | Works | +| Windows | Works | Works | +| Linux | Works | Works | + +**For cross-platform compatibility, always use `%26` instead of `&` in deep link URLs with multiple query parameters.** + +**Correct (works everywhere):** + +``` +sage:xch1abc...?amount=1000000000000%26fee=1000000%26memos=hello +``` + +**Incorrect (fails on Android):** + +``` +sage:xch1abc...?amount=1000000000000&fee=1000000&memos=hello +``` + +The app handles both formats, but Android will truncate URLs with literal `&` before they reach the app. + ## Platform-Specific Information ### macOS @@ -202,7 +253,11 @@ The generated manifest includes: 3. **Test the deep link:** ```bash + # Offer link adb shell am start -a android.intent.action.VIEW -d "sage:offer1qqr83wcuu..." + + # Address link with parameters (note: use %26 for &) + adb shell am start -a android.intent.action.VIEW -d "sage:xch1abc...?amount=1000000000000%26fee=1000000%26memos=hello" ``` #### Development Testing @@ -217,6 +272,10 @@ pnpm tauri android dev adb shell am start -a android.intent.action.VIEW -d "sage:offer1qqr83wcuu..." ``` +#### URL Encoding Note + +When testing address links with multiple query parameters, remember to use `%26` instead of `&`. See [Android URL Encoding Requirement](#android-url-encoding-requirement) for details. + --- ## Configuration @@ -298,6 +357,18 @@ The following capabilities are required: - Check `adb logcat` for any activity resolution errors - Ensure no other app has registered the same scheme +### Query parameters missing on Android + +If only the address is populated but amount, fee, or memos are missing, the URL likely contains literal `&` characters. Android's Intent system truncates URLs at the first `&`. Use `%26` instead: + +``` +# Wrong - parameters after first & will be lost +sage:xch1...?amount=100&fee=100&memos=test + +# Correct - all parameters will be received +sage:xch1...?amount=100%26fee=100%26memos=test +``` + ## References - [Tauri Deep Linking Plugin Documentation](https://v2.tauri.app/plugin/deep-linking/) diff --git a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist index a0c77e1bb..dfafce6fc 100644 --- a/src-tauri/gen/apple/sage-tauri_iOS/Info.plist +++ b/src-tauri/gen/apple/sage-tauri_iOS/Info.plist @@ -1,63 +1,63 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.12.7 - CFBundleVersion - 0.12.7 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - arm64 - metal - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSCameraUsageDescription - Read QR codes - NFCReaderUsageDescription - Read NFC tags - NSFaceIDUsageDescription - Authenticate with biometric - NSPhotoLibraryAddUsageDescription - This app needs access to save images to your photo library. - CFBundleURLTypes - - - CFBundleURLName - com.rigidnetwork.sage - CFBundleURLSchemes - - chia - - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.12.7 + CFBundleVersion + 0.12.7 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + Read QR codes + NFCReaderUsageDescription + Read NFC tags + NSFaceIDUsageDescription + Authenticate with biometric + NSPhotoLibraryAddUsageDescription + This app needs access to save images to your photo library. + CFBundleURLTypes + + + CFBundleURLName + com.rigidnetwork.sage + CFBundleURLSchemes + + sage + + + + \ No newline at end of file diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 60e5e1f50..152e0ccfc 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -69,7 +69,19 @@ function parseDeepLinkUrl(url: string, prefix: string): ParseResult { }; if (queryString) { - const params = new URLSearchParams(queryString); + // Always decode the query string first to handle Android's URL encoding + // Android's Intent system requires & to be encoded as %26, otherwise it + // truncates the URL at the first &. We decode here so both formats work. + let decodedQueryString = queryString; + if (queryString.includes('%')) { + try { + decodedQueryString = decodeURIComponent(queryString); + } catch { + // If decoding fails, use the original string + } + } + + const params = new URLSearchParams(decodedQueryString); const amount = params.get('amount'); const fee = params.get('fee'); const memo = params.get('memos'); From d96263110c6930b8376975e6ed95bab26b771ab4 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Tue, 20 Jan 2026 14:29:03 -0600 Subject: [PATCH 11/14] prettier --- docs/DEEP_LINKING.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md index 4ecb9f4ad..96ddb2dc3 100644 --- a/docs/DEEP_LINKING.md +++ b/docs/DEEP_LINKING.md @@ -20,22 +20,22 @@ sage:offer1qqr83wcuu2rykcmqvpsxvgqq... ### Address Links (Send XCH) -``` +```bash sage:
?amount=[&fee=][&memos=] ``` Opens the send screen with pre-filled values. -| Parameter | Required | Description | -|-----------|----------|-------------| -| `address` | Yes | The destination address (xch1... or txch1...) | -| `amount` | No | Amount to send in mojos (1 XCH = 1,000,000,000,000 mojos) | -| `fee` | No | Transaction fee in mojos | -| `memos` | No | Memo text to attach to the transaction | +| Parameter | Required | Description | +| --------- | -------- | --------------------------------------------------------- | +| `address` | Yes | The destination address (xch1... or txch1...) | +| `amount` | No | Amount to send in mojos (1 XCH = 1,000,000,000,000 mojos) | +| `fee` | No | Transaction fee in mojos | +| `memos` | No | Memo text to attach to the transaction | **Example:** -``` +```bash sage:xch1abc123...?amount=1000000000000&fee=1000000&memos=Payment%20for%20services ``` @@ -44,12 +44,12 @@ sage:xch1abc123...?amount=1000000000000&fee=1000000&memos=Payment%20for%20servic **Important:** On Android, the `&` character in query parameters must be URL-encoded as `%26`. Android's Intent system interprets literal `&` as a command separator, which causes the URL to be truncated at the first `&`. | Platform | `&` (literal) | `%26` (encoded) | -|----------|---------------|-----------------| -| Android | URL truncated | Works | -| iOS | Works | Works | -| macOS | Works | Works | -| Windows | Works | Works | -| Linux | Works | Works | +| -------- | ------------- | --------------- | +| Android | URL truncated | Works | +| iOS | Works | Works | +| macOS | Works | Works | +| Windows | Works | Works | +| Linux | Works | Works | **For cross-platform compatibility, always use `%26` instead of `&` in deep link URLs with multiple query parameters.** From 22f5b27a5923482194b3817d867c2f2dc56edf15 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 21 Jan 2026 09:54:38 -0600 Subject: [PATCH 12/14] let the send page validate that the address is for the logged in network --- src/hooks/useDeepLink.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 152e0ccfc..2cd8e299b 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -1,6 +1,5 @@ import { useWallet } from '@/contexts/WalletContext'; import { isValidAddress } from '@/lib/utils'; -import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -29,7 +28,7 @@ interface ParseResult { error?: string; } -function parseDeepLinkUrl(url: string, prefix: string): ParseResult { +function parseDeepLinkUrl(url: string): ParseResult { if (!url.toLowerCase().startsWith(SCHEME_PREFIX)) { return { data: null, error: 'invalid_scheme' }; } @@ -62,7 +61,7 @@ function parseDeepLinkUrl(url: string, prefix: string): ParseResult { return { data: result }; } - if (isValidAddress(mainPart, prefix)) { + if (isValidAddress(mainPart, 'xch') || isValidAddress(mainPart, 'txch')) { const result: AddressDeepLink = { type: 'address', address: mainPart, @@ -114,30 +113,25 @@ function parseDeepLinkUrl(url: string, prefix: string): ParseResult { export function useDeepLink() { const navigate = useNavigate(); const { wallet } = useWallet(); - const walletState = useWalletState(); // Use refs so the effect doesn't re-run when these change const walletRef = useRef(wallet); - const walletStateRef = useRef(walletState); const navigateRef = useRef(navigate); // Keep refs up to date useEffect(() => { walletRef.current = wallet; - walletStateRef.current = walletState; navigateRef.current = navigate; - }, [wallet, walletState, navigate]); + }, [wallet, navigate]); useEffect(() => { let cleanup: (() => void) | null = null; let isMounted = true; const handleDeepLinkUrls = (urls: string[]) => { - const prefix = walletStateRef.current.sync.unit.ticker.toLowerCase(); - for (const url of urls) { // Parse and validate URL first before checking wallet - const { data: deepLinkData, error } = parseDeepLinkUrl(url, prefix); + const { data: deepLinkData, error } = parseDeepLinkUrl(url); if (!deepLinkData) { if (error) { toast.error(t`Invalid deep link`); From b882b2779099ae513e5dab9328594aa516377ece Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 22 Jan 2026 07:01:35 -0600 Subject: [PATCH 13/14] add fee to offer variant --- docs/DEEP_LINKING.md | 13 ++++++++++--- src/hooks/useDeepLink.ts | 12 +++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/DEEP_LINKING.md b/docs/DEEP_LINKING.md index 96ddb2dc3..ec17a9a73 100644 --- a/docs/DEEP_LINKING.md +++ b/docs/DEEP_LINKING.md @@ -10,12 +10,19 @@ Sage supports custom URL scheme deep linking via the `sage:` protocol. When a `s sage:[?fee=] ``` -Where `` is a valid Chia offer string starting with `offer1`. +| Parameter | Required | Description | +| --------- | -------- | ------------------------------------------------- | +| `offer_string` | Yes | A valid Chia offer string starting with `offer1`| +| `fee` | No | Network fee in mojos to prepopulate when taking the offer | -**Example:** +**Examples:** -``` +```bash +# Basic offer link sage:offer1qqr83wcuu2rykcmqvpsxvgqq... + +# Offer link with pre-filled fee (1 million mojos = 0.000001 XCH) +sage:offer1qqr83wcuu2rykcmqvpsxvgqq...?fee=1000000 ``` ### Address Links (Send XCH) diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 2cd8e299b..bb5e3cf10 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -52,7 +52,17 @@ function parseDeepLinkUrl(url: string): ParseResult { const result: OfferDeepLink = { type: 'offer', offerString: mainPart }; if (queryString) { - const params = new URLSearchParams(queryString); + // Decode query string to handle Android's URL encoding (see address handling below) + let decodedQueryString = queryString; + if (queryString.includes('%')) { + try { + decodedQueryString = decodeURIComponent(queryString); + } catch { + // If decoding fails, use the original string + } + } + + const params = new URLSearchParams(decodedQueryString); const fee = params.get('fee'); // Validate fee is a positive integer (mojos) if (fee && /^\d+$/.test(fee)) result.fee = fee; From 5c6a0879ce2b3052cb3c9730a726515c3c0fb8a0 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 31 Jan 2026 07:45:29 -0600 Subject: [PATCH 14/14] helper function --- src/hooks/useDeepLink.ts | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index bb5e3cf10..45020a968 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -28,6 +28,18 @@ interface ParseResult { error?: string; } +function decodeQueryString(queryString: string): URLSearchParams { + let decoded = queryString; + if (queryString.includes('%')) { + try { + decoded = decodeURIComponent(queryString); + } catch { + // If decoding fails, use the original string + } + } + return new URLSearchParams(decoded); +} + function parseDeepLinkUrl(url: string): ParseResult { if (!url.toLowerCase().startsWith(SCHEME_PREFIX)) { return { data: null, error: 'invalid_scheme' }; @@ -52,17 +64,7 @@ function parseDeepLinkUrl(url: string): ParseResult { const result: OfferDeepLink = { type: 'offer', offerString: mainPart }; if (queryString) { - // Decode query string to handle Android's URL encoding (see address handling below) - let decodedQueryString = queryString; - if (queryString.includes('%')) { - try { - decodedQueryString = decodeURIComponent(queryString); - } catch { - // If decoding fails, use the original string - } - } - - const params = new URLSearchParams(decodedQueryString); + const params = decodeQueryString(queryString); const fee = params.get('fee'); // Validate fee is a positive integer (mojos) if (fee && /^\d+$/.test(fee)) result.fee = fee; @@ -78,19 +80,7 @@ function parseDeepLinkUrl(url: string): ParseResult { }; if (queryString) { - // Always decode the query string first to handle Android's URL encoding - // Android's Intent system requires & to be encoded as %26, otherwise it - // truncates the URL at the first &. We decode here so both formats work. - let decodedQueryString = queryString; - if (queryString.includes('%')) { - try { - decodedQueryString = decodeURIComponent(queryString); - } catch { - // If decoding fails, use the original string - } - } - - const params = new URLSearchParams(decodedQueryString); + const params = decodeQueryString(queryString); const amount = params.get('amount'); const fee = params.get('fee'); const memo = params.get('memos');