Merge 5269351094d3280a80136a7e8b70bd5efa27c952 into f89a5cee9abd58ebb3dca17e9c0b5d2ff3889b45

This commit is contained in:
aftermath
2025-09-09 21:02:30 +07:00
committed by GitHub
31 changed files with 188 additions and 9 deletions

View 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),
),
]

View File

@ -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,

View File

@ -99,6 +99,7 @@ class OrderViewSchema:
- `maker_status`
- `taker_status`
- `price_now`
- `description`
### Order Status

View File

@ -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",
)

View File

@ -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,

View File

@ -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")

View File

@ -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

View 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;

View File

@ -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';

View File

@ -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

View File

@ -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={() => {

View File

@ -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;

View File

@ -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) {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -426,6 +426,7 @@
"Order current rate:": "現在の注文レート:",
"Order for ": "注文 ",
"Password": "パスワード",
"Description": "説明",
"Premium over Market (%)": "市場に対するプレミアム(%)",
"Public Duration (HH:mm)": "公開期間(HH:mm)",
"Public order length": "公開注文の期間",

View File

@ -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",

View File

@ -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",

View File

@ -426,6 +426,7 @@
"Order current rate:": "Текущий курс ордера:",
"Order for ": "Ордер на ",
"Password": "Пароль",
"Description": "Описание",
"Premium over Market (%)": "Наценка по сравнению с рынком (%)",
"Public Duration (HH:mm)": "Публичная продолжительность (ЧЧ: мм)",
"Public order length": "Длина общественного ордера",

View File

@ -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",

View File

@ -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",

View File

@ -426,6 +426,7 @@
"Order current rate:": "อัตราปัจจุบันของคำสั่งซื้อ:",
"Order for ": "สั่งซื้อสำหรับ",
"Password": "รหัสผ่าน",
"Description": "คำอธิบาย",
"Premium over Market (%)": "ค่าพรีเมี่ยมเหนือตลาด (%)",
"Public Duration (HH:mm)": "ระยะเวลาเปิดเผยต่อสาธารณะ (ชม:นาที)",
"Public order length": "ความยาวคำสั่งซื้อต่อสาธารณะ",

View File

@ -426,6 +426,7 @@
"Order current rate:": "订单当前价格:",
"Order for ": "订单用于 ",
"Password": "密码",
"Description": "描述",
"Premium over Market (%)": "市场溢价(%",
"Public Duration (HH:mm)": "公开时间 (HH:mm)",
"Public order length": "订单公开长度",

View File

@ -426,6 +426,7 @@
"Order current rate:": "訂單當前價格:",
"Order for ": "訂單的 ",
"Password": "密碼",
"Description": "描述",
"Premium over Market (%)": "高於市場溢價 (%)",
"Public Duration (HH:mm)": "公開時間 (HH:mm)",
"Public order length": "訂單公開時長",

View File

@ -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):
"""