forked from Kieran/snort
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
b93a9a8b72 |
@ -23,6 +23,7 @@
|
||||
--border-color: var(--gray);
|
||||
}
|
||||
|
||||
.modal-body .tab.active,
|
||||
.modal-body button.secondary:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
33
packages/app/src/Hooks/useRates.tsx
Normal file
33
packages/app/src/Hooks/useRates.tsx
Normal 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]),
|
||||
};
|
||||
}
|
@ -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);
|
||||
|
||||
|
41
packages/app/src/Pages/trading/create-offer.css
Normal file
41
packages/app/src/Pages/trading/create-offer.css
Normal 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);
|
||||
}
|
81
packages/app/src/Pages/trading/create-offer.tsx
Normal file
81
packages/app/src/Pages/trading/create-offer.tsx
Normal 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>
|
||||
}
|
9
packages/app/src/Pages/trading/index.tsx
Normal file
9
packages/app/src/Pages/trading/index.tsx
Normal 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>
|
39
packages/app/src/Pages/trading/offers.tsx
Normal file
39
packages/app/src/Pages/trading/offers.tsx
Normal 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>
|
||||
</>
|
||||
}
|
@ -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")));
|
||||
|
14
packages/app/src/trading/index.ts
Normal file
14
packages/app/src/trading/index.ts
Normal 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;
|
||||
}
|
105
packages/app/src/trading/mostro.ts
Normal file
105
packages/app/src/trading/mostro.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user