Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/sage-api/endpoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"view_offer": true,
"import_offer": true,
"get_offers": true,
"get_offers_for_asset": true,
"get_offer": true,
"delete_offer": true,
"cancel_offer": true,
Expand Down
12 changes: 12 additions & 0 deletions crates/sage-api/src/requests/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ pub struct GetOffersResponse {
pub offers: Vec<OfferRecord>,
}

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

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
pub struct GetOffersForAssetResponse {
pub offers: Vec<OfferRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
pub struct GetOffer {
Expand Down
67 changes: 67 additions & 0 deletions crates/sage-database/src/tables/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ impl Database {
pub async fn update_offer_status(&self, offer_id: Bytes32, status: OfferStatus) -> Result<()> {
update_offer_status(&self.pool, offer_id, status).await
}

pub async fn offers_for_asset(
&self,
asset_id: Bytes32,
status: Option<OfferStatus>,
) -> Result<Vec<OfferRow>> {
offers_for_asset(&self.pool, asset_id, status).await
}
}

impl DatabaseTx<'_> {
Expand Down Expand Up @@ -89,6 +97,65 @@ impl DatabaseTx<'_> {
) -> Result<()> {
update_offer_status(&mut *self.tx, offer_id, status).await
}

pub async fn offers_for_asset(
&mut self,
asset_id: Bytes32,
status: Option<OfferStatus>,
) -> Result<Vec<OfferRow>> {
offers_for_asset(&mut *self.tx, asset_id, status).await
}
}

async fn offers_for_asset(
conn: impl SqliteExecutor<'_>,
asset_id: Bytes32,
status: Option<OfferStatus>,
) -> Result<Vec<OfferRow>> {
let status_value = status.map(|s| s as u8);
let asset_id_ref = asset_id.as_ref();

let rows = sqlx::query!(
"SELECT
offers.hash as offer_id,
encoded_offer,
fee,
status,
expiration_height,
expiration_timestamp,
inserted_timestamp
FROM offers
INNER JOIN offer_assets ON offers.id = offer_assets.offer_id
INNER JOIN assets ON offer_assets.asset_id = assets.id
WHERE assets.hash = ? AND offers.status = ? OR ? IS NULL
ORDER BY inserted_timestamp DESC",
asset_id_ref,
status_value,
status_value
)
.fetch_all(conn)
.await?;

rows.into_iter()
.map(|row| {
Ok(OfferRow {
offer_id: row.offer_id.convert()?,
encoded_offer: row.encoded_offer,
expiration_height: row.expiration_height.map(|h| h as u32),
expiration_timestamp: row.expiration_timestamp.map(|t| t as u64),
fee: row.fee.convert()?,
status: match row.status {
0 => OfferStatus::Pending,
1 => OfferStatus::Active,
2 => OfferStatus::Completed,
3 => OfferStatus::Cancelled,
4 => OfferStatus::Expired,
_ => return Err(crate::DatabaseError::InvalidEnumVariant),
},
inserted_timestamp: row.inserted_timestamp as u64,
})
})
.collect()
}

async fn offer_assets(
Expand Down
34 changes: 31 additions & 3 deletions crates/sage/src/endpoints/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ use itertools::Itertools;
use sage_api::{
Amount, CancelOffer, CancelOfferResponse, CancelOffers, CancelOffersResponse, CombineOffers,
CombineOffersResponse, DeleteOffer, DeleteOfferResponse, GetOffer, GetOfferResponse, GetOffers,
GetOffersResponse, ImportOffer, ImportOfferResponse, MakeOffer, MakeOfferResponse, NftRoyalty,
OfferAmount, OfferAsset, OfferRecord, OfferRecordStatus, OfferSummary, OptionAssets, TakeOffer,
TakeOfferResponse, ViewOffer, ViewOfferResponse,
GetOffersForAsset, GetOffersForAssetResponse, GetOffersResponse, ImportOffer,
ImportOfferResponse, MakeOffer, MakeOfferResponse, NftRoyalty, OfferAmount, OfferAsset,
OfferRecord, OfferRecordStatus, OfferSummary, OptionAssets, TakeOffer, TakeOfferResponse,
ViewOffer, ViewOfferResponse,
};
use sage_assets::fetch_uris_with_hash;
use sage_database::{AssetKind, OfferRow, OfferStatus, OfferedAsset};
Expand Down Expand Up @@ -536,6 +537,33 @@ impl Sage {
Ok(GetOffersResponse { offers: records })
}

pub async fn get_offers_for_asset(
&self,
req: GetOffersForAsset,
) -> Result<GetOffersForAssetResponse> {
let wallet = self.wallet()?;

// Try to parse as different asset types based on prefix
let asset_id = if req.asset_id.starts_with("nft") {
parse_nft_id(req.asset_id)?
} else if req.asset_id.starts_with("option") {
parse_option_id(req.asset_id)?
} else {
parse_asset_id(req.asset_id)?
};

let offers = wallet
.db
.offers_for_asset(asset_id, Some(OfferStatus::Active))
.await?;
let mut records = Vec::new();

for offer in offers {
records.push(self.offer_record(&wallet, offer).await?);
}
Ok(GetOffersForAssetResponse { offers: records })
}

pub async fn get_offer(&self, req: GetOffer) -> Result<GetOfferResponse> {
let wallet = self.wallet()?;

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub fn run() {
commands::view_offer,
commands::import_offer,
commands::get_offers,
commands::get_offers_for_asset,
commands::get_offer,
commands::delete_offer,
commands::cancel_offer,
Expand Down
5 changes: 5 additions & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ async importOffer(req: ImportOffer) : Promise<ImportOfferResponse> {
async getOffers(req: GetOffers) : Promise<GetOffersResponse> {
return await TAURI_INVOKE("get_offers", { req });
},
async getOffersForAsset(req: GetOffersForAsset) : Promise<GetOffersForAssetResponse> {
return await TAURI_INVOKE("get_offers_for_asset", { req });
},
async getOffer(req: GetOffer) : Promise<GetOfferResponse> {
return await TAURI_INVOKE("get_offer", { req });
},
Expand Down Expand Up @@ -459,6 +462,8 @@ export type GetNftsResponse = { nfts: NftRecord[]; total: number }
export type GetOffer = { offer_id: string }
export type GetOfferResponse = { offer: OfferRecord }
export type GetOffers = Record<string, never>
export type GetOffersForAsset = { asset_id: string }
export type GetOffersForAssetResponse = { offers: OfferRecord[] }
export type GetOffersResponse = { offers: OfferRecord[] }
export type GetOption = { option_id: string }
export type GetOptionResponse = { option: OptionRecord | null }
Expand Down
12 changes: 2 additions & 10 deletions src/components/OfferSummaryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OfferAsset, OfferRecord } from '@/bindings';
import { NumberFormat } from '@/components/NumberFormat';
import { formatTimestamp, fromMojos } from '@/lib/utils';
import { formatTimestamp, fromMojos, getOfferStatus } from '@/lib/utils';
import { t } from '@lingui/core/macro';
import BigNumber from 'bignumber.js';
import { AssetIcon } from './AssetIcon';
Expand All @@ -16,15 +16,7 @@ export function OfferSummaryCard({ record, content }: OfferSummaryCardProps) {
<div className='flex justify-between'>
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
<div className='flex flex-col gap-1'>
<div>
{record.status === 'active'
? 'Pending'
: record.status === 'completed'
? 'Taken'
: record.status === 'cancelled'
? 'Cancelled'
: 'Expired'}
</div>
<div>{getOfferStatus(record.status)}</div>
<div className='text-muted-foreground text-sm'>
{new Date(record.creation_timestamp * 1000).toLocaleString()}
</div>
Expand Down
78 changes: 78 additions & 0 deletions src/components/selectors/AssetSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { platform } from '@tauri-apps/plugin-os';
import {
AlertTriangle,
ArrowUpToLine,
FilePenLine,
HandCoins,
Expand Down Expand Up @@ -51,6 +52,7 @@ export function AssetSelector({
const [tokenIds, setTokenIds] = useState<number[]>([]);
const [nftIds, setNftIds] = useState<number[]>([]);
const [optionIds, setOptionIds] = useState<number[]>([]);
const [assetsInOffers, setAssetsInOffers] = useState<Set<string>>(new Set());

useEffect(() => {
if (!offering) return;
Expand All @@ -61,6 +63,42 @@ export function AssetSelector({
.catch(console.error);
}, [offering]);

// Check which assets are part of open offers
useEffect(() => {
if (!offering) return;

const checkAssetsInOffers = async () => {
const assetsToCheck = [
...assets.nfts.filter((nft) => nft),
...assets.options.filter((option) => option),
];

const newAssetsInOffers = new Set<string>();

for (const assetId of assetsToCheck) {
if (assetId) {
try {
const response = await commands.getOffersForAsset({
asset_id: assetId,
});
if (response.offers.some((offer) => offer.status === 'active')) {
newAssetsInOffers.add(assetId);
}
} catch (error) {
console.error(
`Failed to check offers for asset ${assetId}:`,
error,
);
}
}
}

setAssetsInOffers(newAssetsInOffers);
};

checkAssetsInOffers();
}, [offering, assets.nfts, assets.options]);

// Generate unique IDs for new items
const generateId = () => Date.now() + Math.random();

Expand Down Expand Up @@ -299,6 +337,26 @@ export function AssetSelector({
onChange={(e) => updateNft(i, e.target.value)}
/>
)}

{/* Warning icon for assets in offers */}
{offering && nft && assetsInOffers.has(nft) && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='flex items-center px-2 bg-accent text-accent-foreground flex-shrink-0 flex-grow-0 h-12'>
<AlertTriangle
className='h-4 w-4'
aria-label={t`This NFT is part of an open offer`}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<Trans>This NFT is part of an open offer</Trans>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

<Button
variant='outline'
className='!border-l-0 !rounded-l-none flex-shrink-0 flex-grow-0 h-12 px-3'
Expand Down Expand Up @@ -340,6 +398,26 @@ export function AssetSelector({
onChange={(e) => updateOption(i, e.target.value)}
/>
)}

{/* Warning icon for assets in offers */}
{offering && option && assetsInOffers.has(option) && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='flex items-center px-2 bg-accent text-accent-foreground flex-shrink-0 flex-grow-0 h-12'>
<AlertTriangle
className='h-4 w-4'
aria-label={t`This option is part of an open offer`}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<Trans>This option is part of an open offer</Trans>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

<Button
variant='outline'
className='!border-l-0 !rounded-l-none flex-shrink-0 flex-grow-0 h-12 px-3'
Expand Down
Loading
Loading