forked from Kieran/snort
tmp
This commit is contained in:
parent
585f031ce1
commit
b93a9a8b72
@ -23,6 +23,7 @@
|
|||||||
--border-color: var(--gray);
|
--border-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body .tab.active,
|
||||||
.modal-body button.secondary:hover {
|
.modal-body button.secondary:hover {
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
@ -33,4 +34,4 @@
|
|||||||
|
|
||||||
.modal.spotlight {
|
.modal.spotlight {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
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 navigate = useNavigate();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ const AccountHeader = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function LogoHeader() {
|
export function LogoHeader() {
|
||||||
const { subscriptions } = useLogin();
|
const { subscriptions } = useLogin();
|
||||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
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 { LoginStore } from "Login";
|
||||||
import { SnortDeckLayout } from "Pages/DeckLayout";
|
import { SnortDeckLayout } from "Pages/DeckLayout";
|
||||||
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
||||||
|
import { TradingRoutes } from "Pages/trading";
|
||||||
|
|
||||||
const WasmQueryOptimizer = {
|
const WasmQueryOptimizer = {
|
||||||
expandFilter: (f: ReqFilter) => {
|
expandFilter: (f: ReqFilter) => {
|
||||||
@ -191,6 +192,7 @@ export const router = createBrowserRouter([
|
|||||||
...NewUserRoutes,
|
...NewUserRoutes,
|
||||||
...WalletRoutes,
|
...WalletRoutes,
|
||||||
...SubscribeRoutes,
|
...SubscribeRoutes,
|
||||||
|
...TradingRoutes,
|
||||||
{
|
{
|
||||||
path: "/debug",
|
path: "/debug",
|
||||||
element: <DebugPage />,
|
element: <DebugPage />,
|
||||||
@ -204,6 +206,7 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "/deck",
|
path: "/deck",
|
||||||
element: <SnortDeckLayout />,
|
element: <SnortDeckLayout />,
|
||||||
|
children: RootTabRoutes,
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
if (!didInit) {
|
if (!didInit) {
|
||||||
didInit = true;
|
didInit = true;
|
||||||
@ -211,8 +214,7 @@ export const router = createBrowserRouter([
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
children: RootTabRoutes,
|
}
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
|
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