From c774d974867678ec504d8c7f6fed84206dbae3ea Mon Sep 17 00:00:00 2001 From: Rigidity Date: Thu, 7 Nov 2024 22:14:56 -0500 Subject: [PATCH] Add NFT URL button --- Cargo.lock | 57 ++++--- Cargo.toml | 2 +- crates/sage-api/src/types.rs | 2 + crates/sage-api/src/types/nft_uri_kind.rs | 10 ++ crates/sage-wallet/src/sync_manager.rs | 5 +- .../src/sync_manager/peer_discovery.rs | 31 +++- crates/sage-wallet/src/wallet/nfts.rs | 59 ++++++- src-tauri/src/commands/transactions.rs | 46 ++++- src-tauri/src/lib.rs | 1 + src/bindings.ts | 9 + src/components/Nav.tsx | 5 +- src/components/NftCard.tsx | 161 +++++++++++++++++- src/pages/Nft.tsx | 2 +- 13 files changed, 350 insertions(+), 40 deletions(-) create mode 100644 crates/sage-api/src/types/nft_uri_kind.rs diff --git a/Cargo.lock b/Cargo.lock index d901432a..09060777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,15 +758,17 @@ dependencies = [ [[package]] name = "chia-sdk-client" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e020c1ba12d33b6361292762c7cfef1344268a1879249cf6ccfbed46bb543ca" +checksum = "c67bd78889331985558c3af7075b897e98d5507a5fb67945b42233dd22efe795" dependencies = [ + "aws-lc-rs", "chia-protocol", "chia-sdk-types", "chia-ssl", "chia-traits 0.15.0", "futures-util", + "once_cell", "rustls 0.22.4", "rustls-pemfile", "thiserror", @@ -778,9 +780,9 @@ dependencies = [ [[package]] name = "chia-sdk-derive" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118a662acfdf6c24f33276d1fdc2da6fe3f125a124d5205a2a1a0d243e4765c5" +checksum = "a12346c930c0fb76fa6bc23e6516b42a6f05e80806df05875470d0775c072bd8" dependencies = [ "convert_case 0.6.0", "quote", @@ -789,9 +791,9 @@ dependencies = [ [[package]] name = "chia-sdk-driver" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515961d6fb37f236bd34eaa895e1acf7a73b607a8d07b4fb4ab43d447bafdb32" +checksum = "58e79b60cf224d50468ee2c8c4fc340cdbf75185d25fffbb0dde10d6bf74917f" dependencies = [ "chia-bls 0.15.0", "chia-protocol", @@ -808,9 +810,9 @@ dependencies = [ [[package]] name = "chia-sdk-offers" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2824942d9353e0c81f3fc4b41697cf39d4190938703c0de501f210932e5ec745" +checksum = "60e1971c02703801882c1eb3d0944b7f7f734eed4cba759a8d5aa221f3fe6533" dependencies = [ "bech32", "chia-bls 0.15.0", @@ -830,9 +832,9 @@ dependencies = [ [[package]] name = "chia-sdk-signer" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f940399682651241696467a2216bdfe9bfb8e50e6a178733126d7d7587948b2d" +checksum = "ceacf2e67ab136876d93953f6eafff83741a32dffdb79db6a7423a6ef725a15b" dependencies = [ "chia-bls 0.15.0", "chia-consensus", @@ -845,9 +847,9 @@ dependencies = [ [[package]] name = "chia-sdk-test" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f4354402e24c5d96b9e2395cd51fb592e07831a3674d58bcc3e308d576f389" +checksum = "8766d35aede3b78992d24c7aa5e5b5b857a08811758dc2a84a099db939e71134" dependencies = [ "anyhow", "bip39", @@ -877,9 +879,9 @@ dependencies = [ [[package]] name = "chia-sdk-types" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1458db7a8f8425d98a8bf04e7f5264a6ea3d4c85524f5d2f6b0d292b00d31c" +checksum = "bd8e649d15459fd9f47230c2620f333885b258b24d653529c7f72035c029e239" dependencies = [ "chia-bls 0.15.0", "chia-consensus", @@ -891,6 +893,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "chia-sdk-utils" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b8f6b1e64bd47130e0b6a3e29ea0e18bcf29c347c0159abaf658e3a9cc23a5c" +dependencies = [ + "bech32", + "chia-protocol", + "hex", + "indexmap 2.5.0", + "rand 0.8.5", + "rand_chacha 0.3.1", + "thiserror", +] + [[package]] name = "chia-sha2" version = "0.15.0" @@ -938,23 +955,17 @@ dependencies = [ [[package]] name = "chia-wallet-sdk" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40898b07209d2da26e5bd01a39c1c82722a2e18c2d8e2247f8b13ffd18aa72d1" +checksum = "670dfb279ca9431828e526914356ce0be5bb25c44fa84d38387bdc7639be9d2c" dependencies = [ - "bech32", - "chia-protocol", "chia-sdk-client", "chia-sdk-driver", "chia-sdk-offers", "chia-sdk-signer", "chia-sdk-test", "chia-sdk-types", - "hex", - "indexmap 2.5.0", - "rand 0.8.5", - "rand_chacha 0.3.1", - "thiserror", + "chia-sdk-utils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8d72fd8b..d2e706d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ tauri-specta = "=2.0.0-rc.18" # Chia chia = "0.15.0" clvmr = "0.9.0" -chia-wallet-sdk = { version = "0.17.0", features = ["rustls"] } +chia-wallet-sdk = { version = "0.18.0", features = ["rustls"] } bip39 = "2.0.0" bech32 = "0.9.1" diff --git a/crates/sage-api/src/types.rs b/crates/sage-api/src/types.rs index 20fec214..5ac1cecf 100644 --- a/crates/sage-api/src/types.rs +++ b/crates/sage-api/src/types.rs @@ -1,5 +1,7 @@ mod amount; +mod nft_uri_kind; mod unit; pub use amount::*; +pub use nft_uri_kind::*; pub use unit::*; diff --git a/crates/sage-api/src/types/nft_uri_kind.rs b/crates/sage-api/src/types/nft_uri_kind.rs new file mode 100644 index 00000000..6b4b9ebb --- /dev/null +++ b/crates/sage-api/src/types/nft_uri_kind.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] +#[serde(rename_all = "snake_case")] +pub enum NftUriKind { + Data, + Metadata, + License, +} diff --git a/crates/sage-wallet/src/sync_manager.rs b/crates/sage-wallet/src/sync_manager.rs index b4deb1e4..0d0347ad 100644 --- a/crates/sage-wallet/src/sync_manager.rs +++ b/crates/sage-wallet/src/sync_manager.rs @@ -171,7 +171,10 @@ impl SyncManager { self.pending_coin_subscriptions.extend(coin_ids); } SyncCommand::ConnectionClosed(ip) => { - self.state.lock().await.remove_peer(ip); + self.state + .lock() + .await + .ban(ip, Duration::from_secs(300), "peer disconnected"); debug!("Peer {ip} disconnected"); } SyncCommand::SetDiscoverPeers(discover_peers) => { diff --git a/crates/sage-wallet/src/sync_manager/peer_discovery.rs b/crates/sage-wallet/src/sync_manager/peer_discovery.rs index c13d1540..a6af74e4 100644 --- a/crates/sage-wallet/src/sync_manager/peer_discovery.rs +++ b/crates/sage-wallet/src/sync_manager/peer_discovery.rs @@ -9,7 +9,7 @@ use chia::{ protocol::{Message, NewPeakWallet, ProtocolMessageTypes}, traits::Streamable, }; -use chia_wallet_sdk::{connect_peer, Peer}; +use chia_wallet_sdk::{connect_peer, Peer, PeerOptions}; use futures_lite::StreamExt; use futures_util::stream::FuturesUnordered; use tokio::{sync::mpsc, time::timeout}; @@ -153,8 +153,11 @@ impl SyncManager { let duration = self.options.connection_timeout; futures.push(async move { - let result = - timeout(duration, connect_peer(network_id, connector, socket_addr)).await; + let result = timeout( + duration, + connect_peer(network_id, connector, socket_addr, PeerOptions::default()), + ) + .await; (socket_addr, result) }); } @@ -229,7 +232,25 @@ impl SyncManager { let ip = peer.socket_addr().ip(); let sender = self.command_sender.clone(); - self.state.lock().await.add_peer(PeerInfo { + let mut state = self.state.lock().await; + + for (peer, height) in state.peers_with_heights() { + if message.height < height.saturating_sub(3) { + debug!( + "Peer {} is behind by more than 3 blocks, disconnecting", + peer.socket_addr() + ); + return false; + } else if message.height > height.saturating_add(3) { + state.ban( + peer.socket_addr().ip(), + Duration::from_secs(900), + "peer is behind", + ); + } + } + + state.add_peer(PeerInfo { peer: WalletPeer::new(peer), claimed_peak: message.height, header_hash: message.header_hash, @@ -242,7 +263,7 @@ impl SyncManager { .await .is_err() { - return; + break; } } diff --git a/crates/sage-wallet/src/wallet/nfts.rs b/crates/sage-wallet/src/wallet/nfts.rs index 9a6df86e..c3eb4eab 100644 --- a/crates/sage-wallet/src/wallet/nfts.rs +++ b/crates/sage-wallet/src/wallet/nfts.rs @@ -3,7 +3,8 @@ use chia::{ puzzles::nft::{NftMetadata, NFT_METADATA_UPDATER_PUZZLE_HASH}, }; use chia_wallet_sdk::{ - Conditions, Did, DidOwner, HashedPtr, Launcher, Nft, NftMint, SpendContext, StandardLayer, + Conditions, Did, DidOwner, HashedPtr, Launcher, MetadataUpdate, Nft, NftMint, SpendContext, + StandardLayer, }; use crate::WalletError; @@ -136,4 +137,60 @@ impl Wallet { Ok((ctx.take(), new_nft)) } + + pub async fn add_nft_uri( + &self, + nft_id: Bytes32, + fee: u64, + uri: MetadataUpdate, + hardened: bool, + reuse: bool, + ) -> Result<(Vec, Nft), WalletError> { + let Some(nft) = self.db.spendable_nft(nft_id).await? else { + return Err(WalletError::MissingNft(nft_id)); + }; + + let total_amount = fee as u128 + 1; + let coins = self.select_p2_coins(total_amount).await?; + let selected: u128 = coins.iter().map(|coin| coin.amount as u128).sum(); + + let change: u64 = (selected - total_amount) + .try_into() + .expect("change amount overflow"); + + let p2_puzzle_hash = self.p2_puzzle_hash(hardened, reuse).await?; + + let mut ctx = SpendContext::new(); + + let nft_metadata_ptr = ctx.alloc(&nft.info.metadata)?; + let nft = nft.with_metadata(HashedPtr::from_ptr(&ctx.allocator, nft_metadata_ptr)); + + let synthetic_key = self.db.synthetic_key(nft.info.p2_puzzle_hash).await?; + let p2 = StandardLayer::new(synthetic_key); + + let update_spend = uri.spend(&mut ctx)?; + let new_nft: Nft = nft.transfer_with_metadata( + &mut ctx, + &p2, + nft.info.p2_puzzle_hash, + update_spend, + Conditions::new(), + )?; + + let mut conditions = Conditions::new().assert_concurrent_spend(nft.coin.coin_id()); + + if fee > 0 { + conditions = conditions.reserve_fee(fee); + } + + if change > 0 { + conditions = conditions.create_coin(p2_puzzle_hash, change, Vec::new()); + } + + self.spend_p2_coins(&mut ctx, coins, conditions).await?; + + let new_nft = new_nft.with_metadata(ctx.serialize(&new_nft.info.metadata)?); + + Ok((ctx.take(), new_nft)) + } } diff --git a/src-tauri/src/commands/transactions.rs b/src-tauri/src/commands/transactions.rs index 59e4b341..418739b1 100644 --- a/src-tauri/src/commands/transactions.rs +++ b/src-tauri/src/commands/transactions.rs @@ -7,12 +7,13 @@ use chia::{ puzzles::nft::NftMetadata, }; use chia_wallet_sdk::{ - decode_address, encode_address, AggSigConstants, MAINNET_CONSTANTS, TESTNET11_CONSTANTS, + decode_address, encode_address, AggSigConstants, MetadataUpdate, MAINNET_CONSTANTS, + TESTNET11_CONSTANTS, }; use hex_literal::hex; use sage_api::{ - Amount, BulkMintNfts, BulkMintNftsResponse, CoinJson, CoinSpendJson, Input, InputKind, Output, - SpendBundleJson, TransactionSummary, + Amount, BulkMintNfts, BulkMintNftsResponse, CoinJson, CoinSpendJson, Input, InputKind, + NftUriKind, Output, SpendBundleJson, TransactionSummary, }; use sage_database::{CatRow, Database}; use sage_wallet::{ @@ -508,6 +509,45 @@ pub async fn transfer_nft( summarize(&state, &wallet, coin_spends, ConfirmationInfo::default()).await } +#[command] +#[specta] +pub async fn add_nft_uri( + state: State<'_, AppState>, + nft_id: String, + uri: String, + kind: NftUriKind, + fee: Amount, +) -> Result { + let state = state.lock().await; + let wallet = state.wallet()?; + + if !state.keychain.has_secret_key(wallet.fingerprint) { + return Err(Error::no_secret_key()); + } + + let (launcher_id, prefix) = decode_address(&nft_id)?; + + if prefix != "nft" { + return Err(Error::invalid_prefix(&prefix)); + } + + let Some(fee) = fee.to_mojos(state.unit.decimals) else { + return Err(Error::invalid_amount(&fee)); + }; + + let uri = match kind { + NftUriKind::Data => MetadataUpdate::NewDataUri(uri), + NftUriKind::Metadata => MetadataUpdate::NewMetadataUri(uri), + NftUriKind::License => MetadataUpdate::NewLicenseUri(uri), + }; + + let (coin_spends, _new_nft) = wallet + .add_nft_uri(launcher_id.into(), fee, uri, false, true) + .await?; + + summarize(&state, &wallet, coin_spends, ConfirmationInfo::default()).await +} + #[command] #[specta] pub async fn transfer_did( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1b144ea9..0593aa1f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -75,6 +75,7 @@ pub fn run() { commands::bulk_mint_nfts, commands::transfer_nft, commands::transfer_did, + commands::add_nft_uri, commands::sign_transaction, commands::submit_transaction, // Peers diff --git a/src/bindings.ts b/src/bindings.ts index dbbde41e..094b72d6 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -405,6 +405,14 @@ async transferDid(didId: string, address: string, fee: Amount) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("add_nft_uri", { nftId, uri, kind, fee }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async signTransaction(coinSpends: CoinSpendJson[]) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("sign_transaction", { coinSpends }) }; @@ -484,6 +492,7 @@ export type NftMint = { edition_number: number | null; edition_total: number | n export type NftRecord = { launcher_id: string; collection_id: string | null; collection_name: string | null; minter_did: string | null; owner_did: string | null; visible: boolean; sensitive_content: boolean; name: string | null; created_height: number | null; data_mime_type: string | null; data: string | null } export type NftSortMode = "name" | "recent" export type NftStatus = { nfts: number; visible_nfts: number; collections: number; visible_collections: number } +export type NftUriKind = "data" | "metadata" | "license" export type Output = { coin_id: string; amount: Amount; address: string; receiving: boolean; burning: boolean } export type PeerRecord = { ip_addr: string; port: number; trusted: boolean; peak_height: number } export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: string | null } diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index a585ae44..16557832 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -1,5 +1,4 @@ import { - ArrowLeftRight, CoinsIcon, ImagePlus, Images, @@ -26,10 +25,10 @@ export function Nav() { Profiles - + {/* Offers - + */} diff --git a/src/components/NftCard.tsx b/src/components/NftCard.tsx index 662be14c..1ee78126 100644 --- a/src/components/NftCard.tsx +++ b/src/components/NftCard.tsx @@ -1,10 +1,22 @@ -import { commands, NftRecord, TransactionSummary } from '@/bindings'; +import { + commands, + NftRecord, + NftUriKind, + TransactionSummary, +} from '@/bindings'; import { amount } from '@/lib/formTypes'; import { nftUri } from '@/lib/nftUri'; import { useWalletState } from '@/state'; import { zodResolver } from '@hookform/resolvers/zod'; import BigNumber from 'bignumber.js'; -import { EyeIcon, EyeOff, Flame, MoreVertical, SendIcon } from 'lucide-react'; +import { + EyeIcon, + EyeOff, + Flame, + LinkIcon, + MoreVertical, + SendIcon, +} from 'lucide-react'; import { PropsWithChildren, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router-dom'; @@ -35,6 +47,13 @@ import { FormMessage, } from './ui/form'; import { Input } from './ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select'; export interface NftProps { nft: NftRecord; @@ -53,6 +72,7 @@ export function NftCard({ nft, updateNfts }: NftProps) { const walletState = useWalletState(); const [transferOpen, setTransferOpen] = useState(false); + const [addUrlOpen, setAddUrlOpen] = useState(false); const [burnOpen, setBurnOpen] = useState(false); const [summary, setSummary] = useState(null); @@ -95,6 +115,42 @@ export function NftCard({ nft, updateNfts }: NftProps) { }); }; + const addUrlFormSchema = z.object({ + url: z.string().min(1, 'URL is required'), + kind: z.string().min(1, 'Kind is required'), + fee: amount(walletState.sync.unit.decimals).refine( + (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), + 'Not enough funds to cover the fee', + ), + }); + + const addUrlForm = useForm>({ + resolver: zodResolver(addUrlFormSchema), + defaultValues: { + url: '', + kind: 'data', + fee: '0', + }, + }); + + const onAddUrlSubmit = (values: z.infer) => { + commands + .addNftUri( + nft.launcher_id, + values.url, + values.kind as NftUriKind, + values.fee, + ) + .then((result) => { + setAddUrlOpen(false); + if (result.status === 'error') { + console.error('Failed to add NFT URL', result.error); + } else { + setSummary(result.data); + } + }); + }; + const burnFormSchema = z.object({ fee: amount(walletState.sync.unit.decimals).refine( (amount) => BigNumber(walletState.sync.balance).gte(amount || 0), @@ -169,6 +225,19 @@ export function NftCard({ nft, updateNfts }: NftProps) { Transfer + { + e.stopPropagation(); + addUrlForm.reset(); + setAddUrlOpen(true); + }} + disabled={!nft.created_height} + > + + Add URL + + { @@ -228,6 +297,7 @@ export function NftCard({ nft, updateNfts }: NftProps) { )} /> + )} /> + + + + + + + + @@ -283,6 +439,7 @@ export function NftCard({ nft, updateNfts }: NftProps) { )} /> +