Skip to content

Commit 6970556

Browse files
amalcaraznesitoraliel
authored
feat: Implement comprehensive credit-based cost calculation and lifecycle management (#836)
* feature: support aleph credits * feat: parse new credit airdrop messages and refactor credit_balances table and primary key * fix: change credit amount type from Decimal to int * fix: lint errors * feat: implement credit cost calculation and garbage collector * point to tmp branch for aleph-message package * fix: added missing migration for error code * fix: balance Decimal type in http response * fix: fix migrations order * feat: filter accepted credit messages with channel + address + post types * feat: remove /api/v0/addresses/{address}/credit_balance and integrate it in /api/v0/addresses/{address}/balance * feat: support credit distribution, transfer, expense * fix: added more tests * fix: credit_history table and fifo calculation taking into account expiration and expenses * feat: credit history endpoint + lint fixes * fix: validate_balance_for_payment to take into account all resources + new one for the next 24 hours * fix: validate_balance_for_payment to take into account all resources + new one for the next 24 hours * Solve format issue on balance pre-check (#855) * Fix: Solve issue on format getting IPFS file sizes before downloading the entire content. * Fix: Added test cae for string dag_node format * Fix RabbitMQ connection timeouts during long operations. (#856) * Fix IPFS conf: use the recommended method to update IPFS configuration (#846) * fix: lint * fix: pending_message queue getting stuck due to bad error handling * Fix: Added a default internal code for errors. * fix: bugfix caching expired credit balances * fix: lint fix * fix: CI fixes due to hatch problematic dep * feat: new index in credit_history.expiration_date column * fix: added cost_credit in cost http response * fix: change pricing aggregate owner for testing * feat: added charged_address in /price and /price/estimate response * Fix: Upgrade `aleph_message` version to `1.0.5` * Fix: Pin pydantic version to the previous one from latest. * Fix: Solved failing test and put legacy and current PAYG message tests. * fix: fix account addresses for credit and settings * fix: new credit channel --------- Co-authored-by: nesitor <[email protected]> Co-authored-by: Alie.E <[email protected]>
1 parent 2acce93 commit 6970556

33 files changed

+2394
-105
lines changed

.github/workflows/code-quality.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Install pip and hatch
1919
run: |
2020
sudo apt-get install -y python3-pip
21-
pip3 install hatch hatch-vcs
21+
pip3 install "click<8.2" hatch hatch-vcs
2222
2323
- name: Cache dependencies
2424
uses: actions/cache@v4

deployment/docker-build/dev/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ COPY .git ./.git
5454
COPY src ./src
5555

5656
RUN pip install -e .
57-
RUN pip install hatch
57+
RUN pip install "click<8.2" hatch
5858

5959
FROM base
6060

deployment/docker-build/test/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ COPY src ./src
5353

5454
# Install project deps and test deps
5555
RUN pip install -e .[testing,docs]
56-
RUN pip install hatch
56+
RUN pip install "click<8.2" hatch
5757

5858
# Install project test deps
5959
RUN apt-get update && apt-get install -y \
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""add credit_history and credit_balances tables
2+
3+
Revision ID: a1b2c3d4e5f6
4+
Revises: 35a67ccc4451
5+
Create Date: 2025-07-14 00:00:00.000000
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.sql import func
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'a1b2c3d4e5f6'
15+
down_revision = '35a67ccc4451'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade() -> None:
21+
# Create credit_history table (detailed audit trail)
22+
op.create_table(
23+
'credit_history',
24+
sa.Column('id', sa.BigInteger(), autoincrement=True),
25+
sa.Column('address', sa.String(), nullable=False),
26+
sa.Column('amount', sa.BigInteger(), nullable=False),
27+
sa.Column('ratio', sa.DECIMAL(), nullable=True),
28+
sa.Column('tx_hash', sa.String(), nullable=True),
29+
sa.Column('token', sa.String(), nullable=True),
30+
sa.Column('chain', sa.String(), nullable=True),
31+
sa.Column('provider', sa.String(), nullable=True),
32+
sa.Column('origin', sa.String(), nullable=True),
33+
sa.Column('origin_ref', sa.String(), nullable=True),
34+
sa.Column('payment_method', sa.String(), nullable=True),
35+
sa.Column('credit_ref', sa.String(), nullable=False),
36+
sa.Column('credit_index', sa.Integer(), nullable=False),
37+
sa.Column('expiration_date', sa.TIMESTAMP(timezone=True), nullable=True),
38+
sa.Column('message_timestamp', sa.TIMESTAMP(timezone=True), nullable=False),
39+
sa.Column('last_update', sa.TIMESTAMP(timezone=True), nullable=False,
40+
server_default=func.now(), onupdate=func.now()),
41+
sa.PrimaryKeyConstraint('credit_ref', 'credit_index', name='credit_history_pkey'),
42+
)
43+
44+
# Create indexes on credit_history for efficient lookups
45+
op.create_index('ix_credit_history_address', 'credit_history', ['address'], unique=False)
46+
op.create_index('ix_credit_history_message_timestamp', 'credit_history', ['message_timestamp'], unique=False)
47+
48+
# Create credit_balances table (cached balance summary)
49+
op.create_table(
50+
'credit_balances',
51+
sa.Column('address', sa.String(), nullable=False),
52+
sa.Column('balance', sa.BigInteger(), nullable=False, default=0),
53+
sa.Column('last_update', sa.TIMESTAMP(timezone=True), nullable=False,
54+
server_default=func.now(), onupdate=func.now()),
55+
sa.PrimaryKeyConstraint('address', name='credit_balances_pkey'),
56+
)
57+
58+
# Create index on address for the cached balances table
59+
op.create_index('ix_credit_balances_address', 'credit_balances', ['address'], unique=False)
60+
61+
62+
def downgrade() -> None:
63+
# Drop the credit_balances table
64+
op.drop_index('ix_credit_balances_address', 'credit_balances')
65+
op.drop_table('credit_balances')
66+
67+
# Drop the credit_history table
68+
op.drop_index('ix_credit_history_address', 'credit_history')
69+
op.drop_index('ix_credit_history_message_timestamp', 'credit_history')
70+
op.drop_table('credit_history')
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""add cost_credit column to account_costs table
2+
3+
Revision ID: b7c8d9e0f1a2
4+
Revises: a1b2c3d4e5f6
5+
Create Date: 2025-01-11 00:00:00.000000
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'b7c8d9e0f1a2'
14+
down_revision = 'a1b2c3d4e5f6'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# Add cost_credit column to account_costs table
21+
op.add_column('account_costs', sa.Column('cost_credit', sa.DECIMAL(), nullable=False, server_default='0'))
22+
23+
# Add missing CREDIT_INSUFFICIENT error code
24+
op.execute(
25+
"""
26+
INSERT INTO error_codes(code, description) VALUES
27+
(6, 'Insufficient credit')
28+
ON CONFLICT (code) DO NOTHING
29+
"""
30+
)
31+
32+
33+
def downgrade() -> None:
34+
# Remove CREDIT_INSUFFICIENT error code
35+
op.execute("DELETE FROM error_codes WHERE code = 6")
36+
37+
# Remove cost_credit column from account_costs table
38+
op.drop_column('account_costs', 'cost_credit')
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""add credit_balance cron job
2+
3+
Revision ID: c8d9e0f1a2b3
4+
Revises: b7c8d9e0f1a2
5+
Create Date: 2025-01-11 00:00:00.000000
6+
7+
"""
8+
from alembic import op
9+
10+
11+
# revision identifiers, used by Alembic.
12+
revision = 'c8d9e0f1a2b3'
13+
down_revision = 'b7c8d9e0f1a2'
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade() -> None:
19+
# Add credit_balance cron job to run every hour (3600 seconds)
20+
op.execute(
21+
"""
22+
INSERT INTO cron_jobs(id, interval, last_run)
23+
VALUES ('credit_balance', 3600, '2025-01-01 00:00:00')
24+
"""
25+
)
26+
27+
28+
def downgrade() -> None:
29+
# Remove credit_balance cron job
30+
op.execute(
31+
"""
32+
DELETE FROM cron_jobs WHERE id = 'credit_balance'
33+
"""
34+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""add credit_history expiration_date index for cache invalidation performance
2+
3+
Revision ID: d0e1f2a3b4c5
4+
Revises: c8d9e0f1a2b3
5+
Create Date: 2025-01-11 00:00:00.000000
6+
7+
"""
8+
from alembic import op
9+
10+
11+
# revision identifiers, used by Alembic.
12+
revision = 'd0e1f2a3b4c5'
13+
down_revision = 'c8d9e0f1a2b3'
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade() -> None:
19+
# Add index on expiration_date for efficient cache invalidation queries
20+
# This index optimizes the query that checks if any credits expired
21+
# after the cached balance was last updated
22+
op.create_index(
23+
'ix_credit_history_expiration_date',
24+
'credit_history',
25+
['expiration_date'],
26+
unique=False
27+
)
28+
29+
30+
def downgrade() -> None:
31+
# Drop the expiration_date index
32+
op.drop_index('ix_credit_history_expiration_date', 'credit_history')

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dependencies = [
2828
"aiohttp-jinja2==1.6",
2929
"aioipfs~=0.7.1",
3030
"alembic==1.15.1",
31-
"aleph-message~=1.0.2",
31+
"aleph-message~=1.0.5",
3232
"aleph-nuls2==0.1",
3333
"aleph-p2p-client @ git+https://github.com/aleph-im/p2p-service-client-python@cbfebb871db94b2ca580e66104a67cd730c5020c",
3434
"asyncpg==0.30",
@@ -47,7 +47,7 @@ dependencies = [
4747
"orjson>=3.7.7", # Minimum version for Python 3.11
4848
"psycopg2-binary==2.9.10", # Note: psycopg3 is not yet supported by SQLAlchemy
4949
"pycryptodome==3.22.0",
50-
"pydantic>=2.0.0,<3.0.0",
50+
"pydantic==2.11.10",
5151
"pymultihash==0.8.2", # for libp2p-stubs
5252
"pynacl==1.5",
5353
"pytezos-crypto==3.13.4.1",

src/aleph/commands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from aleph.exceptions import InvalidConfigException, KeyNotFoundException
3131
from aleph.jobs import start_jobs
3232
from aleph.jobs.cron.balance_job import BalanceCronJob
33+
from aleph.jobs.cron.credit_balance_job import CreditBalanceCronJob
3334
from aleph.jobs.cron.cron_job import CronJob, cron_job_task
3435
from aleph.network import listener_tasks
3536
from aleph.services import p2p
@@ -151,7 +152,10 @@ async def main(args: List[str]) -> None:
151152
)
152153
cron_job = CronJob(
153154
session_factory=session_factory,
154-
jobs={"balance": BalanceCronJob(session_factory=session_factory)},
155+
jobs={
156+
"balance": BalanceCronJob(session_factory=session_factory),
157+
"credit_balance": CreditBalanceCronJob(session_factory=session_factory),
158+
},
155159
)
156160
chain_data_service = ChainDataService(
157161
session_factory=session_factory,

src/aleph/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ def get_defaults():
3333
# POST message type for balance updates.
3434
"post_type": "balances-update",
3535
},
36+
"credit_balances": {
37+
# Addresses allowed to publish credit balance updates.
38+
"addresses": [
39+
"0x2E4454fAD1906c0Ce6e45cBFA05cE898Ac3AC1dC",
40+
],
41+
# POST message types for credit balance updates.
42+
"post_types": [
43+
"aleph_credit_distribution",
44+
"aleph_credit_transfer",
45+
"aleph_credit_expense",
46+
],
47+
# Allowed channels for credit balance messages.
48+
"channels": ["ALEPH_CREDIT"],
49+
},
3650
"jobs": {
3751
"pending_messages": {
3852
# Maximum number of retries for a message.

0 commit comments

Comments
 (0)