mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Merge 5269351094d3280a80136a7e8b70bd5efa27c952 into f89a5cee9abd58ebb3dca17e9c0b5d2ff3889b45
This commit is contained in:
18
api/migrations/0055_order_description.py
Normal file
18
api/migrations/0055_order_description.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.9 on 2025-07-31 20:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0054_order_password'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=None, max_length=240, null=True),
|
||||
),
|
||||
]
|
||||
@ -173,6 +173,14 @@ class Order(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# optionally makers can set a description to give more details about the contract terms
|
||||
description = models.TextField(
|
||||
max_length=240,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# how many sats at creation and at last check (relevant for marked to market)
|
||||
t0_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
|
||||
@ -99,6 +99,7 @@ class OrderViewSchema:
|
||||
- `maker_status`
|
||||
- `taker_status`
|
||||
- `price_now`
|
||||
- `description`
|
||||
|
||||
### Order Status
|
||||
|
||||
|
||||
@ -406,6 +406,11 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
required=False,
|
||||
help_text="The index of the last message sent in the trade chatroom",
|
||||
)
|
||||
description = serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Order description",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@ -491,6 +496,7 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
"latitude",
|
||||
"longitude",
|
||||
"chat_last_index",
|
||||
"description",
|
||||
)
|
||||
|
||||
|
||||
@ -590,6 +596,7 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
||||
"latitude",
|
||||
"longitude",
|
||||
"password",
|
||||
"description",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
from unittest.mock import MagicMock, Mock, mock_open, patch
|
||||
from unittest.mock import Mock, mock_open, patch
|
||||
|
||||
import numpy as np
|
||||
from decouple import config
|
||||
from django.test import TestCase
|
||||
|
||||
from api.models import Order
|
||||
from api.utils import (
|
||||
base91_to_hex,
|
||||
bitcoind_rpc,
|
||||
|
||||
@ -10,7 +10,6 @@ from base91 import decode, encode
|
||||
from decouple import config
|
||||
|
||||
from api.errors import new_error
|
||||
from api.models import Order
|
||||
|
||||
logger = logging.getLogger("api.utils")
|
||||
|
||||
|
||||
@ -119,6 +119,7 @@ class MakerView(CreateAPIView):
|
||||
latitude = serializer.data.get("latitude")
|
||||
longitude = serializer.data.get("longitude")
|
||||
password = serializer.data.get("password")
|
||||
description = serializer.data.get("description")
|
||||
|
||||
# Optional params
|
||||
if public_duration is None:
|
||||
@ -166,6 +167,7 @@ class MakerView(CreateAPIView):
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
password=password,
|
||||
description=description,
|
||||
)
|
||||
|
||||
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
||||
@ -282,6 +284,7 @@ class OrderView(viewsets.ViewSet):
|
||||
data["is_disputed"] = order.is_disputed
|
||||
data["ur_nick"] = request.user.username
|
||||
data["satoshis_now"] = order.last_satoshis
|
||||
data["description"] = order.description
|
||||
|
||||
# Add whether hold invoices are LOCKED (ACCEPTED)
|
||||
# Is there a maker bond? If so, True if locked, False otherwise
|
||||
|
||||
49
frontend/src/components/Dialogs/OrderDescription.tsx
Normal file
49
frontend/src/components/Dialogs/OrderDescription.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Button,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import { Order } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClickBack: () => void;
|
||||
onClickDone: () => void;
|
||||
order: Order;
|
||||
}
|
||||
|
||||
const OrderDescriptionDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onClickBack,
|
||||
onClickDone,
|
||||
order,
|
||||
}: Props): React.JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('Order description')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('{{description}}', { description: order?.description })}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClickBack} autoFocus>{t('Go back')}</Button>
|
||||
<Button onClick={onClickDone}>{t('Continue')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDescriptionDialog;
|
||||
@ -14,3 +14,4 @@ export { default as F2fMapDialog } from './F2fMap';
|
||||
export { default as UpdateDialog } from './Update';
|
||||
export { default as WarningDialog } from './Warning';
|
||||
export { default as DeleteRobotConfirmationDialog } from './DeleteRobotConfirmation';
|
||||
export { default as OrderDescriptionDialog } from './OrderDescription';
|
||||
@ -253,6 +253,7 @@ const MakerForm = ({
|
||||
longitude: maker.longitude,
|
||||
shortAlias: maker.coordinator,
|
||||
password: maker.password ? sha256(maker.password) : null,
|
||||
description: maker.description ? maker.description : null,
|
||||
};
|
||||
|
||||
void slot
|
||||
@ -294,6 +295,14 @@ const MakerForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionChange = function (event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setMaker({
|
||||
...maker,
|
||||
description: event.target.value,
|
||||
badDescription: event.target.value.length > 240,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeEscrowDuration = function (date: Date): void {
|
||||
const d = new Date(date);
|
||||
const hours: number = d.getHours();
|
||||
@ -395,7 +404,8 @@ const MakerForm = ({
|
||||
maker.badPremiumText !== '' ||
|
||||
federation.getCoordinator(maker.coordinator)?.limits === undefined ||
|
||||
typeof maker.premium !== 'number' ||
|
||||
maker.paymentMethods.length === 0
|
||||
maker.paymentMethods.length === 0 ||
|
||||
maker.badDescription
|
||||
);
|
||||
}, [maker, maker.premium, amountLimits, federationUpdatedAt, fav.type, makerHasAmountRange]);
|
||||
|
||||
@ -866,6 +876,30 @@ const MakerForm = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Collapse in={maker.advancedOptions} sx={{ width: '100%' }}>
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={`${t('Description')}`}
|
||||
type='description'
|
||||
value={maker.description}
|
||||
style={{ marginBottom: 8 }}
|
||||
inputProps={{
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{maker.badDescription && (
|
||||
<FormHelperText error={true}>
|
||||
{t('Must be equal to or shorter than 240 characters')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
|
||||
@ -21,7 +21,7 @@ import Countdown from 'react-countdown';
|
||||
import currencies from '../../../static/assets/currencies.json';
|
||||
|
||||
import { type Order, type Info } from '../../models';
|
||||
import { ConfirmationDialog } from '../Dialogs';
|
||||
import { ConfirmationDialog, OrderDescriptionDialog } from '../Dialogs';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { computeSats } from '../../utils';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
@ -40,9 +40,10 @@ interface TakeButtonProps {
|
||||
|
||||
interface OpenDialogsProps {
|
||||
inactiveMaker: boolean;
|
||||
description: boolean;
|
||||
confirmation: boolean;
|
||||
}
|
||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
||||
const closeAll = { inactiveMaker: false, description: false, confirmation: false };
|
||||
|
||||
const TakeButton = ({
|
||||
password,
|
||||
@ -117,7 +118,7 @@ const TakeButton = ({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen({ inactiveMaker: false, confirmation: true });
|
||||
setOpen({ inactiveMaker: false, description: true, confirmation: false });
|
||||
}}
|
||||
>
|
||||
{t('Sounds fine')}
|
||||
@ -181,9 +182,11 @@ const TakeButton = ({
|
||||
|
||||
const onTakeOrderClicked = function (): void {
|
||||
if (currentOrder?.maker_status === 'Inactive') {
|
||||
setOpen({ inactiveMaker: true, confirmation: false });
|
||||
setOpen({ inactiveMaker: true, description: false, confirmation: false });
|
||||
} else if (currentOrder?.description) {
|
||||
setOpen({ inactiveMaker: false, description: true, confirmation: false });
|
||||
} else {
|
||||
setOpen({ inactiveMaker: false, confirmation: true });
|
||||
setOpen({ inactiveMaker: false, description: false, confirmation: true });
|
||||
}
|
||||
};
|
||||
|
||||
@ -368,6 +371,14 @@ const TakeButton = ({
|
||||
<></>
|
||||
)}
|
||||
|
||||
<OrderDescriptionDialog
|
||||
open={open.description}
|
||||
onClose={() => setOpen({ ...open, description: false })}
|
||||
onClickBack={() => setOpen({ ...open, description: false })}
|
||||
onClickDone={() => setOpen({ ...open, description: false, confirmation: true })}
|
||||
order={currentOrder}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={open.confirmation}
|
||||
onClose={() => {
|
||||
|
||||
@ -22,6 +22,8 @@ export interface Maker {
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
password: string | null;
|
||||
description: string | null;
|
||||
badDescription: boolean;
|
||||
}
|
||||
|
||||
export const defaultMaker: Maker = {
|
||||
@ -49,6 +51,8 @@ export const defaultMaker: Maker = {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
password: null,
|
||||
description: null,
|
||||
badDescription: false,
|
||||
};
|
||||
|
||||
export default Maker;
|
||||
|
||||
@ -86,6 +86,7 @@ class Order {
|
||||
latitude: number = 0;
|
||||
longitude: number = 0;
|
||||
password: string | undefined = undefined;
|
||||
description: string | undefined = undefined;
|
||||
premium_now: number | undefined = undefined;
|
||||
tg_enabled: boolean = false; // deprecated
|
||||
tg_token: string = '';
|
||||
@ -201,6 +202,7 @@ class Order {
|
||||
latitude: this.latitude,
|
||||
longitude: this.longitude,
|
||||
password: this.password,
|
||||
description: this.description,
|
||||
};
|
||||
|
||||
if (slot) {
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Preu actual:",
|
||||
"Order for ": "Ordre per ",
|
||||
"Password": "Password",
|
||||
"Description": "Description",
|
||||
"Premium over Market (%)": "Prima sobre el mercat (%)",
|
||||
"Public Duration (HH:mm)": "Duració pública (HH:mm)",
|
||||
"Public order length": "Duració de l'ordre pública",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Aktuální kurz objednávky:",
|
||||
"Order for ": "Objednávka pro ",
|
||||
"Password": "Heslo",
|
||||
"Description": "Popis",
|
||||
"Premium over Market (%)": "Přirážka oproti trhu (%)",
|
||||
"Public Duration (HH:mm)": "Doba zveřejnění (HH:mm)",
|
||||
"Public order length": "Délka veřejné objednávky",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Aktueller Bestellkurs:",
|
||||
"Order for ": "Bestellung für ",
|
||||
"Password": "Passwort",
|
||||
"Description": "Beschreibung",
|
||||
"Premium over Market (%)": "Premium über Markt (%)",
|
||||
"Public Duration (HH:mm)": "Öffentliche Dauer (HH:mm)",
|
||||
"Public order length": "Öffentliche Bestelllänge",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Order current rate:",
|
||||
"Order for ": "Order for ",
|
||||
"Password": "Password",
|
||||
"Description": "Description",
|
||||
"Premium over Market (%)": "Premium over Market (%)",
|
||||
"Public Duration (HH:mm)": "Public Duration (HH:mm)",
|
||||
"Public order length": "Public order length",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Precio actual:",
|
||||
"Order for ": "Orden por ",
|
||||
"Password": "Contraseña",
|
||||
"Description": "Descripción",
|
||||
"Premium over Market (%)": "Prima sobre el mercado (%)",
|
||||
"Public Duration (HH:mm)": "Duración pública (HH:mm)",
|
||||
"Public order length": "Longitud de la orden pública",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Uneko Prezioa:",
|
||||
"Order for ": "Eskaera: ",
|
||||
"Password": "Pasahitza",
|
||||
"Description": "Deskribapena",
|
||||
"Premium over Market (%)": "Merkatuarekiko Prima (%)",
|
||||
"Public Duration (HH:mm)": "Iraupen publikoa (HH:mm)",
|
||||
"Public order length": "Eskaera publikoaren iraupena",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Taux actuel de la commande :",
|
||||
"Order for ": "Ordre pour ",
|
||||
"Password": "Mot de passe",
|
||||
"Description": "Description",
|
||||
"Premium over Market (%)": "Prime sur le marché (%)",
|
||||
"Public Duration (HH:mm)": "Durée publique (HH:mm)",
|
||||
"Public order length": "Durée de l'ordre public",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Tasso corrente dell'ordine:",
|
||||
"Order for ": "Ordine per ",
|
||||
"Password": "Password",
|
||||
"Description": "Descrizione",
|
||||
"Premium over Market (%)": "Prezzo maggiore del Mercato (%)",
|
||||
"Public Duration (HH:mm)": "Durata Pubblica (HH:mm)",
|
||||
"Public order length": "Lunghezza dell'ordine pubblico",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "現在の注文レート:",
|
||||
"Order for ": "注文 ",
|
||||
"Password": "パスワード",
|
||||
"Description": "説明",
|
||||
"Premium over Market (%)": "市場に対するプレミアム(%)",
|
||||
"Public Duration (HH:mm)": "公開期間(HH:mm)",
|
||||
"Public order length": "公開注文の期間",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Aktualny kurs zamówienia:",
|
||||
"Order for ": "Zamówienie dla ",
|
||||
"Password": "Hasło",
|
||||
"Description": "Opis",
|
||||
"Premium over Market (%)": "Premia ponad rynek (%)",
|
||||
"Public Duration (HH:mm)": "Czas trwania publicznego (HH:mm)",
|
||||
"Public order length": "Długość zamówienia publicznego",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Taxa atual do pedido:",
|
||||
"Order for ": "Ordem para ",
|
||||
"Password": "Senha",
|
||||
"Description": "Descrição",
|
||||
"Premium over Market (%)": "Prêmio sobre o Mercado (%)",
|
||||
"Public Duration (HH:mm)": "Duração Pública (HH:mm)",
|
||||
"Public order length": "Comprimento da ordem pública",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Текущий курс ордера:",
|
||||
"Order for ": "Ордер на ",
|
||||
"Password": "Пароль",
|
||||
"Description": "Описание",
|
||||
"Premium over Market (%)": "Наценка по сравнению с рынком (%)",
|
||||
"Public Duration (HH:mm)": "Публичная продолжительность (ЧЧ: мм)",
|
||||
"Public order length": "Длина общественного ордера",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Order aktuell kurs: ",
|
||||
"Order for ": "Order för ",
|
||||
"Password": "Lösenord",
|
||||
"Description": "Beskrivning",
|
||||
"Premium over Market (%)": "Premium över marknaden (%)",
|
||||
"Public Duration (HH:mm)": "Publik varaktighet (HH:mm)",
|
||||
"Public order length": "Publik orderlängd",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "Kiwango cha sasa cha agizo:",
|
||||
"Order for ": "Agizo la ",
|
||||
"Password": "Nenosiri",
|
||||
"Description": "Maelezo",
|
||||
"Premium over Market (%)": "Faida juu ya Soko (%)",
|
||||
"Public Duration (HH:mm)": "Muda wa Umma (HH:mm)",
|
||||
"Public order length": "Urefu wa agizo la umma",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "อัตราปัจจุบันของคำสั่งซื้อ:",
|
||||
"Order for ": "สั่งซื้อสำหรับ",
|
||||
"Password": "รหัสผ่าน",
|
||||
"Description": "คำอธิบาย",
|
||||
"Premium over Market (%)": "ค่าพรีเมี่ยมเหนือตลาด (%)",
|
||||
"Public Duration (HH:mm)": "ระยะเวลาเปิดเผยต่อสาธารณะ (ชม:นาที)",
|
||||
"Public order length": "ความยาวคำสั่งซื้อต่อสาธารณะ",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "订单当前价格:",
|
||||
"Order for ": "订单用于 ",
|
||||
"Password": "密码",
|
||||
"Description": "描述",
|
||||
"Premium over Market (%)": "市场溢价(%)",
|
||||
"Public Duration (HH:mm)": "公开时间 (HH:mm)",
|
||||
"Public order length": "订单公开长度",
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"Order current rate:": "訂單當前價格:",
|
||||
"Order for ": "訂單的 ",
|
||||
"Password": "密碼",
|
||||
"Description": "描述",
|
||||
"Premium over Market (%)": "高於市場溢價 (%)",
|
||||
"Public Duration (HH:mm)": "公開時間 (HH:mm)",
|
||||
"Public order length": "訂單公開時長",
|
||||
|
||||
@ -479,6 +479,32 @@ class TradeTest(BaseAPITestCase):
|
||||
|
||||
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||
trade.cancel_order()
|
||||
|
||||
def test_make_and_take_description_order(self):
|
||||
"""
|
||||
Tests a trade with a description from order creation to taken.
|
||||
"""
|
||||
description = "Test"
|
||||
description_maker_form = maker_form_buy_with_range.copy()
|
||||
description_maker_form["description"] = description
|
||||
|
||||
trade = Trade(
|
||||
self.client,
|
||||
# Add description to order
|
||||
maker_form=description_maker_form,
|
||||
)
|
||||
trade.publish_order()
|
||||
trade.take_order()
|
||||
data = trade.response.json()
|
||||
|
||||
self.assertEqual(trade.response.status_code, 200)
|
||||
self.assertResponse(trade.response)
|
||||
|
||||
self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label)
|
||||
self.assertEqual(data["description"], description)
|
||||
|
||||
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||
trade.cancel_order()
|
||||
|
||||
def test_make_and_take_password_order(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user