Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
595 changes: 594 additions & 1 deletion api/logics.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Generated by Django 5.1.15 on 2026-02-13 11:31

import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0057_robot_webhook_enabled_alter_order_escrow_duration'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='order',
name='is_taproot',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting for maker bond'), (1, 'Public'), (2, 'Paused'), (3, 'Waiting for taker bond'), (4, 'Cancelled'), (5, 'Expired'), (6, 'Waiting for trade collateral and buyer invoice'), (7, 'Waiting only for seller trade collateral'), (8, 'Waiting only for buyer invoice'), (9, 'Sending fiat - In chatroom'), (10, 'Fiat sent - In chatroom'), (11, 'In dispute'), (12, 'Collaboratively cancelled'), (13, 'Sending satoshis to buyer'), (14, 'Successful trade'), (15, 'Failed lightning network routing'), (16, 'Wait for dispute resolution'), (17, 'Maker lost dispute'), (18, 'Taker lost dispute'), (19, 'Waiting for maker taproot bond'), (20, 'Public (taproot mode)'), (21, 'Waiting for taker taproot bond'), (22, 'Waiting for taproot escrow funding'), (23, 'Taproot escrow confirmed - In chatroom'), (24, 'Fiat sent - Taproot escrow'), (25, 'In dispute - Taproot escrow'), (26, 'Signing taproot payout'), (27, 'Successful taproot trade'), (28, 'Failed taproot trade')], default=0),
),
migrations.CreateModel(
name='TaprootPayment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('concept', models.PositiveSmallIntegerField(choices=[(0, 'Maker bond'), (1, 'Taker bond'), (2, 'Trade escrow'), (3, 'Payout')], default=2)),
('status', models.PositiveSmallIntegerField(choices=[(0, 'Created'), (1, 'Funded'), (2, 'Confirmed'), (3, 'Spent'), (4, 'Cancelled'), (5, 'Disputed')], default=0)),
('escrow_output_descriptor', models.TextField(blank=True, default=None, help_text='Full Taproot output descriptor string, e.g. tr(musig_agg_pk,{{leaf_a,leaf_b},{leaf_c,leaf_d}})', null=True)),
('escrow_txid', models.CharField(blank=True, default=None, help_text='Escrow locking transaction ID (hex)', max_length=64, null=True, unique=True)),
('escrow_vout', models.PositiveSmallIntegerField(blank=True, default=None, help_text='Output index of the escrow UTXO in the locking TX', null=True)),
('escrow_amount_sat', models.PositiveBigIntegerField(help_text='Total satoshis locked in the escrow output', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000000)])),
('payout_txid', models.CharField(blank=True, default=None, help_text='Payout transaction ID once the escrow is spent (hex)', max_length=64, null=True, unique=True)),
('maker_taproot_pubkey', models.CharField(blank=True, default=None, help_text='Maker x-only Taproot pubkey (hex, 64 chars)', max_length=64, null=True)),
('taker_taproot_pubkey', models.CharField(blank=True, default=None, help_text='Taker x-only Taproot pubkey (hex, 64 chars)', max_length=64, null=True)),
('coordinator_taproot_pubkey', models.CharField(blank=True, default=None, help_text='Coordinator x-only Taproot pubkey (hex, 64 chars)', max_length=64, null=True)),
('maker_musig_pubkey', models.CharField(blank=True, default=None, help_text='Maker compressed MuSig2 pubkey (hex, 66 chars)', max_length=66, null=True)),
('taker_musig_pubkey', models.CharField(blank=True, default=None, help_text='Taker compressed MuSig2 pubkey (hex, 66 chars)', max_length=66, null=True)),
('maker_musig_pubnonce', models.CharField(blank=True, default=None, help_text='Maker MuSig2 public nonce (hex, 132 chars)', max_length=132, null=True)),
('taker_musig_pubnonce', models.CharField(blank=True, default=None, help_text='Taker MuSig2 public nonce (hex, 132 chars)', max_length=132, null=True)),
('maker_partial_sig', models.CharField(blank=True, default=None, help_text='Maker partial MuSig2 signature for keyspend (hex)', max_length=64, null=True)),
('taker_partial_sig', models.CharField(blank=True, default=None, help_text='Taker partial MuSig2 signature for keyspend (hex)', max_length=64, null=True)),
('aggregated_musig_pubkey_ctx', models.TextField(blank=True, default=None, help_text='Serialized KeyAggContext (hex) for MuSig2 verification', null=True)),
('escrow_psbt_hex', models.TextField(blank=True, default=None, help_text='Unsigned escrow locking PSBT (hex)', null=True)),
('payout_psbt_hex', models.TextField(blank=True, default=None, help_text='Unsigned payout PSBT (hex)', null=True)),
('maker_signed_escrow_psbt', models.TextField(blank=True, default=None, help_text="Maker's partially-signed escrow PSBT (hex)", null=True)),
('taker_signed_escrow_psbt', models.TextField(blank=True, default=None, help_text="Taker's partially-signed escrow PSBT (hex)", null=True)),
('bond_tx_hex_maker', models.TextField(blank=True, default=None, help_text="Maker's signed bond TX (held, not broadcast unless cheating)", null=True)),
('bond_tx_hex_taker', models.TextField(blank=True, default=None, help_text="Taker's signed bond TX (held, not broadcast unless cheating)", null=True)),
('bond_amount_sat', models.PositiveBigIntegerField(blank=True, default=None, help_text='Bond amount in satoshis', null=True)),
('coordinator_fee_sat', models.PositiveBigIntegerField(blank=True, default=None, help_text='Coordinator service fee in satoshis', null=True)),
('mining_fee_sat', models.PositiveBigIntegerField(default=0, help_text='Estimated mining fee for the escrow TX in satoshis')),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('confirmed_at', models.DateTimeField(blank=True, default=None, help_text='Timestamp when the escrow TX reached sufficient confirmations', null=True)),
('dispute_winner', models.CharField(blank=True, choices=[('maker', 'Maker'), ('taker', 'Taker')], default=None, help_text='Set by coordinator after dispute resolution', max_length=10, null=True)),
('dispute_payout_psbt_hex', models.TextField(blank=True, default=None, help_text='Script-path spend PSBT for dispute payout (hex)', null=True)),
('maker', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='taproot_maker', to=settings.AUTH_USER_MODEL)),
('taker', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='taproot_taker', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Taproot payment',
'verbose_name_plural': 'Taproot payments',
},
),
migrations.AddField(
model_name='order',
name='taproot_escrow',
field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_taproot_escrow', to='api.taprootpayment'),
),
]
2 changes: 2 additions & 0 deletions api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .robot import Robot
from .notification import Notification
from .take_order import TakeOrder
from .taproot_payment import TaprootPayment

__all__ = [
"Currency",
Expand All @@ -16,4 +17,5 @@
"Robot",
"Notification",
"TakeOrder",
"TaprootPayment",
]
45 changes: 45 additions & 0 deletions api/models/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ class Status(models.IntegerChoices):
WFR = 16, "Wait for dispute resolution"
MLD = 17, "Maker lost dispute"
TLD = 18, "Taker lost dispute"
# ── Taproot/MAST escrow statuses ─────────────────────────
TAP_WFB = 19, "Waiting for maker taproot bond"
TAP_PUB = 20, "Public (taproot mode)"
TAP_TAK = 21, "Waiting for taker taproot bond"
TAP_WFE = 22, "Waiting for taproot escrow funding"
TAP_ESC = 23, "Taproot escrow confirmed - In chatroom"
TAP_FSE = 24, "Fiat sent - Taproot escrow"
TAP_DIS = 25, "In dispute - Taproot escrow"
TAP_PAY = 26, "Signing taproot payout"
TAP_SUC = 27, "Successful taproot trade"
TAP_FAI = 28, "Failed taproot trade"

class ExpiryReasons(models.IntegerChoices):
NTAKEN = 0, "Expired not taken"
Expand Down Expand Up @@ -276,6 +287,18 @@ class ExpiryReasons(models.IntegerChoices):
default=None,
blank=True,
)
# ── Taproot escrow ───────────────────────────────────────────
# Flag: is this order using the Taproot/MAST escrow pipeline?
is_taproot = models.BooleanField(default=False, null=False)
# Reference to the TaprootPayment tracking all escrow UTXO state
taproot_escrow = models.OneToOneField(
"api.TaprootPayment",
related_name="order_taproot_escrow",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)

# coordinator proceeds (sats revenue for this order)
proceeds = models.PositiveBigIntegerField(
Expand Down Expand Up @@ -335,6 +358,21 @@ def t_to_expire(self, status):
16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution'
17: 100 * 24 * 60 * 60, # 'Maker lost dispute'
18: 100 * 24 * 60 * 60, # 'Taker lost dispute'
# ── Taproot escrow statuses ──────────────────────────
19: config(
"EXP_MAKER_BOND_INVOICE", cast=int, default=300
), # TAP_WFB - Waiting for maker taproot bond
20: self.public_duration, # TAP_PUB - Public (taproot)
21: config(
"EXP_TAKER_BOND_INVOICE", cast=int, default=150
), # TAP_TAK - Waiting for taker taproot bond
22: int(self.escrow_duration), # TAP_WFE - Waiting for escrow funding
23: 60 * 60 * settings.FIAT_EXCHANGE_DURATION, # TAP_ESC - In chatroom
24: 60 * 60 * settings.FIAT_EXCHANGE_DURATION, # TAP_FSE - Fiat sent
25: 1 * 24 * 60 * 60, # TAP_DIS - In dispute (taproot)
26: 100 * 24 * 60 * 60, # TAP_PAY - Signing payout
27: 100 * 24 * 60 * 60, # TAP_SUC - Successful taproot trade
28: 100 * 24 * 60 * 60, # TAP_FAI - Failed taproot trade
}

return t_to_expire[status]
Expand Down Expand Up @@ -384,3 +422,10 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
lnpayment.delete()
except Exception:
pass

# Also clean up any TaprootPayment attached to this order
try:
if instance.taproot_escrow:
instance.taproot_escrow.delete()
except Exception:
pass
Loading
Loading