Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
092e5c7
reusable profile component
dkackman Jul 30, 2025
7562176
wording
dkackman Jul 31, 2025
b6c3e05
Merge remote-tracking branch 'upstream/main' into mintgarden-component
dkackman Aug 2, 2025
d866653
Merge branch 'main' into mintgarden-component
dkackman Aug 2, 2025
cec2065
adapt to AssetIcon change
dkackman Aug 2, 2025
ae1b3ae
Merge branch 'main' into mintgarden-component
dkackman Aug 2, 2025
55df792
service to handle cache and rate limiting
dkackman Aug 3, 2025
e65fc06
use tauri plugin store for mintgarden cache
dkackman Aug 3, 2025
dc10495
Merge remote-tracking branch 'upstream/main' into mintgarden-component
dkackman Aug 3, 2025
35cc740
re-enable mintgarden profile on nft minter view
dkackman Aug 3, 2025
852eeef
simplify card view
dkackman Aug 3, 2025
60092ce
rename Profile to ProfileCard
dkackman Aug 3, 2025
20e421c
profile page
dkackman Aug 3, 2025
5155526
Merge remote-tracking branch 'upstream/main' into profile-page
dkackman Aug 3, 2025
5ded5b4
owned_did
dkackman Aug 3, 2025
460af74
navigate to profile page
dkackman Aug 3, 2025
0c5ffe0
parse did id
dkackman Aug 4, 2025
78ae2f3
clean up and unify did code
dkackman Aug 4, 2025
7ceb73e
bulk load mint garden api
dkackman Aug 6, 2025
8467f85
Merge remote-tracking branch 'upstream/main' into profile-page
dkackman Aug 6, 2025
f2fd25d
passing coin id is wrong
dkackman Aug 6, 2025
fde90b3
remove unneeded code
dkackman Aug 6, 2025
96842c2
Merge remote-tracking branch 'upstream/main' into profile-page
dkackman Aug 11, 2025
d204a77
rate limit the bulk load
dkackman Aug 11, 2025
c96381d
Merge remote-tracking branch 'upstream/main' into profile-page
dkackman Aug 14, 2025
4307adb
re-incorporate DidInfo
dkackman Aug 14, 2025
95f9339
Merge remote-tracking branch 'upstream/main' into profile-page
dkackman Aug 16, 2025
acda8c6
restyle
dkackman Aug 16, 2025
3e34ff0
Merge remote-tracking branch 'upstream' into profile-page
dkackman Aug 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 174 additions & 91 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/sage-api/endpoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"get_all_cats": true,
"get_token": true,
"get_dids": true,
"get_profile": true,
"get_minter_did_ids": true,
"get_options": true,
"get_option": true,
Expand Down
12 changes: 12 additions & 0 deletions crates/sage-api/src/requests/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@ pub struct GetTokenResponse {
pub token: Option<TokenRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
pub struct GetProfile {
pub launcher_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
pub struct GetProfileResponse {
pub did: Option<DidRecord>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
pub struct GetDids {}
Expand Down
67 changes: 64 additions & 3 deletions crates/sage-database/src/tables/assets/did.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,67 @@ pub struct DidRow {
}

impl Database {
pub async fn owned_did(&self, hash: Bytes32) -> Result<Option<DidRow>> {
let hash = hash.as_ref();

query!(
"
SELECT
asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url,
asset_description, asset_is_visible, asset_is_sensitive_content,
asset_hidden_puzzle_hash, owned_coins.created_height, spent_height,
parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,
metadata, recovery_list_hash, num_verifications_required,
offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp,
clawback_expiration_seconds AS 'clawback_timestamp?'
FROM owned_coins
INNER JOIN dids ON dids.asset_id = owned_coins.asset_id
WHERE owned_coins.asset_hash = ?
",
hash
)
.fetch_optional(&self.pool)
.await?
.map(|row| {
Ok(DidRow {
asset: Asset {
hash: row.asset_hash.convert()?,
name: row.asset_name,
ticker: row.asset_ticker,
precision: row.asset_precision.convert()?,
icon_url: row.asset_icon_url,
description: row.asset_description,
is_visible: row.asset_is_visible,
is_sensitive_content: row.asset_is_sensitive_content,
hidden_puzzle_hash: row.asset_hidden_puzzle_hash.convert()?,
kind: AssetKind::Did,
},
did_info: DidCoinInfo {
metadata: row.metadata.into(),
recovery_list_hash: row.recovery_list_hash.convert()?,
num_verifications_required: row.num_verifications_required.convert()?,
},
coin_row: CoinRow {
coin: Coin::new(
row.parent_coin_hash.convert()?,
row.puzzle_hash.convert()?,
row.amount.convert()?,
),
p2_puzzle_hash: row.p2_puzzle_hash.convert()?,
kind: CoinKind::Did,
mempool_item_hash: None,
offer_hash: row.offer_hash.convert()?,
clawback_timestamp: row.clawback_timestamp.map(|t| t as u64),
created_height: row.created_height.convert()?,
spent_height: row.spent_height.convert()?,
created_timestamp: row.created_timestamp.convert()?,
spent_timestamp: row.spent_timestamp.convert()?,
},
})
})
.transpose()
}

pub async fn owned_dids(&self) -> Result<Vec<DidRow>> {
query!(
"
Expand All @@ -27,8 +88,8 @@ impl Database {
asset_hidden_puzzle_hash, owned_coins.created_height, spent_height,
parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,
metadata, recovery_list_hash, num_verifications_required,
offer_hash, created_timestamp, spent_timestamp,
clawback_expiration_seconds AS clawback_timestamp
offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp,
clawback_expiration_seconds AS 'clawback_timestamp?'
FROM owned_coins
INNER JOIN dids ON dids.asset_id = owned_coins.asset_id
ORDER BY asset_name ASC
Expand Down Expand Up @@ -66,7 +127,7 @@ impl Database {
kind: CoinKind::Did,
mempool_item_hash: None,
offer_hash: row.offer_hash.convert()?,
clawback_timestamp: row.clawback_timestamp.convert()?,
clawback_timestamp: row.clawback_timestamp.map(|t| t as u64),
created_height: row.created_height.convert()?,
spent_height: row.spent_height.convert()?,
created_timestamp: row.created_timestamp.convert()?,
Expand Down
28 changes: 25 additions & 3 deletions crates/sage/src/endpoints/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use sage_api::{
GetNftCollections, GetNftCollectionsResponse, GetNftData, GetNftDataResponse, GetNftIcon,
GetNftIconResponse, GetNftResponse, GetNftThumbnail, GetNftThumbnailResponse, GetNfts,
GetNftsResponse, GetOption, GetOptionResponse, GetOptions, GetOptionsResponse,
GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount,
GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetToken,
GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions,
GetPendingTransactions, GetPendingTransactionsResponse, GetProfile, GetProfileResponse,
GetSpendableCoinCount, GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse,
GetToken, GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions,
GetTransactionsResponse, GetVersion, GetVersionResponse, NftCollectionRecord, NftData,
NftRecord, NftSortMode as ApiNftSortMode, OptionRecord, OptionSortMode as ApiOptionSortMode,
PendingTransactionRecord, PerformDatabaseMaintenance, PerformDatabaseMaintenanceResponse,
Expand Down Expand Up @@ -359,6 +359,28 @@ impl Sage {
Ok(GetTokenResponse { token })
}

pub async fn get_profile(&self, req: GetProfile) -> Result<GetProfileResponse> {
let wallet = self.wallet()?;

let launcher_id = parse_did_id(req.launcher_id.clone())?;
let Some(row) = wallet.db.owned_did(launcher_id).await? else {
return Ok(GetProfileResponse { did: None });
};

let did = DidRecord {
launcher_id: Address::new(row.asset.hash, "did:chia:".to_string()).encode()?,
name: row.asset.name,
visible: row.asset.is_visible,
coin_id: hex::encode(row.coin_row.coin.coin_id()),
address: Address::new(row.coin_row.p2_puzzle_hash, self.network().prefix()).encode()?,
amount: Amount::u64(row.coin_row.coin.amount),
recovery_hash: row.did_info.recovery_list_hash.map(hex::encode),
created_height: row.coin_row.created_height,
};

Ok(GetProfileResponse { did: Some(did) })
}

pub async fn get_dids(&self, _req: GetDids) -> Result<GetDidsResponse> {
let wallet = self.wallet()?;

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-store": "^2.3.0",
"@use-gesture/react": "^10.3.1",
"@walletconnect/sign-client": "^2.17.2",
"@walletconnect/types": "^2.17.2",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ reqwest = { workspace = true }
# https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms
aws-lc-rs = { version = "1", features = ["bindgen"] }
tauri-plugin-sharesheet = "0.0.1"
tauri-plugin-store = "2.3.0"

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-window-state = { workspace = true }
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"clipboard-manager:default",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"opener:default"
"opener:default",
"store:default",
"store:allow-load",
"store:allow-save"
]
}
4 changes: 3 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub fn run() {
commands::get_all_cats,
commands::get_token,
commands::get_dids,
commands::get_profile,
commands::get_minter_did_ids,
commands::get_options,
commands::get_option,
Expand Down Expand Up @@ -146,7 +147,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_store::Builder::new().build());

#[cfg(not(mobile))]
{
Expand Down
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { WalletConnectProvider } from './contexts/WalletConnectContext';
import { WalletProvider } from './contexts/WalletContext';
import useInitialization from './hooks/useInitialization';
import { useTransactionFailures } from './hooks/useTransactionFailures';
import { initializeMintGardenService } from './lib/mintGardenConfig';
import { loadCatalog } from './i18n';
import Addresses from './pages/Addresses';
import CollectionMetaData from './pages/CollectionMetaData';
Expand Down Expand Up @@ -54,6 +55,7 @@ import { TokenList } from './pages/TokenList';
import Transaction from './pages/Transaction';
import { Transactions } from './pages/Transactions';
import Wallet from './pages/Wallet';
import Profile from './pages/Profile';

const router = createHashRouter(
createRoutesFromElements(
Expand Down Expand Up @@ -83,6 +85,7 @@ const router = createHashRouter(
<Route path='/dids' element={<Wallet />}>
<Route path='' element={<DidList />} />
<Route path='create' element={<CreateProfile />} />
<Route path=':launcher_id' element={<Profile />} />
</Route>
<Route path='/options' element={<Wallet />}>
<Route path='' element={<OptionList />} />
Expand Down Expand Up @@ -170,6 +173,11 @@ function AppInner() {
// Enable global transaction failure handling
useTransactionFailures();

// Initialize MintGarden service with rate limiting configuration
useEffect(() => {
initializeMintGardenService();
}, []);

useEffect(() => {
const initLocale = async () => {
await loadCatalog(locale);
Expand Down
5 changes: 5 additions & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ async getToken(req: GetToken) : Promise<GetTokenResponse> {
async getDids(req: GetDids) : Promise<GetDidsResponse> {
return await TAURI_INVOKE("get_dids", { req });
},
async getProfile(req: GetProfile) : Promise<GetProfileResponse> {
return await TAURI_INVOKE("get_profile", { req });
},
async getMinterDidIds(req: GetMinterDidIds) : Promise<GetMinterDidIdsResponse> {
return await TAURI_INVOKE("get_minter_did_ids", { req });
},
Expand Down Expand Up @@ -468,6 +471,8 @@ export type GetPeers = Record<string, never>
export type GetPeersResponse = { peers: PeerRecord[] }
export type GetPendingTransactions = Record<string, never>
export type GetPendingTransactionsResponse = { transactions: PendingTransactionRecord[] }
export type GetProfile = { launcher_id: string }
export type GetProfileResponse = { did: DidRecord | null }
export type GetSecretKey = { fingerprint: number }
export type GetSecretKeyResponse = { secrets: SecretKeyInfo | null }
export type GetSpendableCoinCount = { asset_id: string | null }
Expand Down
10 changes: 6 additions & 4 deletions src/components/AssetIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ export interface AssetIconProps {
kind: AssetKind;
revocation_address: string | null;
};
size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
className?: string;
}

const sizeClasses = {
sm: 'max-w-6 max-h-6',
md: 'max-w-8 max-h-8',
lg: 'max-w-10 max-h-10',
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-10 h-10',
xl: 'w-16 h-16',
xxl: 'w-24 h-24',
};

export function AssetIcon({
Expand Down
23 changes: 23 additions & 0 deletions src/components/DidInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ProfileCard from '@/components/ProfileCard';
import { t } from '@lingui/core/macro';
import { AddressItem } from './AddressItem';
export interface DidInfoProps {
did?: string | null;
title: string;
className?: string;
}

export function DidInfo({ did, title, className = '' }: DidInfoProps) {
if (!did) {
return (
<AddressItem label={title} address={t`None`} className={className} />
);
}

return (
<div className={className}>
<AddressItem label={title} address={did} />
<ProfileCard did={did} variant='compact' className='mt-1' />
</div>
);
}
Loading
Loading