Skip to content

Commit

Permalink
Merge pull request #117 from xch-dev/expiring-offers
Browse files Browse the repository at this point in the history
Expiring offers
  • Loading branch information
Rigidity authored Nov 24, 2024
2 parents f1239ea + 30f4523 commit 0ddacbb
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 84 deletions.
1 change: 1 addition & 0 deletions crates/sage-api/src/requests/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub struct MakeOffer {
pub requested_assets: Assets,
pub offered_assets: Assets,
pub fee: Amount,
pub expires_at_second: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
Expand Down
2 changes: 1 addition & 1 deletion crates/sage-wallet/src/sync_manager/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Default for Timeouts {
nft_uri_delay: Duration::from_secs(1),
puzzle_delay: Duration::from_secs(1),
transaction_delay: Duration::from_secs(1),
offer_delay: Duration::from_secs(1),
offer_delay: Duration::from_secs(5),
connection: Duration::from_secs(3),
initial_peak: Duration::from_secs(2),
remove_subscription: Duration::from_secs(3),
Expand Down
3 changes: 3 additions & 0 deletions crates/sage-wallet/src/wallet/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ mod tests {
cats: indexmap! { asset_id => 1000 },
nfts: IndexMap::new(),
},
None,
false,
true,
)
Expand Down Expand Up @@ -134,6 +135,7 @@ mod tests {
},
},
},
None,
false,
true,
)
Expand Down Expand Up @@ -208,6 +210,7 @@ mod tests {
cats: IndexMap::new(),
nfts: IndexMap::new(),
},
None,
false,
true,
)
Expand Down
7 changes: 6 additions & 1 deletion crates/sage-wallet/src/wallet/offer/make_offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl Wallet {
&self,
maker: MakerSide,
taker: TakerSide,
expires_at: Option<u64>,
hardened: bool,
reuse: bool,
) -> Result<UnsignedMakeOffer, WalletError> {
Expand Down Expand Up @@ -151,10 +152,14 @@ impl Wallet {
let trade_prices = calculate_trade_prices(&taker_amounts, maker.nfts.len())?;

let (assertions, builder) = builder.finish();
let extra_conditions = Conditions::new()
let mut extra_conditions = Conditions::new()
.extend(assertions)
.extend(maker_royalties.assertions());

if let Some(expires_at) = expires_at {
extra_conditions = extra_conditions.assert_before_seconds_absolute(expires_at);
}

// Spend the assets.
self.lock_assets(
&mut ctx,
Expand Down
1 change: 1 addition & 0 deletions crates/sage/src/endpoints/offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl Sage {
cats: requested_cats,
nfts: requested_nfts,
},
req.expires_at_second,
false,
true,
)
Expand Down
2 changes: 1 addition & 1 deletion src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ export type Login = { fingerprint: number }
export type LoginResponse = Record<string, never>
export type Logout = Record<string, never>
export type LogoutResponse = Record<string, never>
export type MakeOffer = { requested_assets: Assets; offered_assets: Assets; fee: Amount }
export type MakeOffer = { requested_assets: Assets; offered_assets: Assets; fee: Amount; expires_at_second: number | null }
export type MakeOfferResponse = { offer: string }
export type Network = { default_port: number; ticker: string; address_prefix: string; precision: number; genesis_challenge: string; agg_sig_me: string; dns_introducers: string[] }
export type NetworkConfig = { network_id: string; target_peers: number; discover_peers: boolean }
Expand Down
2 changes: 1 addition & 1 deletion src/pages/CreateProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function CreateProfile() {
className='pr-12'
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 sm:text-sm'>
<span className='text-gray-500 text-sm'>
{walletState.sync.unit.ticker}
</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/IssueToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default function IssueToken() {
className='pr-12'
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 sm:text-sm'>CAT</span>
<span className='text-gray-500 text-sm'>CAT</span>
</div>
</div>
</FormControl>
Expand All @@ -134,7 +134,7 @@ export default function IssueToken() {
className='pr-12'
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 sm:text-sm'>
<span className='text-gray-500 text-sm'>
{walletState.sync.unit.ticker}
</span>
</div>
Expand Down
195 changes: 155 additions & 40 deletions src/pages/MakeOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { clearOffer, useWalletState } from '@/state';
import { Switch } from '@/components/ui/switch';
import { clearOffer, useOfferState, useWalletState } from '@/state';
import {
HandCoins,
Handshake,
Expand All @@ -26,6 +27,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

export function MakeOffer() {
const state = useOfferState();
const walletState = useWalletState();
const navigate = useNavigate();

Expand All @@ -35,22 +37,29 @@ export function MakeOffer() {
commands
.makeOffer({
offered_assets: {
xch: walletState.offerAssets.xch || '0',
cats: walletState.offerAssets.cats.map((cat) => ({
xch: state.offered.xch || '0',
cats: state.offered.cats.map((cat) => ({
asset_id: cat.asset_id,
amount: cat.amount || '0',
})),
nfts: walletState.offerAssets.nfts,
nfts: state.offered.nfts,
},
requested_assets: {
xch: walletState.requestedAssets.xch || '0',
cats: walletState.requestedAssets.cats.map((cat) => ({
xch: state.requested.xch || '0',
cats: state.requested.cats.map((cat) => ({
asset_id: cat.asset_id,
amount: cat.amount || '0',
})),
nfts: walletState.requestedAssets.nfts,
nfts: state.requested.nfts,
},
fee: walletState.offerFee || '0',
fee: state.fee || '0',
expires_at_second:
state.expiration === null
? null
: Math.ceil(Date.now() / 1000) +
Number(state.expiration.days || '0') * 24 * 60 * 60 +
Number(state.expiration.hours || '0') * 60 * 60 +
Number(state.expiration.minutes || '0') * 60,
})
.then((result) => {
if (result.status === 'error') {
Expand All @@ -61,6 +70,12 @@ export function MakeOffer() {
});
};

const invalid =
state.expiration !== null &&
(isNaN(Number(state.expiration.days)) ||
isNaN(Number(state.expiration.hours)) ||
isNaN(Number(state.expiration.minutes)));

return (
<>
<Header title='New Offer' />
Expand All @@ -81,36 +96,11 @@ export function MakeOffer() {

<AssetSelector
prefix='offer'
assets={walletState.offerAssets}
assets={state.offered}
setAssets={(assets) =>
useWalletState.setState({ offerAssets: assets })
useOfferState.setState({ offered: assets })
}
/>

<div className='mt-4 flex flex-col space-y-1.5'>
<Label htmlFor='fee'>Network Fee</Label>
<div className='relative'>
<Input
id='fee'
type='text'
placeholder='0.00'
className='pr-12'
value={walletState.offerFee}
onChange={(e) =>
useWalletState.setState({ offerFee: e.target.value })
}
/>

<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span
className='text-gray-500 sm:text-sm'
id='price-currency'
>
{walletState.sync.unit.ticker}
</span>
</div>
</div>
</div>
</CardContent>
</Card>

Expand All @@ -128,17 +118,144 @@ export function MakeOffer() {

<AssetSelector
prefix='requested'
assets={walletState.requestedAssets}
assets={state.requested}
setAssets={(assets) =>
useWalletState.setState({ requestedAssets: assets })
useOfferState.setState({ requested: assets })
}
/>
</CardContent>
</Card>

<div className='flex flex-col gap-4'>
<div className='flex flex-col space-y-1.5'>
<Label htmlFor='fee'>Network Fee</Label>
<div className='relative'>
<Input
id='fee'
type='text'
placeholder='0.00'
className='pr-12'
value={state.fee}
onChange={(e) =>
useOfferState.setState({ fee: e.target.value })
}
/>

<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 text-sm' id='price-currency'>
{walletState.sync.unit.ticker}
</span>
</div>
</div>
</div>

<div className='flex flex-col gap-2'>
<div className='flex items-center gap-2'>
<label htmlFor='expiring'>Expiring offer</label>
<Switch
id='expiring'
checked={state.expiration !== null}
onCheckedChange={(value) => {
if (value) {
useOfferState.setState({
expiration: {
days: '1',
hours: '',
minutes: '',
},
});
} else {
useOfferState.setState({
expiration: null,
});
}
}}
/>
</div>

{state.expiration !== null && (
<div className='flex gap-2'>
<div className='relative'>
<Input
className='pr-12'
value={state.expiration.days}
placeholder='0'
onChange={(e) => {
if (state.expiration === null) return;

useOfferState.setState({
expiration: {
...state.expiration,
days: e.target.value,
},
});
}}
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 text-sm'>Days</span>
</div>
</div>

<div className='relative'>
<Input
className='pr-12'
value={state.expiration.hours}
placeholder='0'
onChange={(e) => {
if (state.expiration === null) return;

useOfferState.setState({
expiration: {
...state.expiration,
hours: e.target.value,
},
});
}}
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 text-sm'>Hours</span>
</div>
</div>

<div className='relative'>
<Input
className='pr-12'
value={state.expiration.minutes}
placeholder='0'
onChange={(e) => {
if (state.expiration === null) return;

useOfferState.setState({
expiration: {
...state.expiration,
minutes: e.target.value,
},
});
}}
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 text-sm'>Minutes</span>
</div>
</div>
</div>
)}
</div>
</div>
</div>

<div className='mt-4 flex gap-2'>
<Button onClick={make}>Create Offer</Button>
<Button
variant='outline'
onClick={() => {
clearOffer();
navigate('/offers', { replace: true });
}}
>
Cancel Offer
</Button>
<Button onClick={make} disabled={invalid}>
Create Offer
</Button>
</div>

<Dialog open={!!offer} onOpenChange={() => setOffer('')}>
Expand Down Expand Up @@ -182,8 +299,6 @@ interface AssetSelectorProps {
}

function AssetSelector({ prefix, assets, setAssets }: AssetSelectorProps) {
const walletState = useWalletState();

const [includeAmount, setIncludeAmount] = useState(!!assets.xch);

return (
Expand Down
4 changes: 2 additions & 2 deletions src/pages/MintNft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default function MintNft() {
className='pr-12'
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 sm:text-sm'>%</span>
<span className='text-gray-500 text-sm'>%</span>
</div>
</div>
</FormControl>
Expand All @@ -253,7 +253,7 @@ export default function MintNft() {
className='pr-12'
/>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>
<span className='text-gray-500 sm:text-sm'>
<span className='text-gray-500 text-sm'>
{walletState.sync.unit.ticker}
</span>
</div>
Expand Down
Loading

0 comments on commit 0ddacbb

Please sign in to comment.