diff --git a/packages/examples/cvat/exchange-oracle/src/chain/escrow.py b/packages/examples/cvat/exchange-oracle/src/chain/escrow.py index 43b72a28f1..0afcecc2a6 100644 --- a/packages/examples/cvat/exchange-oracle/src/chain/escrow.py +++ b/packages/examples/cvat/exchange-oracle/src/chain/escrow.py @@ -1,13 +1,14 @@ import json +from decimal import Decimal from functools import partial import httpx2 from human_protocol_sdk.constants import ChainId, Status from human_protocol_sdk.encryption import Encryption, EncryptionUtils -from human_protocol_sdk.escrow import EscrowData, EscrowUtils +from human_protocol_sdk.escrow import EscrowClient, EscrowData, EscrowUtils from human_protocol_sdk.utils import validate_url -from src.chain.web3 import get_token_symbol +from src.chain.web3 import get_token_decimals, get_token_symbol, get_web3 from src.core.config import Config from src.core.types import OracleWebhookTypes from src.services.cache import Cache @@ -108,3 +109,25 @@ def get_escrow_fund_token_symbol(chain_id: int, escrow_address: str) -> str: token_address=escrow.token, set_callback=partial(get_token_symbol, chain_id, escrow.token), ) + + +def get_escrow_fund_token_decimals(chain_id: int, escrow_address: str) -> int: + """ + ERC-20 decimals: divide the raw token amount by 10**decimals for the user representation + (https://eips.ethereum.org/EIPS/eip-20). + """ + + escrow = get_escrow(chain_id, escrow_address) + return get_token_decimals(chain_id, escrow.token) + + +def get_remaining_escrow_funds(chain_id: int, escrow_address: str) -> Decimal | None: + web3 = get_web3(chain_id) + client = EscrowClient(web3) + + remaining_funds = client.get_remaining_funds(escrow_address) + if remaining_funds is None: + return None + + token_decimals = get_escrow_fund_token_decimals(chain_id, escrow_address) + return Decimal(remaining_funds) / Decimal(10**token_decimals) diff --git a/packages/examples/cvat/exchange-oracle/src/chain/web3.py b/packages/examples/cvat/exchange-oracle/src/chain/web3.py index 9bf9f033bd..a28add23ed 100644 --- a/packages/examples/cvat/exchange-oracle/src/chain/web3.py +++ b/packages/examples/cvat/exchange-oracle/src/chain/web3.py @@ -19,6 +19,16 @@ } ] # ABI for fetching token symbol +decimals_abi = [ + { + "constant": True, + "inputs": [], + "name": "decimals", + "outputs": [{"name": "", "type": "uint8"}], + "type": "function", + } +] # ABI for fetching token decimals (ERC-20 optional method; https://eips.ethereum.org/EIPS/eip-20) + def get_web3(chain_id: int | Networks): match chain_id: @@ -83,3 +93,9 @@ def get_token_symbol(chain_id: int, token_address: str) -> str: w3 = get_web3(chain_id) contract = w3.eth.contract(address=w3.to_checksum_address(token_address), abi=symbol_abi) return contract.functions.symbol().call() + + +def get_token_decimals(chain_id: int, token_address: str) -> int: + w3 = get_web3(chain_id) + contract = w3.eth.contract(address=w3.to_checksum_address(token_address), abi=decimals_abi) + return contract.functions.decimals().call() diff --git a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py index 0c996dfbc2..a656ba6457 100644 --- a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py @@ -6,6 +6,7 @@ from argparse import ArgumentParser from collections.abc import Generator from contextlib import ExitStack, contextmanager +from decimal import Decimal from logging import Logger from pathlib import Path, PurePosixPath from typing import Any @@ -118,6 +119,9 @@ def patched_get_escrow(chain_id: int, escrow_address: str) -> EscrowData: mock.patch.object(EscrowUtils, "get_escrow", patched_get_escrow), mock.patch("src.chain.escrow.get_token_symbol", return_value="HMT"), mock.patch("src.chain.web3.get_token_symbol", return_value="HMT"), + mock.patch( + "src.handlers.job_creation.utils.get_remaining_escrow_funds", return_value=Decimal(100) + ), ): yield diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index 5746ba5295..d86959e39e 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -1,13 +1,12 @@ from __future__ import annotations import os -from decimal import Decimal from typing import TYPE_CHECKING, TypeVar from datumaro.util.image import IMAGE_EXTENSIONS import src.cvat.api_calls as cvat_api -from src.chain.escrow import get_escrow +from src.chain.escrow import get_remaining_escrow_funds from src.core.tasks import TaskTypes from src.services.cloud import CloudProviders @@ -54,12 +53,14 @@ def get_assignment_bounty( def _get_assignment_bounty_from_escrow( escrow_address: str, chain_id: int, job_count: int ) -> str | None: - # TODO: refine (fees, already-paid amount, token decimals). Rough split of the escrow's total - # funds across the jobs. if not job_count: return None - escrow = get_escrow(chain_id, escrow_address) - return str(Decimal(escrow.total_funded_amount) / job_count) + + funds = get_remaining_escrow_funds(chain_id, escrow_address) + if funds is None: + return None + + return str(funds / job_count) def is_image(path: str) -> bool: diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py index 2c853789f5..d2c4615b6f 100644 --- a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py +++ b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py @@ -1,5 +1,6 @@ import json import subprocess +from decimal import Decimal from pathlib import Path from unittest.mock import Mock, patch @@ -143,16 +144,18 @@ def test_create_audio_transcription_task(fxt_audio_transcription_input): manifest, ds_rois_tsv, gt_tsv = fxt_audio_transcription_input cvat_api = _make_cvat_api_mock() - escrow = Mock(total_funded_amount="40") with ( patch.object(handlers, "get_escrow_manifest", return_value=manifest), patch("src.handlers.job_creation.builders.audio.transcription.cvat_api", cvat_api), - patch("src.handlers.job_creation.utils.get_escrow", return_value=escrow) as mock_get_escrow, + patch( + "src.handlers.job_creation.utils.get_remaining_escrow_funds", + return_value=Decimal(40), + ) as mock_remaining_funds, ): handlers.create_task(ESCROW_ADDRESS, CHAIN_ID) # v2 manifest → per-assignment bounty derived from escrow funds / job count - mock_get_escrow.assert_called_once_with(CHAIN_ID, ESCROW_ADDRESS) + mock_remaining_funds.assert_called_once_with(CHAIN_ID, ESCROW_ADDRESS) # Check CVAT API calls cvat_api.create_project.assert_called_once() @@ -172,7 +175,7 @@ def test_create_audio_transcription_task(fxt_audio_transcription_input): assert project.cvat_cloudstorage_id == 100 assert project.job_type == TaskTypes.audio_transcription.value assert project.chain_id == CHAIN_ID - assert project.assignment_bounty == "10" # 40 escrow funds / 4 assignments + assert project.assignment_bounty == "10" # 40 tokens remaining / 4 assignments tasks = db_service.get_tasks_by_cvat_project_id(session, project.cvat_id) assert len(tasks) == assignment_count