Skip to content

Commit 0f8fe34

Browse files
feat(wrap): public release of Bitcoin Wrap (W) for Universal BRC-20
- Add Wrap Validator Service with audited Taproot validation path (multisig/csv) - Introduce Wrap API endpoints and validation endpoints - Consolidate wrap DB schema and tables: extended contracts, swap positions - Add remaining_supply on deploys; initialize from max_supply for existing data - Update services to use IntermediateState; align tests to new API - Improve error handling and structured logging across indexer/processor Migrations & Models - Single consolidated Alembic migration: extended_contracts, swap_positions, remaining_supply - Add enums, constraints, and indexes for new tables API & Services - New routers: wrap, validation - Query services for extended contracts and TVL - Processor integration for W mint/burn via wrap services Tooling & CI - Add system deps for secp256k1 build in CI (pkg-config, libsecp256k1-dev, build-essential, python3-dev) - Add Python deps (bech32m, secp256k1, varint) and document dev setup Quality - Black/Flake8: clean - Tests: 438 passed, 7 skipped locally - README updated for Taproot crypto system dependencies Authored-by: Universal BRC-20 Indexer Team
1 parent 2bdc8da commit 0f8fe34

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+5699
-2754
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ jobs:
5454
python -m pip install --upgrade pip
5555
pip install pipenv
5656
57+
- name: Install system dependencies (secp256k1 build)
58+
run: |
59+
sudo apt-get update
60+
sudo apt-get install -y pkg-config libsecp256k1-dev build-essential python3-dev
61+
5762
- name: Cache pipenv dependencies
5863
uses: actions/cache@v4
5964
with:
@@ -208,11 +213,11 @@ jobs:
208213
209214
echo "DATABASE_URL: $DATABASE_URL"
210215
echo "REDIS_URL: $REDIS_URL"
211-
echo "Health check URL: http://localhost:8080/v1/indexer/brc20/health"
216+
echo "Health check URL: http://localhost:8083/v1/indexer/brc20/health"
212217
sleep 6
213218
# Wait up to 6 seconds for the service to be up
214219
for i in {1..60}; do
215-
if curl -f http://localhost:8080/v1/indexer/brc20/health; then
220+
if curl -f http://localhost:8083/v1/indexer/brc20/health; then
216221
echo "Service is up!"
217222
docker stop test-indexer
218223
exit 0

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ RUN apt-get update && apt-get install -y \
77
build-essential \
88
libpq-dev \
99
gcc \
10+
pkg-config \
11+
libsecp256k1-dev \
1012
&& rm -rf /var/lib/apt/lists/*
1113

1214
# Install pipenv
@@ -32,6 +34,7 @@ ENV HOME=/home/indexer
3234
RUN apt-get update && apt-get install -y \
3335
libpq5 \
3436
curl \
37+
libsecp256k1-2 \
3538
&& rm -rf /var/lib/apt/lists/*
3639

3740
# Copy Python packages from builder

Pipfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ sqlalchemy = "*"
2828
mypy = "*"
2929
bandit = "*"
3030
safety = "*"
31+
bech32m = "*"
32+
secp256k1 = "*"
33+
varint = "*"
3134

3235
[dev-packages]
3336
pytest = "*"

Pipfile.lock

Lines changed: 2506 additions & 2079 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,23 @@ pipenv run alembic upgrade head
109109
pipenv run python run.py --continuous
110110
```
111111

112+
### System Dependencies for Taproot Crypto (Developers)
113+
114+
Taproot helpers use `secp256k1` bindings and Bech32m:
115+
116+
- Install OS libs (macOS):
117+
```bash
118+
brew install pkg-config secp256k1
119+
```
120+
- Install OS libs (Debian/Ubuntu in CI/containers):
121+
```bash
122+
sudo apt-get update && sudo apt-get install -y pkg-config libsecp256k1-dev
123+
```
124+
- Python deps are declared in `Pipfile` and `requirements.txt`:
125+
```bash
126+
pipenv run pip install bech32m secp256k1
127+
```
128+
112129
### Available Commands
113130

114131
The project includes a comprehensive Makefile for common operations:
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""Consolidate wrap schema into simplicity (extended_contracts, vaults, swap_positions, remaining_supply)
2+
3+
Revision ID: 20251030_01
4+
Revises: f0e124fbfbf0
5+
Create Date: 2025-10-30
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
from sqlalchemy.sql import text
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "20251030_01"
17+
down_revision: Union[str, Sequence[str], None] = "f0e124fbfbf0"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# 1) deploys.remaining_supply (align with model)
24+
conn = op.get_bind()
25+
26+
# Add column nullable first (if not exists)
27+
op.add_column(
28+
"deploys",
29+
sa.Column("remaining_supply", sa.Numeric(precision=38, scale=8), nullable=True),
30+
)
31+
32+
# Initialize values: set remaining_supply = max_supply
33+
conn.execute(text("UPDATE deploys SET remaining_supply = max_supply"))
34+
# Special case for W with max_supply = 0 → remaining_supply = 0
35+
conn.execute(text("UPDATE deploys SET remaining_supply = 0 WHERE ticker = 'W' AND max_supply = 0"))
36+
# Set NOT NULL
37+
op.alter_column("deploys", "remaining_supply", nullable=False)
38+
39+
# 2) extended_contracts
40+
op.create_table(
41+
"extended_contracts",
42+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
43+
sa.Column("script_address", sa.String(), nullable=False),
44+
sa.Column("initiator_address", sa.String(), nullable=False),
45+
sa.Column("status", sa.String(), nullable=False, server_default="active"),
46+
sa.Column("timelock_delay", sa.Integer(), nullable=False, comment="Timelock delay in blocks"),
47+
sa.Column("initial_amount", sa.Numeric(precision=38, scale=8), nullable=True, comment="Amount of tokens initially wrapped"),
48+
sa.Column("creation_txid", sa.String(), nullable=False),
49+
sa.Column("creation_timestamp", sa.DateTime(), nullable=False),
50+
sa.Column("creation_height", sa.Integer(), nullable=False),
51+
sa.Column("internal_pubkey", sa.String(), nullable=True),
52+
sa.Column("tapscript_hex", sa.Text(), nullable=True),
53+
sa.Column("merkle_root", sa.String(), nullable=True),
54+
sa.Column("closure_txid", sa.String(), nullable=True, comment="Transaction ID that closed the contract"),
55+
sa.Column("closure_timestamp", sa.DateTime(), nullable=True, comment="Block timestamp when contract was closed"),
56+
sa.Column("closure_height", sa.Integer(), nullable=True, comment="Block height when contract was closed"),
57+
sa.Column("extension_data", sa.Text(), nullable=True),
58+
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
59+
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
60+
sa.PrimaryKeyConstraint("id", name=op.f("extended_contracts_pkey")),
61+
)
62+
op.create_index(op.f("ix_extended_contracts_script_address"), "extended_contracts", ["script_address"], unique=True)
63+
op.create_index(op.f("ix_extended_contracts_initiator_address"), "extended_contracts", ["initiator_address"], unique=False)
64+
op.create_index(op.f("ix_extended_contracts_creation_txid"), "extended_contracts", ["creation_txid"], unique=False)
65+
op.create_index(op.f("ix_extended_contracts_creation_height"), "extended_contracts", ["creation_height"], unique=False)
66+
67+
# 3) vaults (Enum values in lowercase to align with models)
68+
vaultstatus = sa.Enum("active", "abandoned", "recycled", "sovereign_recovery", "closed", name="vaultstatus")
69+
vaultstatus.create(op.get_bind(), checkfirst=True)
70+
71+
op.create_table(
72+
"vaults",
73+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
74+
sa.Column("vault_type", sa.String(), nullable=False, comment="Defines the type of contract, allowing for future protocol extensions."),
75+
sa.Column("status", vaultstatus, nullable=False, comment="The current state of the game for this vault."),
76+
sa.Column("p2tr_address", sa.String(), nullable=False, comment="The Taproot (P2TR) address encoding the contract's spend paths."),
77+
sa.Column("owner_address", sa.String(), nullable=False, comment="The address of the vault's sovereign owner."),
78+
sa.Column("collateral_amount_sats", sa.Numeric(precision=38, scale=0), nullable=False, comment="The amount of BTC collateral in satoshis."),
79+
sa.Column("remaining_blocks", sa.Integer(), nullable=True, comment="Countdown for the liquidation timelock."),
80+
sa.Column("w_proof_commitment", sa.String(), nullable=False, comment="Hash of the W_PROOF from the reveal transaction's witness."),
81+
sa.Column("reveal_operation_id", sa.Integer(), nullable=False),
82+
sa.Column("closing_operation_id", sa.Integer(), nullable=True),
83+
sa.Column("reveal_txid", sa.String(), nullable=False, comment="TXID of the transaction that locked the collateral."),
84+
sa.Column("reveal_block_height", sa.Integer(), nullable=False),
85+
sa.Column("reveal_timestamp", sa.DateTime(), nullable=False),
86+
sa.Column("closing_txid", sa.String(), nullable=True, comment="TXID of the transaction that unlocked the collateral."),
87+
sa.Column("closing_block_height", sa.Integer(), nullable=True),
88+
sa.Column("closing_timestamp", sa.DateTime(), nullable=True),
89+
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
90+
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
91+
sa.ForeignKeyConstraint(["reveal_operation_id"], ["brc20_operations.id"]),
92+
sa.ForeignKeyConstraint(["closing_operation_id"], ["brc20_operations.id"]),
93+
sa.PrimaryKeyConstraint("id"),
94+
)
95+
op.create_index(op.f("ix_vaults_vault_type"), "vaults", ["vault_type"], unique=False)
96+
op.create_index(op.f("ix_vaults_status"), "vaults", ["status"], unique=False)
97+
op.create_index(op.f("ix_vaults_p2tr_address"), "vaults", ["p2tr_address"], unique=True)
98+
op.create_index(op.f("ix_vaults_owner_address"), "vaults", ["owner_address"], unique=False)
99+
op.create_index(op.f("ix_vaults_remaining_blocks"), "vaults", ["remaining_blocks"], unique=False)
100+
op.create_index(op.f("ix_vaults_reveal_block_height"), "vaults", ["reveal_block_height"], unique=False)
101+
op.create_index(op.f("ix_vaults_reveal_operation_id"), "vaults", ["reveal_operation_id"], unique=True)
102+
op.create_index(op.f("ix_vaults_reveal_txid"), "vaults", ["reveal_txid"], unique=True)
103+
op.create_index(op.f("ix_vaults_closing_operation_id"), "vaults", ["closing_operation_id"], unique=True)
104+
op.create_index(op.f("ix_vaults_closing_txid"), "vaults", ["closing_txid"], unique=True)
105+
op.create_index(op.f("ix_vaults_closing_block_height"), "vaults", ["closing_block_height"], unique=False)
106+
107+
# 4) swap_positions (status as VARCHAR + CHECK to align with model native_enum=False)
108+
active_values = ("active", "expired", "closed")
109+
op.create_table(
110+
"swap_positions",
111+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
112+
sa.Column("owner_address", sa.String(), nullable=False),
113+
sa.Column("pool_id", sa.String(), nullable=False, comment="Canonical pair id (alphabetical)"),
114+
sa.Column("src_ticker", sa.String(), nullable=False),
115+
sa.Column("dst_ticker", sa.String(), nullable=False),
116+
sa.Column("amount_locked", sa.Numeric(precision=38, scale=8), nullable=False),
117+
sa.Column("lock_duration_blocks", sa.Integer(), nullable=False),
118+
sa.Column("lock_start_height", sa.Integer(), nullable=False),
119+
sa.Column("unlock_height", sa.Integer(), nullable=False),
120+
sa.Column("status", sa.String(), nullable=False, server_default="active"),
121+
sa.Column("init_operation_id", sa.Integer(), sa.ForeignKey("brc20_operations.id"), nullable=False, unique=True),
122+
sa.Column("closing_operation_id", sa.Integer(), sa.ForeignKey("brc20_operations.id"), nullable=True, unique=True),
123+
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
124+
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
125+
sa.CheckConstraint(f"status in {active_values}", name="ck_swap_positions_status_values"),
126+
)
127+
op.create_index("ix_swap_positions_owner_address", "swap_positions", ["owner_address"])
128+
op.create_index("ix_swap_positions_pool_id", "swap_positions", ["pool_id"])
129+
op.create_index("ix_swap_positions_src_ticker", "swap_positions", ["src_ticker"])
130+
op.create_index("ix_swap_positions_dst_ticker", "swap_positions", ["dst_ticker"])
131+
op.create_index("ix_swap_positions_lock_start_height", "swap_positions", ["lock_start_height"])
132+
op.create_index("ix_swap_positions_unlock_height", "swap_positions", ["unlock_height"])
133+
op.create_index("ix_swap_positions_status", "swap_positions", ["status"])
134+
135+
# Drop obsolete unique constraint if present (idempotent)
136+
try:
137+
op.drop_constraint("uq_swap_pos_owner_pool_unlock", "swap_positions", type_="unique")
138+
except Exception:
139+
pass
140+
141+
142+
def downgrade() -> None:
143+
# swap_positions
144+
op.drop_index("ix_swap_positions_status", table_name="swap_positions")
145+
op.drop_index("ix_swap_positions_unlock_height", table_name="swap_positions")
146+
op.drop_index("ix_swap_positions_lock_start_height", table_name="swap_positions")
147+
op.drop_index("ix_swap_positions_dst_ticker", table_name="swap_positions")
148+
op.drop_index("ix_swap_positions_src_ticker", table_name="swap_positions")
149+
op.drop_index("ix_swap_positions_pool_id", table_name="swap_positions")
150+
op.drop_index("ix_swap_positions_owner_address", table_name="swap_positions")
151+
op.drop_table("swap_positions")
152+
153+
# vaults
154+
op.drop_index(op.f("ix_vaults_closing_block_height"), table_name="vaults")
155+
op.drop_index(op.f("ix_vaults_closing_txid"), table_name="vaults")
156+
op.drop_index(op.f("ix_vaults_closing_operation_id"), table_name="vaults")
157+
op.drop_index(op.f("ix_vaults_reveal_txid"), table_name="vaults")
158+
op.drop_index(op.f("ix_vaults_reveal_operation_id"), table_name="vaults")
159+
op.drop_index(op.f("ix_vaults_reveal_block_height"), table_name="vaults")
160+
op.drop_index(op.f("ix_vaults_remaining_blocks"), table_name="vaults")
161+
op.drop_index(op.f("ix_vaults_p2tr_address"), table_name="vaults")
162+
op.drop_index(op.f("ix_vaults_owner_address"), table_name="vaults")
163+
op.drop_index(op.f("ix_vaults_status"), table_name="vaults")
164+
op.drop_index(op.f("ix_vaults_vault_type"), table_name="vaults")
165+
op.drop_table("vaults")
166+
try:
167+
sa.Enum(name="vaultstatus").drop(op.get_bind(), checkfirst=True)
168+
except Exception:
169+
pass
170+
171+
# extended_contracts
172+
op.drop_index(op.f("ix_extended_contracts_creation_height"), table_name="extended_contracts")
173+
op.drop_index(op.f("ix_extended_contracts_creation_txid"), table_name="extended_contracts")
174+
op.drop_index(op.f("ix_extended_contracts_initiator_address"), table_name="extended_contracts")
175+
op.drop_index(op.f("ix_extended_contracts_script_address"), table_name="extended_contracts")
176+
op.drop_table("extended_contracts")
177+
178+
# deploys.remaining_supply
179+
op.drop_column("deploys", "remaining_supply")
180+
181+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Add trigger to update vault status when remaining_blocks reaches zero
2+
3+
Revision ID: 20251030_02
4+
Revises: 20251030_01
5+
Create Date: 2025-10-30
6+
"""
7+
from typing import Sequence, Union
8+
9+
from alembic import op
10+
11+
12+
revision: str = "20251030_02"
13+
down_revision: Union[str, Sequence[str], None] = "20251030_01"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
CREATE_TRIGGER_FUNCTION_SQL = """
19+
CREATE OR REPLACE FUNCTION update_vault_status_on_countdown()
20+
RETURNS TRIGGER AS $$
21+
BEGIN
22+
IF NEW.status = 'active' AND NEW.remaining_blocks = 0 AND COALESCE(OLD.remaining_blocks, 0) > 0 THEN
23+
NEW.status := 'abandoned';
24+
END IF;
25+
RETURN NEW;
26+
END;
27+
$$ LANGUAGE plpgsql;
28+
"""
29+
30+
31+
CREATE_TRIGGER_SQL = """
32+
CREATE TRIGGER trigger_vault_abandonment
33+
BEFORE UPDATE ON vaults
34+
FOR EACH ROW
35+
EXECUTE FUNCTION update_vault_status_on_countdown();
36+
"""
37+
38+
39+
DROP_TRIGGER_SQL = "DROP TRIGGER IF EXISTS trigger_vault_abandonment ON vaults;"
40+
DROP_FUNCTION_SQL = "DROP FUNCTION IF EXISTS update_vault_status_on_countdown();"
41+
42+
43+
def upgrade() -> None:
44+
op.execute(CREATE_TRIGGER_FUNCTION_SQL)
45+
op.execute(CREATE_TRIGGER_SQL)
46+
47+
48+
def downgrade() -> None:
49+
op.execute(DROP_TRIGGER_SQL)
50+
op.execute(DROP_FUNCTION_SQL)
51+
52+

docs/architecture/OPI_ARCHITECTURE_DIAGRAM.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
graph TD
77
A[Block Ingestion] --> B[Transaction Parsing]
88
B --> C{OPI Router}
9-
C -->|op: "swap"| D[OPI-1 Processor]
10-
C -->|op: "no_return"| E[OPI-0 Processor]
11-
C -->|op: "deploy"| F[Legacy BRC20 Processor]
9+
C -->|op: swap| D[OPI-1 Processor]
10+
C -->|op: no_return| E[OPI-0 Processor]
11+
C -->|op: deploy| F[Legacy BRC20 Processor]
1212
D --> G[State Validation]
1313
E --> G
1414
F --> G

docs/architecture/OPI_DEVELOPER_GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ The OPI (Operation Proposal Improvement) framework is a modular architecture tha
4242
graph TD
4343
A[Block Ingestion] --> B[Transaction Parsing]
4444
B --> C{OPI Router}
45-
C -->|op: "swap"| D[OPI-1 Processor]
46-
C -->|op: "no_return"| E[OPI-0 Processor]
47-
C -->|op: "deploy"| F[Legacy BRC20 Processor]
45+
C -->|op: swap| D[OPI-1 Processor]
46+
C -->|op: no_return| E[OPI-0 Processor]
47+
C -->|op: deploy| F[Legacy BRC20 Processor]
4848
D --> G[State Validation]
4949
E --> G
5050
F --> G

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ uvicorn[standard]==0.35.0; python_version >= '3.9'
5555
uvloop==0.21.0; python_full_version >= '3.8.0'
5656
watchfiles==1.1.0; python_version >= '3.9'
5757
websockets==15.0.1; python_version >= '3.9'
58+
bech32m==1.0.0
59+
secp256k1==0.14.0
60+
varint==1.0.2

0 commit comments

Comments
 (0)