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.
This commit is contained in:
padreug 2025-11-01 23:45:28 +01:00
parent 8b16ead5b1
commit e2472d13a2
3 changed files with 75 additions and 39 deletions

View file

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

View file

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

View file

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