Compare commits

...

1 Commits

Author SHA1 Message Date
b93a9a8b72
tmp 2023-10-09 09:51:16 +01:00
10 changed files with 330 additions and 5 deletions

View File

@ -23,6 +23,7 @@
--border-color: var(--gray);
}
.modal-body .tab.active,
.modal-body button.secondary:hover {
background-color: var(--gray);
}
@ -33,4 +34,4 @@
.modal.spotlight {
color: #fff;
}
}

View File

@ -0,0 +1,33 @@
import { bech32ToHex } from "@snort/shared";
import { EventKind, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
// Snort backend publishes rates
const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
export function useRates(symbol: string, leaveOpen = true) {
const sub = useMemo(() => {
const rb = new RequestBuilder(`rates:${symbol}`);
rb.withOptions({
leaveOpen,
});
rb.withFilter()
.kinds([1009 as EventKind])
.authors([bech32ToHex(SnortPubkey)])
.tag("d", [symbol])
.limit(1);
return rb;
}, [symbol]);
const data = useRequestBuilder(ReplaceableNoteStore, sub);
const tag = data?.data?.tags.find(a => a[0] === "d" && a[1] === symbol);
return {
time: data.data?.created_at,
ask: Number(tag?.[2]),
bid: Number(tag?.[3]),
low: Number(tag?.[4]),
hight: Number(tag?.[5]),
};
}

View File

@ -93,7 +93,7 @@ const NoteCreatorButton = () => {
);
};
const AccountHeader = () => {
export const AccountHeader = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
@ -203,7 +203,7 @@ const AccountHeader = () => {
);
};
function LogoHeader() {
export function LogoHeader() {
const { subscriptions } = useLogin();
const currentSubscription = getCurrentSubscription(subscriptions);

View File

@ -0,0 +1,41 @@
.create-offer h2 {
margin: 0;
font-weight: 600;
font-size: 16px;
line-height: 19px;
}
.create-offer h3 {
margin: 0;
color: var(--font-secondary-color);
font-size: 11px;
letter-spacing: 0.11em;
font-weight: 600;
line-height: 13px;
text-transform: uppercase;
}
.create-offer .premium {
display: flex;
border-radius: 12px;
border: 1px solid;
overflow: hidden;
}
.create-offer .premium>div {
flex: 1;
text-align: center;
padding: 10px 4px;
cursor: pointer;
}
.create-offer .premium>div:nth-of-type(1) {
min-width: 2em;
}
.create-offer .premium>div:nth-of-type(2) {
background-color: var(--success);
}
.create-offer .premium>div:nth-of-type(3) {
background-color: var(--error);
}

View File

@ -0,0 +1,81 @@
import "./create-offer.css";
import { useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import Tabs, { Tab } from "Element/Tabs";
import { useRates } from "Hooks/useRates";
import { unwrap } from "SnortUtils";
import AsyncButton from "Element/AsyncButton";
export function CreateOffer() {
const [side, setSide] = useState(0);
const [premium, setPremium] = useState(1);
const [amount, setAmount] = useState(10);
const [fiat, setFiat] = useState("USD");
const rate = useRates(`BTC${fiat}`, true);
const tabs = [
{
text: <FormattedMessage defaultMessage="Buy" />,
value: 0
},
{
text: <FormattedMessage defaultMessage="Sell" />,
value: 1
}
] as Array<Tab>
const rx = side === 0 ? rate.bid : rate.ask;
return <div className="create-offer flex-column g12">
<h2>
<FormattedMessage defaultMessage="New Offer" />
</h2>
<div className="flex-column g4">
<h3>
<FormattedMessage defaultMessage="Side" />
</h3>
<Tabs tab={unwrap(tabs.find(a => a.value === side))} tabs={tabs} setTab={t => setSide(t.value)} />
</div>
<div className="flex-column g4">
<h3>
<FormattedMessage defaultMessage="Fiat" />
</h3>
<select value={fiat} onChange={e => setFiat(e.target.value)}>
<option>USD</option>
<option>EUR</option>
</select>
</div>
<div className="flex g4">
<div className="flex-column g4">
<h3>
<FormattedMessage defaultMessage="Premium" />
</h3>
<div className="premium">
<div>
<FormattedNumber style="percent" value={premium / 100} />
</div>
<div onClick={() => setPremium(s => s + 1)}>+</div>
<div onClick={() => setPremium(s => s - 1)}>-</div>
</div>
</div>
<div className="flex-column g4">
<h3>
<FormattedMessage defaultMessage="Amount" />
</h3>
<input type="number" value={amount} onChange={e => setAmount(Number(e.target.value))} />
</div>
<div className="flex-column g4">
<h3>
<FormattedMessage defaultMessage="Rate" />
</h3>
<FormattedMessage defaultMessage="{n} @ {rate}" values={{
n: <FormattedNumber value={amount} style="currency" currency={fiat} />,
rate: <FormattedNumber value={rx * (1 + (premium / 100))} style="currency" currency={fiat} />
}} />
</div>
</div>
<AsyncButton onClick={() => { }}>
<FormattedMessage defaultMessage="Create Offer" />
</AsyncButton>
</div>
}

View File

@ -0,0 +1,9 @@
import { RouteObject } from "react-router-dom";
import { OfferList } from "./offers";
export const TradingRoutes = [
{
path: "/trading",
element: <OfferList />
}
] as Array<RouteObject>

View File

@ -0,0 +1,39 @@
import { SnortContext } from "@snort/system-react"
import Modal from "Element/Modal";
import { useContext, useEffect, useState } from "react"
import { FormattedMessage } from "react-intl";
import { TradeOffer } from "trading";
import { MostroTrading } from "trading/mostro";
import { CreateOffer } from "./create-offer";
export function OfferList() {
const system = useContext(SnortContext);
const [offers, setOffers] = useState<Array<TradeOffer>>([]);
const [newOffer, setNewOffer] = useState(false);
useEffect(() => {
const mostro = new MostroTrading(system, "7590450f6b4d2c6793cacc8c0894e2c6bd2e8a83894912e79335f8f98436d2d8");
mostro.listOffers().then(o => setOffers(o));
}, []);
return <>
<div className="flex f-space">
<h2>
<FormattedMessage defaultMessage="Offers" />
</h2>
<button type="button" onClick={() => setNewOffer(true)}>
<FormattedMessage defaultMessage="Create Offer" />
</button>
{newOffer && <Modal id="new-offer" onClose={() => setNewOffer(false)}>
<CreateOffer />
</Modal>}
</div>
<pre>{JSON.stringify(offers, undefined, 2)}</pre>
<div className="flex-column g8">
{offers.map(v => <div>
{v.side}
</div>)}
</div>
</>
}

View File

@ -46,6 +46,7 @@ import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
import { LoginStore } from "Login";
import { SnortDeckLayout } from "Pages/DeckLayout";
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
import { TradingRoutes } from "Pages/trading";
const WasmQueryOptimizer = {
expandFilter: (f: ReqFilter) => {
@ -191,6 +192,7 @@ export const router = createBrowserRouter([
...NewUserRoutes,
...WalletRoutes,
...SubscribeRoutes,
...TradingRoutes,
{
path: "/debug",
element: <DebugPage />,
@ -204,6 +206,7 @@ export const router = createBrowserRouter([
{
path: "/deck",
element: <SnortDeckLayout />,
children: RootTabRoutes,
loader: async () => {
if (!didInit) {
didInit = true;
@ -211,8 +214,7 @@ export const router = createBrowserRouter([
}
return null;
},
children: RootTabRoutes,
},
}
]);
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));

View File

@ -0,0 +1,14 @@
export interface TradingPlatform {
listOffers(): Promise<Array<TradeOffer>>;
}
export const enum TradeCurrency {
BTC = "BTC",
EUR = "EUR",
}
export interface TradeOffer {
side: "BUY" | "SELL";
currency: TradeCurrency;
rate: number;
}

View File

@ -0,0 +1,105 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EventKind, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system";
import { TradeOffer, TradingPlatform } from ".";
const MostroOfferKind = 30_000 as EventKind;
export class MostroTrading implements TradingPlatform {
#system: SystemInterface;
constructor(
system: SystemInterface,
readonly pubkey: string,
) {
this.#system = system;
}
async listOffers(): Promise<TradeOffer[]> {
const rb = new RequestBuilder("list-offers");
rb.withFilter().kinds([MostroOfferKind]).authors([this.pubkey]);
const offers = (await this.#system.Fetch(rb)) as Array<TaggedNostrEvent>;
return offers.map(v => {
const order = JSON.parse(v.content) as NewOrder;
return {
side: order.kind === OrderKind.Buy ? "BUY" : "SELL",
} as TradeOffer;
});
}
}
const enum MostroMessageAction {
Order = "Order",
TakeSell = "TakeSell",
TakeBuy = "TakeBuy",
PayInvoice = "PayInvoice",
FiatSent = "FiatSent",
Release = "Release",
Cancel = "Cancel",
CooperativeCancelInitiatedByYou = "CooperativeCancelInitiatedByYou",
CooperativeCancelInitiatedByPeer = "CooperativeCancelInitiatedByPeer",
DisputeInitiatedByYou = "DisputeInitiatedByYou",
DisputeInitiatedByPeer = "DisputeInitiatedByPeer",
CooperativeCancelAccepted = "CooperativeCancelAccepted",
BuyerInvoiceAccepted = "BuyerInvoiceAccepted",
SaleCompleted = "SaleCompleted",
PurchaseCompleted = "PurchaseCompleted",
HoldInvoicePaymentAccepted = "HoldInvoicePaymentAccepted",
HoldInvoicePaymentSettled = "HoldInvoicePaymentSettled",
HoldInvoicePaymentCanceled = "HoldInvoicePaymentCanceled",
WaitingSellerToPay = "WaitingSellerToPay",
WaitingBuyerInvoice = "WaitingBuyerInvoice",
AddInvoice = "AddInvoice",
BuyerTookOrder = "BuyerTookOrder",
RateUser = "RateUser",
CantDo = "CantDo",
Received = "Received",
Dispute = "Dispute",
AdminCancel = "AdminCancel",
AdminSettle = "AdminSettle",
}
interface MostroMessage<T> {
version: number;
order_id?: string;
pubkey?: string;
action: MostroMessageAction;
content?: T;
}
const enum OrderKind {
Buy = "Buy",
Sell = "Sell",
}
const enum OrderStatus {
Active = "Active",
Canceled = "Canceled",
CanceledByAdmin = "CanceledByAdmin",
SettledByAdmin = "SettledByAdmin",
CompletedByAdmin = "CompletedByAdmin",
Dispute = "Dispute",
Expired = "Expired",
FiatSent = "FiatSent",
SettledHoldInvoice = "SettledHoldInvoice",
Pending = "Pending",
Success = "Success",
WaitingBuyerInvoice = "WaitingBuyerInvoice",
WaitingPayment = "WaitingPayment",
CooperativelyCanceled = "CooperativelyCanceled",
}
interface NewOrder {
id?: string;
kind: OrderKind;
status: OrderStatus;
amount: number;
fiat_code: string;
fiat_amount: number;
payment_method: string;
premium: number;
master_buyer_pubkey?: string;
master_seller_pubkey?: string;
buyer_invoice?: string;
created_at?: number;
}