Fix race condition onchain (#388)

* Fix race condition swaps

* Collect new phrases

* Increase random delay interval
This commit is contained in:
Reckless_Satoshi 2023-03-14 17:23:11 +00:00 committed by GitHub
parent 2fd4a0123e
commit 42f208fad4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 120 additions and 33 deletions

View File

@ -1,6 +1,5 @@
import hashlib import hashlib
import os import os
import random
import secrets import secrets
import time import time
from base64 import b64decode from base64 import b64decode
@ -129,7 +128,7 @@ class LNNode:
} }
@classmethod @classmethod
def pay_onchain(cls, onchainpayment, valid_code=1, on_mempool_code=2): def pay_onchain(cls, onchainpayment, queue_code=5, on_mempool_code=2):
"""Send onchain transaction for buyer payouts""" """Send onchain transaction for buyer payouts"""
if config("DISABLE_ONCHAIN", cast=bool): if config("DISABLE_ONCHAIN", cast=bool):
@ -144,9 +143,12 @@ class LNNode:
) )
# Cheap security measure to ensure there has been some non-deterministic time between request and DB check # Cheap security measure to ensure there has been some non-deterministic time between request and DB check
time.sleep(random.uniform(0.5, 10)) delay = (
secrets.randbelow(2**256) / (2**256) * 10
) # Random uniform 0 to 5 secs with good entropy
time.sleep(3 + delay)
if onchainpayment.status == valid_code: if onchainpayment.status == queue_code:
# Changing the state to "MEMPO" should be atomic with SendCoins. # Changing the state to "MEMPO" should be atomic with SendCoins.
onchainpayment.status = on_mempool_code onchainpayment.status = on_mempool_code
onchainpayment.save() onchainpayment.save()

View File

@ -481,9 +481,9 @@ class Logics:
"bad_request": "Only orders in dispute accept dispute statements" "bad_request": "Only orders in dispute accept dispute statements"
} }
if len(statement) > 5000: if len(statement) > 10000:
return False, { return False, {
"bad_statement": "The statement is longer than 5000 characters" "bad_statement": "The statement is longer than 10000 characters"
} }
if len(statement) < 100: if len(statement) < 100:
@ -554,7 +554,7 @@ class Logics:
confirmed = onchain_payment.balance.onchain_confirmed confirmed = onchain_payment.balance.onchain_confirmed
reserve = 300000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve) reserve = 300000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve)
pending_txs = OnchainPayment.objects.filter( pending_txs = OnchainPayment.objects.filter(
status=OnchainPayment.Status.VALID status__in=[OnchainPayment.Status.VALID, OnchainPayment.Status.QUEUE]
).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
if pending_txs is None: if pending_txs is None:
@ -1049,13 +1049,6 @@ class Logics:
safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED
# Convert to blocks using assummed average block time (~8 mins/block) # Convert to blocks using assummed average block time (~8 mins/block)
cltv_expiry_blocks = int(safe_cltv_expiry_secs / (BLOCK_TIME * 60)) cltv_expiry_blocks = int(safe_cltv_expiry_secs / (BLOCK_TIME * 60))
print(
invoice_concept,
" cltv_expiry_hours:",
cltv_expiry_secs / 3600,
" cltv_expiry_blocks:",
cltv_expiry_blocks,
)
return cltv_expiry_blocks return cltv_expiry_blocks
@ -1442,20 +1435,16 @@ class Logics:
else: else:
if not order.payout_tx.status == OnchainPayment.Status.VALID: if not order.payout_tx.status == OnchainPayment.Status.VALID:
return False return False
else:
valid = LNNode.pay_onchain( # Add onchain payment to queue
order.payout_tx,
valid_code=OnchainPayment.Status.VALID,
on_mempool_code=OnchainPayment.Status.MEMPO,
)
if valid:
order.status = Order.Status.SUC order.status = Order.Status.SUC
order.payout_tx.status = OnchainPayment.Status.QUEUE
order.payout_tx.save()
order.save() order.save()
send_message.delay(order.id, "trade_successful") send_message.delay(order.id, "trade_successful")
order.contract_finalization_time = timezone.now() order.contract_finalization_time = timezone.now()
order.save() order.save()
return True return True
return False
@classmethod @classmethod
def confirm_fiat(cls, order, user): def confirm_fiat(cls, order, user):

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
from api.logics import Logics from api.logics import Logics
from api.models import LNPayment, Order from api.models import LNPayment, OnchainPayment, Order
from api.tasks import follow_send_payment, send_message from api.tasks import follow_send_payment, send_message
MACAROON = b64decode(config("LND_MACAROON_BASE64")) MACAROON = b64decode(config("LND_MACAROON_BASE64"))
@ -16,7 +16,7 @@ MACAROON = b64decode(config("LND_MACAROON_BASE64"))
class Command(BaseCommand): class Command(BaseCommand):
help = "Follows all active hold invoices" help = "Follows all active hold invoices, sends out payments"
rest = 5 # seconds between consecutive checks for invoice updates rest = 5 # seconds between consecutive checks for invoice updates
def handle(self, *args, **options): def handle(self, *args, **options):
@ -130,6 +130,14 @@ class Command(BaseCommand):
self.stdout.write(str(debug)) self.stdout.write(str(debug))
def send_payments(self): def send_payments(self):
"""
Checks for invoices and onchain payments that are due to be paid.
Sends the payments.
"""
self.send_ln_payments()
self.send_onchain_payments()
def send_ln_payments(self):
""" """
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts. Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
Checks if any payment is due for retry, and tries to pay it. Checks if any payment is due for retry, and tries to pay it.
@ -153,8 +161,36 @@ class Command(BaseCommand):
queryset = queryset.union(queryset_retries) queryset = queryset.union(queryset_retries)
for lnpayment in queryset: if len(queryset) > 0:
follow_send_payment(lnpayment.payment_hash) for lnpayment in queryset:
follow_send_payment.delay(lnpayment.payment_hash)
def send_onchain_payments(self):
queryset = OnchainPayment.objects.filter(
status=OnchainPayment.Status.QUEUE,
)
if len(queryset) > 0:
for onchainpayment in queryset:
# Checks that this onchain payment is part of an order with a settled escrow
if not hasattr(onchainpayment, "order_paid_TX"):
self.stdout.write(
f"Onchain payment {str(onchainpayment)} has no parent order!"
)
return
order = onchainpayment.order_paid_TX
if order.trade_escrow.status == LNPayment.Status.SETLED:
# Sends out onchainpayment
LNNode.pay_onchain(
onchainpayment,
OnchainPayment.Status.QUEUE,
OnchainPayment.Status.MEMPO,
)
else:
self.stdout.write(
f"Onchain payment {str(onchainpayment)} for order {str(order)} escrow is not settled!"
)
def update_order_status(self, lnpayment): def update_order_status(self, lnpayment):
"""Background process following LND hold invoices """Background process following LND hold invoices

View File

@ -185,10 +185,11 @@ class OnchainPayment(models.Model):
class Status(models.IntegerChoices): class Status(models.IntegerChoices):
CREAT = 0, "Created" # User was given platform fees and suggested mining fees CREAT = 0, "Created" # User was given platform fees and suggested mining fees
VALID = 1, "Valid" # Valid onchain address submitted VALID = 1, "Valid" # Valid onchain address and fee submitted
MEMPO = 2, "In mempool" # Tx is sent to mempool MEMPO = 2, "In mempool" # Tx is sent to mempool
CONFI = 3, "Confirmed" # Tx is confirme +2 blocks CONFI = 3, "Confirmed" # Tx is confirmed +2 blocks
CANCE = 4, "Cancelled" # Cancelled tx CANCE = 4, "Cancelled" # Cancelled tx
QUEUE = 5, "Queued" # Payment is queued to be sent out
def get_balance(): def get_balance():
balance = BalanceLog.objects.create() balance = BalanceLog.objects.create()
@ -441,10 +442,10 @@ class Order(models.Model):
# in dispute # in dispute
is_disputed = models.BooleanField(default=False, null=False) is_disputed = models.BooleanField(default=False, null=False)
maker_statement = models.TextField( maker_statement = models.TextField(
max_length=5000, null=True, default=None, blank=True max_length=10000, null=True, default=None, blank=True
) )
taker_statement = models.TextField( taker_statement = models.TextField(
max_length=5000, null=True, default=None, blank=True max_length=10000, null=True, default=None, blank=True
) )
# LNpayments # LNpayments

View File

@ -501,7 +501,7 @@ class UpdateOrderSerializer(serializers.Serializer):
max_length=100, allow_null=True, allow_blank=True, default=None max_length=100, allow_null=True, allow_blank=True, default=None
) )
statement = serializers.CharField( statement = serializers.CharField(
max_length=10000, allow_null=True, allow_blank=True, default=None max_length=11000, allow_null=True, allow_blank=True, default=None
) )
action = serializers.ChoiceField( action = serializers.ChoiceField(
choices=( choices=(

View File

@ -97,6 +97,7 @@ def follow_send_payment(hash):
payment_request=lnpayment.invoice, payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat, fee_limit_sat=fee_limit_sat,
timeout_seconds=timeout_seconds, timeout_seconds=timeout_seconds,
allow_self_payment=True,
) )
order = lnpayment.order_paid_LN order = lnpayment.order_paid_LN

View File

@ -471,12 +471,15 @@ class OrderView(viewsets.ViewSet):
if order.is_swap: if order.is_swap:
data["num_satoshis"] = order.payout_tx.num_satoshis data["num_satoshis"] = order.payout_tx.num_satoshis
data["sent_satoshis"] = order.payout_tx.sent_satoshis data["sent_satoshis"] = order.payout_tx.sent_satoshis
data["network"] = str(config("NETWORK"))
if order.payout_tx.status in [ if order.payout_tx.status in [
OnchainPayment.Status.MEMPO, OnchainPayment.Status.MEMPO,
OnchainPayment.Status.CONFI, OnchainPayment.Status.CONFI,
]: ]:
data["txid"] = order.payout_tx.txid data["txid"] = order.payout_tx.txid
data["network"] = str(config("NETWORK")) elif order.payout_tx.status == OnchainPayment.Status.QUEUE:
data["tx_queued"] = True
data["address"] = order.payout_tx.address
return Response(data, status.HTTP_200_OK) return Response(data, status.HTTP_200_OK)

View File

@ -11,6 +11,7 @@ import {
Tooltip, Tooltip,
IconButton, IconButton,
Button, Button,
CircularProgress,
} from '@mui/material'; } from '@mui/material';
import currencies from '../../../../static/assets/currencies.json'; import currencies from '../../../../static/assets/currencies.json';
import TradeSummary from '../TradeSummary'; import TradeSummary from '../TradeSummary';
@ -150,6 +151,43 @@ export const SuccessfulPrompt = ({
</Alert> </Alert>
</Collapse> </Collapse>
<Collapse in={order.tx_queued && order.address != undefined}>
<Alert severity='info'>
<AlertTitle>
<CircularProgress sx={{ maxWidth: '0.8em', maxHeight: '0.8em' }} />
<a> </a>
{t('Sending coins to')}
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
color='inherit'
onClick={() => {
systemClient.copyToClipboard(order.address);
}}
>
<ContentCopy sx={{ width: '0.8em', height: '0.8em' }} />
</IconButton>
</Tooltip>
</AlertTitle>
<Typography
variant='body2'
align='center'
sx={{ wordWrap: 'break-word', width: '15.71em' }}
>
<Link
target='_blank'
href={
'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/' +
(order.network == 'testnet' ? 'testnet/' : '') +
'address/' +
order.address
}
>
{order.address}
</Link>
</Typography>
</Alert>
</Collapse>
<Grid item container alignItems='center' justifyContent='space-evenly'> <Grid item container alignItems='center' justifyContent='space-evenly'>
<Grid item> <Grid item>
<Button color='primary' variant='outlined' onClick={onClickStartAgain}> <Button color='primary' variant='outlined' onClick={onClickStartAgain}>

View File

@ -49,7 +49,7 @@ const statusToDelay = [
100000, // 'In dispute' 100000, // 'In dispute'
999999, // 'Collaboratively cancelled' 999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer' 10000, // 'Sending satoshis to buyer'
999999, // 'Sucessful trade' 60000, // 'Sucessful trade'
30000, // 'Failed lightning network routing' 30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution' 300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute' 300000, // 'Maker lost dispute'

View File

@ -94,6 +94,8 @@ export interface Order {
num_satoshis: number; num_satoshis: number;
sent_satoshis: number; sent_satoshis: number;
txid: string; txid: string;
tx_queued: boolean;
address: string;
network: 'mainnet' | 'testnet'; network: 'mainnet' | 'testnet';
} }

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats millora amb més liquiditat i usuaris. Explica-li a un amic bitcoiner sobre RoboSats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats millora amb més liquiditat i usuaris. Explica-li a un amic bitcoiner sobre RoboSats!",
"Thank you for using Robosats!": "Gràcies per fer servir RoboSats!", "Thank you for using Robosats!": "Gràcies per fer servir RoboSats!",
"Your TXID": "El teu TXID", "Your TXID": "El teu TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Començar de nou", "Start Again": "Començar de nou",
"Renew": "Renovar", "Renew": "Renovar",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats bude lepší s větší likviditou a uživateli. Pověz svým přátelům o Robosats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats bude lepší s větší likviditou a uživateli. Pověz svým přátelům o Robosats!",
"Thank you for using Robosats!": "Děkujeme, že používáš Robosats!", "Thank you for using Robosats!": "Děkujeme, že používáš Robosats!",
"Your TXID": "Tvé TXID", "Your TXID": "Tvé TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Začít znovu", "Start Again": "Začít znovu",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats wird noch besser mit mehr Nutzern und Liquidität. Erzähl einem Bitcoin-Freund von uns!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats wird noch besser mit mehr Nutzern und Liquidität. Erzähl einem Bitcoin-Freund von uns!",
"Thank you for using Robosats!": "Danke, dass du Robosats benutzt hast!", "Thank you for using Robosats!": "Danke, dass du Robosats benutzt hast!",
"Your TXID": "Your TXID", "Your TXID": "Your TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Nochmal", "Start Again": "Nochmal",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!",
"Thank you for using Robosats!": "Thank you for using Robosats!", "Thank you for using Robosats!": "Thank you for using Robosats!",
"Your TXID": "Your TXID", "Your TXID": "Your TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Start Again", "Start Again": "Start Again",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats mejora con más liquidez y usuarios. ¡Cuéntale a un amigo bitcoiner sobre RoboSats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats mejora con más liquidez y usuarios. ¡Cuéntale a un amigo bitcoiner sobre RoboSats!",
"Thank you for using Robosats!": "¡Gracias por usar RoboSats!", "Thank you for using Robosats!": "¡Gracias por usar RoboSats!",
"Your TXID": "Tu TXID", "Your TXID": "Tu TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Empezar de nuevo", "Start Again": "Empezar de nuevo",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats hobetu egiten da likidezia eta erabiltzaile gehiagorekin. Aipatu Robosats lagun bitcoiner bati!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats hobetu egiten da likidezia eta erabiltzaile gehiagorekin. Aipatu Robosats lagun bitcoiner bati!",
"Thank you for using Robosats!": "Mila esker Robosats erabiltzeagatik!", "Thank you for using Robosats!": "Mila esker Robosats erabiltzeagatik!",
"Your TXID": "Zure TXID", "Your TXID": "Zure TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Berriz Hasi", "Start Again": "Berriz Hasi",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats s'améliore avec plus de liquidité et d'utilisateurs. Parlez de Robosats à un ami bitcoiner!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats s'améliore avec plus de liquidité et d'utilisateurs. Parlez de Robosats à un ami bitcoiner!",
"Thank you for using Robosats!": "Merci d'utiliser Robosats!", "Thank you for using Robosats!": "Merci d'utiliser Robosats!",
"Your TXID": "Your TXID", "Your TXID": "Your TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Recommencer", "Start Again": "Recommencer",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats migliora se ha più liquidità ed utenti. Parla di Robosats ai tuoi amici bitcoiner!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats migliora se ha più liquidità ed utenti. Parla di Robosats ai tuoi amici bitcoiner!",
"Thank you for using Robosats!": "Grazie per aver usato Robosats!", "Thank you for using Robosats!": "Grazie per aver usato Robosats!",
"Your TXID": "Your TXID", "Your TXID": "Your TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Ricomincia", "Start Again": "Ricomincia",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats staje się lepszy dzięki większej płynności i użytkownikom. Powiedz znajomemu bitcoinerowi o Robosats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats staje się lepszy dzięki większej płynności i użytkownikom. Powiedz znajomemu bitcoinerowi o Robosats!",
"Thank you for using Robosats!": "Dziękujemy za korzystanie z Robosatów!", "Thank you for using Robosats!": "Dziękujemy za korzystanie z Robosatów!",
"Your TXID": "Your TXID", "Your TXID": "Your TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Zacznij jeszcze raz", "Start Again": "Zacznij jeszcze raz",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats fica melhor com mais liquidez e usuários. Conte a um amigo bitcoiner sobre Robosats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats fica melhor com mais liquidez e usuários. Conte a um amigo bitcoiner sobre Robosats!",
"Thank you for using Robosats!": "Obrigado por usar Robosats!", "Thank you for using Robosats!": "Obrigado por usar Robosats!",
"Your TXID": "Sua TXID", "Your TXID": "Sua TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Comece de novo", "Start Again": "Comece de novo",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats становится лучше с большей ликвидностью и пользователями. Расскажите другу-биткойнеру о Robosat!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats становится лучше с большей ликвидностью и пользователями. Расскажите другу-биткойнеру о Robosat!",
"Thank you for using Robosats!": "Спасибо за использование Robosats!", "Thank you for using Robosats!": "Спасибо за использование Robosats!",
"Your TXID": "Ваш TXID", "Your TXID": "Ваш TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Начать Снова", "Start Again": "Начать Снова",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats blir bättre med mer likviditet och fler användare. Berätta om RoboSats för en Bitcoiner-vän!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats blir bättre med mer likviditet och fler användare. Berätta om RoboSats för en Bitcoiner-vän!",
"Thank you for using Robosats!": "Tack för att du använder RoboSats!", "Thank you for using Robosats!": "Tack för att du använder RoboSats!",
"Your TXID": "Ditt TXID", "Your TXID": "Ditt TXID",
"Sending coins to": "Sending coins to",
"Start Again": "Börja om", "Start Again": "Börja om",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats จะดีขึ้นเมื่อมีสภาพคล่องและผู้ใช้งานมากขึ้น ช่วยกันชวนเพื่อนของคุณมาใช้ Robosats!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats จะดีขึ้นเมื่อมีสภาพคล่องและผู้ใช้งานมากขึ้น ช่วยกันชวนเพื่อนของคุณมาใช้ Robosats!",
"Thank you for using Robosats!": "ขอบคุณที่ใช้งาน Robosats!", "Thank you for using Robosats!": "ขอบคุณที่ใช้งาน Robosats!",
"Your TXID": "TXID ของคุณ", "Your TXID": "TXID ของคุณ",
"Sending coins to": "Sending coins to",
"Start Again": "Start Again", "Start Again": "Start Again",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats 会随着更多的流动性和更高的用户数量而变得更好。把 RoboSats 推荐给你的比特币朋友吧!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats 会随着更多的流动性和更高的用户数量而变得更好。把 RoboSats 推荐给你的比特币朋友吧!",
"Thank you for using Robosats!": "感谢你使用 Robosats", "Thank you for using Robosats!": "感谢你使用 Robosats",
"Your TXID": "你的 TXID", "Your TXID": "你的 TXID",
"Sending coins to": "Sending coins to",
"Start Again": "重新开始", "Start Again": "重新开始",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",

View File

@ -486,6 +486,7 @@
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats 會隨著更多的流動性和更高的用戶数量而變得更好。把 RoboSats 推薦給你的比特幣朋友吧!", "RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats 會隨著更多的流動性和更高的用戶数量而變得更好。把 RoboSats 推薦給你的比特幣朋友吧!",
"Thank you for using Robosats!": "感謝你使用 Robosats!", "Thank you for using Robosats!": "感謝你使用 Robosats!",
"Your TXID": "你的 TXID", "Your TXID": "你的 TXID",
"Sending coins to": "Sending coins to",
"Start Again": "重新開始", "Start Again": "重新開始",
"Renew": "Renew", "Renew": "Renew",
"#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx", "#58": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",