From e2472d13a21f121f2317fa94bfff18cc97c5f6ec Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 1 Nov 2025 23:45:28 +0100 Subject: [PATCH] Adds on-chain Bitcoin payment support Adds support for on-chain Bitcoin payments by: - Introducing a new `Assets:Bitcoin:OnChain` account. - Updating the `SettleReceivable` and `PayUser` models to include `txid` for storing transaction IDs. - Modifying the API endpoints to handle `btc_onchain` as a valid payment method and associate it with the new account. This allows tracking on-chain Bitcoin transactions separately from Lightning Network payments. --- migrations.py | 31 +++++++++++++++++++++ models.py | 7 +++-- views_api.py | 76 ++++++++++++++++++++++++++------------------------- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/migrations.py b/migrations.py index 468057e..5efb00d 100644 --- a/migrations.py +++ b/migrations.py @@ -332,3 +332,34 @@ async def m008_rename_lightning_account(db): WHERE name = 'Assets:Lightning:Balance' """ ) + + +async def m009_add_onchain_bitcoin_account(db): + """ + Add Assets:Bitcoin:OnChain account for on-chain Bitcoin transactions. + This allows tracking on-chain Bitcoin separately from Lightning Network payments. + """ + import uuid + + # Check if the account already exists + existing = await db.fetchone( + """ + SELECT id FROM accounts + WHERE name = 'Assets:Bitcoin:OnChain' + """ + ) + + if not existing: + # Create the on-chain Bitcoin asset account + await db.execute( + f""" + INSERT INTO accounts (id, name, account_type, description, created_at) + VALUES (:id, :name, :type, :description, {db.timestamp_now}) + """, + { + "id": str(uuid.uuid4()), + "name": "Assets:Bitcoin:OnChain", + "type": "asset", + "description": "On-chain Bitcoin wallet" + } + ) diff --git a/models.py b/models.py index bdf79a0..ffde1c6 100644 --- a/models.py +++ b/models.py @@ -188,11 +188,13 @@ class SettleReceivable(BaseModel): user_id: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) - payment_method: str # "cash", "bank_transfer", "lightning", "other" + payment_method: str # "cash", "bank_transfer", "check", "lightning", "btc_onchain", "other" description: str # Description of the payment reference: Optional[str] = None # Optional reference (receipt number, transaction ID, etc.) currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.) amount_sats: Optional[int] = None # Equivalent amount in sats (for reference/conversion tracking) + payment_hash: Optional[str] = None # For lightning payments + txid: Optional[str] = None # For on-chain Bitcoin transactions class PayUser(BaseModel): @@ -200,12 +202,13 @@ class PayUser(BaseModel): user_id: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) - payment_method: str # "cash", "bank_transfer", "lightning", "check", "other" + payment_method: str # "cash", "bank_transfer", "check", "lightning", "btc_onchain", "other" description: Optional[str] = None # Description of the payment reference: Optional[str] = None # Optional reference (receipt number, transaction ID, etc.) currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.) amount_sats: Optional[int] = None # Equivalent amount in sats (for reference/conversion tracking) payment_hash: Optional[str] = None # For lightning payments + txid: Optional[str] = None # For on-chain Bitcoin transactions class AssertionStatus(str, Enum): diff --git a/views_api.py b/views_api.py index 2be05b1..a504e92 100644 --- a/views_api.py +++ b/views_api.py @@ -786,7 +786,7 @@ async def api_settle_receivable( ) # Validate payment method - valid_methods = ["cash", "bank_transfer", "check", "other"] + valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"] if data.payment_method.lower() not in valid_methods: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, @@ -803,6 +803,8 @@ async def api_settle_receivable( "cash": "Cash", "bank_transfer": "Bank Account", "check": "Bank Account", + "lightning": "Assets:Bitcoin:Lightning", + "btc_onchain": "Assets:Bitcoin:OnChain", "other": "Cash" } @@ -852,6 +854,14 @@ async def api_settle_receivable( amount_in_sats = int(data.amount) line_metadata = {} + # Add payment hash for lightning payments + if data.payment_hash: + line_metadata["payment_hash"] = data.payment_hash + + # Add transaction ID for on-chain Bitcoin payments + if data.txid: + line_metadata["txid"] = data.txid + # Add meta information for audit trail entry_meta = { "source": "manual_settlement", @@ -924,7 +934,7 @@ async def api_pay_user( ) # Validate payment method - valid_methods = ["cash", "bank_transfer", "check", "lightning", "other"] + valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"] if data.payment_method.lower() not in valid_methods: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, @@ -937,43 +947,31 @@ async def api_pay_user( ) # Get the appropriate asset account based on payment method - if data.payment_method.lower() == "lightning": - # For lightning, use the Lightning Wallet account - payment_account = await get_account_by_name("Lightning Wallet") - if not payment_account: - # Create it if it doesn't exist - payment_account = await create_account( - CreateAccount( - name="Lightning Wallet", - account_type=AccountType.ASSET, - description="Lightning Network wallet for Castle", - ), - wallet.wallet.id, - ) - else: - # For cash/bank/check - payment_account_map = { - "cash": "Cash", - "bank_transfer": "Bank Account", - "check": "Bank Account", - "other": "Cash" - } - account_name = payment_account_map.get(data.payment_method.lower(), "Cash") - payment_account = await get_account_by_name(account_name) + payment_account_map = { + "cash": "Cash", + "bank_transfer": "Bank Account", + "check": "Bank Account", + "lightning": "Assets:Bitcoin:Lightning", + "btc_onchain": "Assets:Bitcoin:OnChain", + "other": "Cash" + } - if not payment_account: - # Try to find any asset account - all_accounts = await get_all_accounts() - for acc in all_accounts: - if acc.account_type == AccountType.ASSET and "receivable" not in acc.name.lower(): - payment_account = acc - break + account_name = payment_account_map.get(data.payment_method.lower(), "Cash") + payment_account = await get_account_by_name(account_name) - if not payment_account: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail=f"Payment account '{account_name}' not found. Please create it first.", - ) + if not payment_account: + # Try to find any asset account that's not receivable + all_accounts = await get_all_accounts() + for acc in all_accounts: + if acc.account_type == AccountType.ASSET and "receivable" not in acc.name.lower(): + payment_account = acc + break + + if not payment_account: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Payment account '{account_name}' not found. Please create it first.", + ) # Determine the amount to record in the journal # IMPORTANT: Always record in satoshis to match the payable account balance @@ -1003,6 +1001,10 @@ async def api_pay_user( if data.payment_hash: line_metadata["payment_hash"] = data.payment_hash + # Add transaction ID for on-chain Bitcoin payments + if data.txid: + line_metadata["txid"] = data.txid + # Create journal entry # DR Accounts Payable (liability decreased), CR Cash/Lightning/Bank (asset decreased) # This records that castle paid its debt