Skip to content

Commit 30afd1f

Browse files
authored
Merge pull request #712 from xch-dev/clawback-finalize
Add support for finalizing clawbacks
2 parents 4da3eb7 + 0bfa706 commit 30afd1f

File tree

13 files changed

+325
-22
lines changed

13 files changed

+325
-22
lines changed

crates/sage-api/endpoints.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"mint_option": true,
5959
"transfer_options": true,
6060
"exercise_options": true,
61+
"finalize_clawback": true,
6162
"sign_coin_spends": true,
6263
"view_coin_spends": true,
6364
"submit_transaction": true,

crates/sage-api/src/requests/transactions.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,29 @@ pub struct TransferOptions {
696696
pub auto_submit: bool,
697697
}
698698

699+
/// Send CAT tokens to an address
700+
#[cfg_attr(
701+
feature = "openapi",
702+
crate::openapi_attr(
703+
tag = "XCH Transactions",
704+
description = "Finalize the clawback for a set of coins.",
705+
response_type = "TransactionResponse"
706+
)
707+
)]
708+
#[derive(Debug, Clone, Serialize, Deserialize)]
709+
#[cfg_attr(feature = "tauri", derive(specta::Type))]
710+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
711+
pub struct FinalizeClawback {
712+
/// The coins to finalize the clawback for
713+
pub coin_ids: Vec<String>,
714+
/// Transaction fee
715+
pub fee: Amount,
716+
/// Whether to automatically submit the transaction
717+
#[serde(default)]
718+
#[cfg_attr(feature = "openapi", schema(default = false))]
719+
pub auto_submit: bool,
720+
}
721+
699722
/// Sign coin spends to create a transaction
700723
#[cfg_attr(
701724
feature = "openapi",
@@ -807,3 +830,4 @@ pub type TransferDidsResponse = TransactionResponse;
807830
pub type NormalizeDidsResponse = TransactionResponse;
808831
pub type TransferOptionsResponse = TransactionResponse;
809832
pub type ExerciseOptionsResponse = TransactionResponse;
833+
pub type FinalizeClawbackResponse = TransactionResponse;

crates/sage-database/src/tables/p2_puzzles.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub enum P2Puzzle {
2525

2626
#[derive(Debug, Clone, Copy)]
2727
pub struct Clawback {
28-
pub public_key: PublicKey,
28+
pub public_key: Option<PublicKey>,
2929
pub sender_puzzle_hash: Bytes32,
3030
pub receiver_puzzle_hash: Bytes32,
3131
pub seconds: u64,
@@ -490,10 +490,10 @@ async fn clawback(conn: impl SqliteExecutor<'_>, p2_puzzle_hash: Bytes32) -> Res
490490

491491
let row = query!(
492492
"
493-
SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds
493+
SELECT key AS 'key?', sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds
494494
FROM p2_puzzles
495495
INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id
496-
INNER JOIN public_keys ON public_keys.p2_puzzle_id IN (
496+
LEFT JOIN public_keys ON public_keys.p2_puzzle_id IN (
497497
SELECT id FROM p2_puzzles
498498
WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)
499499
OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)

crates/sage-wallet/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ pub enum WalletError {
117117
#[error("Unsupported underlying coin kind: {0:?}")]
118118
UnsupportedUnderlyingCoinKind(CoinKind),
119119

120+
#[error("Unsupported clawback coin kind: {0:?}")]
121+
UnsupportedClawbackCoinKind(CoinKind),
122+
123+
#[error("Cannot find clawback info for coin with id {0}")]
124+
MissingClawbackInfo(Bytes32),
125+
120126
#[error("Try from int error: {0}")]
121127
TryFromInt(#[from] TryFromIntError),
122128
}

crates/sage-wallet/src/wallet.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,11 @@ impl Wallet {
309309
P2Puzzle::PublicKey(public_key) => StandardLayer::new(*public_key)
310310
.spend_with_conditions(ctx, spend.finish()),
311311
P2Puzzle::Clawback(clawback) => {
312-
let custody = StandardLayer::new(clawback.public_key);
312+
let Some(public_key) = clawback.public_key else {
313+
return Err(DriverError::MissingKey);
314+
};
315+
316+
let custody = StandardLayer::new(public_key);
313317
let spend = custody.spend_with_conditions(ctx, spend.finish())?;
314318

315319
let clawback = ClawbackV2::new(

crates/sage-wallet/src/wallet/xch.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ use chia::{
22
clvm_utils::ToTreeHash,
33
protocol::{Bytes, Bytes32, CoinSpend},
44
};
5-
use chia_wallet_sdk::driver::{Action, ClawbackV2, Id, SpendContext};
5+
use chia_wallet_sdk::{
6+
driver::{Action, Cat, CatSpend, ClawbackV2, Id, SpendContext},
7+
prelude::AssertConcurrentSpend,
8+
};
9+
use sage_database::{CoinKind, P2Puzzle};
610

711
use crate::{
812
wallet::memos::{calculate_memos, Hint},
@@ -53,6 +57,86 @@ impl Wallet {
5357

5458
Ok(ctx.take())
5559
}
60+
61+
pub async fn finalize_clawback(
62+
&self,
63+
coin_ids: Vec<Bytes32>,
64+
fee: u64,
65+
) -> Result<Vec<CoinSpend>, WalletError> {
66+
let mut ctx = SpendContext::new();
67+
68+
for &coin_id in &coin_ids {
69+
let Some(coin_kind) = self.db.coin_kind(coin_id).await? else {
70+
return Err(WalletError::MissingCoin(coin_id));
71+
};
72+
73+
match coin_kind {
74+
CoinKind::Xch => {
75+
let Some(coin) = self.db.xch_coin(coin_id).await? else {
76+
return Err(WalletError::MissingXchCoin(coin_id));
77+
};
78+
79+
let P2Puzzle::Clawback(clawback) = self.db.p2_puzzle(coin.puzzle_hash).await?
80+
else {
81+
return Err(WalletError::MissingClawbackInfo(coin_id));
82+
};
83+
84+
let clawback = ClawbackV2::new(
85+
clawback.sender_puzzle_hash,
86+
clawback.receiver_puzzle_hash,
87+
clawback.seconds,
88+
coin.amount,
89+
false,
90+
);
91+
92+
clawback.push_through_coin_spend(&mut ctx, coin)?;
93+
}
94+
CoinKind::Cat => {
95+
let Some(cat) = self.db.cat_coin(coin_id).await? else {
96+
return Err(WalletError::MissingCatCoin(coin_id));
97+
};
98+
99+
let P2Puzzle::Clawback(clawback) =
100+
self.db.p2_puzzle(cat.info.p2_puzzle_hash).await?
101+
else {
102+
return Err(WalletError::MissingClawbackInfo(coin_id));
103+
};
104+
105+
let clawback = ClawbackV2::new(
106+
clawback.sender_puzzle_hash,
107+
clawback.receiver_puzzle_hash,
108+
clawback.seconds,
109+
cat.coin.amount,
110+
true,
111+
);
112+
113+
let spend = clawback.push_through_spend(&mut ctx)?;
114+
Cat::spend_all(&mut ctx, &[CatSpend::new(cat, spend)])?;
115+
}
116+
_ => {
117+
return Err(WalletError::UnsupportedClawbackCoinKind(coin_kind));
118+
}
119+
}
120+
}
121+
122+
if fee > 0 {
123+
let actions = [Action::fee(fee)];
124+
125+
let mut spends = self.prepare_spends(&mut ctx, vec![], &actions).await?;
126+
127+
for &coin_id in &coin_ids {
128+
spends
129+
.conditions
130+
.required
131+
.push(AssertConcurrentSpend::new(coin_id));
132+
}
133+
134+
let deltas = spends.apply(&mut ctx, &actions)?;
135+
self.complete_spends(&mut ctx, &deltas, spends).await?;
136+
}
137+
138+
Ok(ctx.take())
139+
}
56140
}
57141

58142
#[cfg(test)]

crates/sage/src/endpoints/transactions.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ use itertools::Itertools;
1313
use sage_api::{
1414
AddNftUri, AssignNftsToDid, AutoCombineCat, AutoCombineCatResponse, AutoCombineXch,
1515
AutoCombineXchResponse, BulkMintNfts, BulkMintNftsResponse, BulkSendCat, BulkSendXch, Combine,
16-
CreateDid, ExerciseOptions, IssueCat, MintOption, MintOptionResponse, MultiSend, NftUriKind,
17-
NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends, SignCoinSpendsResponse, Split,
18-
SubmitTransaction, SubmitTransactionResponse, TransactionResponse, TransferDids, TransferNfts,
19-
TransferOptions, ViewCoinSpends, ViewCoinSpendsResponse,
16+
CreateDid, ExerciseOptions, FinalizeClawback, IssueCat, MintOption, MintOptionResponse,
17+
MultiSend, NftUriKind, NormalizeDids, OptionAsset, SendCat, SendXch, SignCoinSpends,
18+
SignCoinSpendsResponse, Split, SubmitTransaction, SubmitTransactionResponse,
19+
TransactionResponse, TransferDids, TransferNfts, TransferOptions, ViewCoinSpends,
20+
ViewCoinSpendsResponse,
2021
};
2122
use sage_assets::fetch_uris_without_hash;
2223
use sage_database::{Asset, AssetKind};
@@ -552,6 +553,15 @@ impl Sage {
552553
self.transact(coin_spends, req.auto_submit).await
553554
}
554555

556+
pub async fn finalize_clawback(&self, req: FinalizeClawback) -> Result<TransactionResponse> {
557+
let wallet = self.wallet()?;
558+
let coin_ids = parse_coin_ids(req.coin_ids)?;
559+
let fee = parse_amount(req.fee)?;
560+
561+
let coin_spends = wallet.finalize_clawback(coin_ids, fee).await?;
562+
self.transact(coin_spends, req.auto_submit).await
563+
}
564+
555565
pub async fn sign_coin_spends(&self, req: SignCoinSpends) -> Result<SignCoinSpendsResponse> {
556566
let coin_spends = req
557567
.coin_spends
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
DROP VIEW clawback_coins;
2+
3+
CREATE VIEW clawback_coins AS
4+
SELECT *
5+
FROM wallet_coins
6+
WHERE 1=1
7+
AND spent_height IS NULL
8+
AND clawback_expiration_seconds IS NOT NULL;

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub fn run() {
5555
commands::exercise_options,
5656
commands::add_nft_uri,
5757
commands::assign_nfts_to_did,
58+
commands::finalize_clawback,
5859
commands::sign_coin_spends,
5960
commands::view_coin_spends,
6061
commands::submit_transaction,

src/bindings.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ async addNftUri(req: AddNftUri) : Promise<TransactionResponse> {
101101
async assignNftsToDid(req: AssignNftsToDid) : Promise<TransactionResponse> {
102102
return await TAURI_INVOKE("assign_nfts_to_did", { req });
103103
},
104+
async finalizeClawback(req: FinalizeClawback) : Promise<TransactionResponse> {
105+
return await TAURI_INVOKE("finalize_clawback", { req });
106+
},
104107
async signCoinSpends(req: SignCoinSpends) : Promise<SignCoinSpendsResponse> {
105108
return await TAURI_INVOKE("sign_coin_spends", { req });
106109
},
@@ -817,6 +820,22 @@ export type FilterUnlockedCoinsResponse = {
817820
* List of unlocked coin IDs
818821
*/
819822
coin_ids: string[] }
823+
/**
824+
* Send CAT tokens to an address
825+
*/
826+
export type FinalizeClawback = {
827+
/**
828+
* The coins to finalize the clawback for
829+
*/
830+
coin_ids: string[];
831+
/**
832+
* Transaction fee
833+
*/
834+
fee: Amount;
835+
/**
836+
* Whether to automatically submit the transaction
837+
*/
838+
auto_submit?: boolean }
820839
/**
821840
* Generate a new mnemonic phrase for wallet creation
822841
*/

0 commit comments

Comments
 (0)