This commit is contained in:
halo 2026-09-08 09:29:20 -04:00
commit 4c38d67a23
16 changed files with 358 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Python / local tooling
__pycache__/
*.py[cod]
.venv/
venv/
*.egg-info/
build/
dist/
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
.idea/
.DS_Store
# Local configuration and key material must never be committed.
.env
.env.*
!.env.example
*.pem
*.key
keystore/

7
AGENT.md Normal file
View File

@ -0,0 +1,7 @@
# Agent 规则入口
本项目的唯一协作规则正文是 [AGENTS.md](AGENTS.md)。开始工作前请完整阅读并遵守它,
同时遵守上级目录适用的规则。
本文件保留用户约定的 `AGENT.md` 入口;规则只在 `AGENTS.md` 维护,避免两份内容漂移。
项目设计见 [docs/architecture.md](docs/architecture.md),使用方式见 [README.md](README.md)。

46
AGENTS.md Normal file
View File

@ -0,0 +1,46 @@
# ituac-crypto-hub 开发约定
## 定位与边界
- 本仓库提供 ETH、后续 BTC 等链相关操作的 Python 能力库,不是交易所、交易策略或资产托管系统。
- 默认中文沟通和文档,英文代码标识;仅修改本仓库内与当前任务相关的文件。
- 开始前检查 `git status --short --branch`、HEAD新仓库可能不存在及相关差异。
保留用户已有的工作树和暂存区内容,不自动提交、推送、重置、清理、部署或操作数据库。
- 仅讨论或只读请求不构成开发授权。新增依赖、框架和真实链操作应与明确需求匹配。
## 架构约束
- 使用 Python 3.11+、`src/core``src/eth` 同级包布局及 `pyproject.toml` 构建配置。
- `core` 仅容纳已被实际需求证明的公共原语;不能反向依赖币种包或链 SDK。
- `eth`、未来 `btc` 等包同级且互不依赖,各自拥有链模型、用例和适配器。
- 不把 ETH 的账户、nonce、gas 等概念强加给其他链;不提前实现万能基类、注册中心或空服务层。
- 外部节点、SDK、签名器接入时放入对应链的适配器通过明确接口注入导入包不得发起网络请求。
- 只导出稳定公共接口;新增能力、依赖、执行方式或边界时同步更新 README 和架构文档。
## 金额、资金与安全
- 链上金额用最小单位整数;人类可读金额只接受明确的十进制字符串或 `Decimal`
拒绝 `float`、非法数、负金额和不可精确表示的最小单位,不隐式四舍五入。
- 不手写密码学算法,不把通用地址格式检查描述成完整校验;代币精度不能套用 ETH 原生币精度。
- 不读取、打印、硬编码或提交真实私钥、助记词、RPC 凭据。忽略规则不能替代秘密审查。
- 网络和 chain ID 必须显式指定并核验,不默认连接主网;测试默认离线,集成测试明确选择加入。
- 签名、广播、授权合约、转账属于独立高风险操作,必须明确目标网络、账户、金额、费用及用户授权。
- 后续实现必须分离构建、签名、广播、回执和最终确认;广播超时是结果未知,不代表失败,不能盲目重试。
- 未支持或状态未知时明确报错,不能伪造余额、交易成功或确认状态。
## 代码与最小验证
- 公共函数需类型注解和契约说明;模块小而职责单一,异常类型明确,避免吞掉外部错误。
- 单元测试使用标准库 `unittest`,按币种组织;先覆盖实际行为、错误边界与金额精度,
不为增加测试数量堆积相同断言。网络替身的结果不能被描述为真实链验证。
- 保留 `tests/__init__.py`,测试发现使用 `-t .`,避免 `tests/eth` 被当作顶层 `eth` 遮蔽源码包。
- 当前不安装依赖即可在根目录执行:
```sh
PYTHONPATH=src python3 -m unittest discover -s tests -t . -v
python3 -m compileall -q src tests
git diff --check
```
- 修改打包配置时,在本地虚拟环境验证安装与包导入,不修改全局 Python 环境。
- 交付时检查新增文件和最终差异,说明已完成、实际验证、未验证范围;不把离线测试通过宣称为真实资金闭环。

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# ituac-crypto-hub
面向 ETH、BTC 等虚拟货币相关操作的模块化 Python 能力库。先建设独立的 `eth` 包,
其他链随实际需求按同级包扩展。
## 当前范围
- 已实现Python 包骨架、公共异常、ETH / Wei 精确单位转换、离线测试。
- 尚未实现:节点 RPC、余额查询、钱包管理、交易签名与广播、ERC-20、BTC。
- 当前无运行时第三方依赖;不是 HTTP 服务,导入及示例均不会连接网络或操作资金。
- 原有 `main.py` 保留为 IDE 示例,不作为库入口。
## 快速开始
使用 Python 3.11+,从仓库根目录执行:
```sh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python -c 'from eth import eth_to_wei; print(eth_to_wei("0.1"))'
```
首次安装可能需要联网获取构建工具。仅运行离线测试不需要安装依赖:
```sh
PYTHONPATH=src python3 -m unittest discover -s tests -t . -v
```
`-t .` 让测试以 `tests.eth` 命名加载,避免覆盖源码中的 `eth` 包。
```python
from eth import eth_to_wei, wei_to_eth
amount_wei = eth_to_wei("0.1") # 100000000000000000
amount_eth = wei_to_eth(amount_wei) # Decimal('0.100000000000000000')
```
`eth_to_wei` 接受十进制字符串或 `Decimal``wei_to_eth` 接受非负整数(不接受 `bool`)。
非法输入、负数及不足一个 Wei 的非零精度会抛出 `InvalidAmountError`,不会静默舍入。
转换只表示金额,不验证余额或交易协议的数值上限,也不用于推断代币精度。
## 目录
```text
AGENT.md # 兼容入口,指向唯一规则正文
AGENTS.md # 项目协作与安全规则
docs/architecture.md # 分层、扩展方式和演进计划
pyproject.toml # Python 包元数据及构建配置
src/
core/ # 公共原语(目前为异常)
eth/ # ETH 专属能力(目前为单位转换)
tests/
__init__.py # 将测试放在 tests 命名空间
eth/ # ETH 离线行为测试
```
新增链与后续能力的边界见 [架构文档](docs/architecture.md)。
ETH 单位依据:[Ethereum 官方 Ether 技术说明](https://ethereum.org/developers/docs/intro-to-ether)。

85
docs/architecture.md Normal file
View File

@ -0,0 +1,85 @@
# 项目架构
## 1. 背景与本次决策
目标是沉淀可复用的虚拟货币操作解决方案,从 ETH 开始,逐步加入 BTC 等链。
现有仓库只有 Python IDE 示例,故延续 Python采用单仓库、单分发包、链能力模块化布局。
`src` 仅作为源码目录,直接包含同级 `core``eth` 包,不再增加项目名命名空间。
分发包名称仍为 `ituac-crypto-hub`,导入使用 `from eth import ...` / `from core import ...`
顶层包名较通用,使用项目独立虚拟环境;未来引入 SDK 时检查是否与 `eth``core` 同名冲突。
本次状态:基础骨架已落地;下文节点、钱包和交易能力均为后续方向,不代表已经实现。
备选方案及取舍:
- 暂不引入 FastAPI / 微服务:尚无 HTTP、认证或部署需求先让能力能被脚本和未来服务复用。
- 暂不为每条链建立独立仓库或分发包:降低早期维护成本;需独立发布或出现依赖冲突时再拆分。
- 不建立统一交易大接口:仅有 ETH 首期需求,提前抽象容易把链特有语义泄漏到公共层。
- 暂不安装链 SDK首期只有离线基础能力节点接入时再按官方维护状态、接口和测试支持选型。
## 2. 依赖方向
```text
调用方(脚本 / 未来 API / 任务)
↓ ↓
eth btc未来
↓ ↓
core
```
`core` 不导入任何币种包;币种包互不导入。当前 `eth.units` 依赖 `core.errors`
`eth.__init__` 暴露经过测试的金额转换接口。
每条链在真实需求出现时再增长为:
```text
eth/
units.py # 已有:原生币单位转换
models.py # 未来:本链请求、结果和值对象
ports.py # 未来:节点与签名器接口,按能力拆分
services/ # 未来:查询、交易等用例,不绑定 SDK
adapters/ # 未来RPC / SDK / 外部签名器实现
```
以上未来文件不预建空壳。外部适配器实现端口,调用入口负责组装和注入;用例不直接实例化 SDK。
单元测试注入离线替身;集成测试独立配置网络及节点,不在包导入时建立连接。
## 3. 公共层与链专属层
| 归属 | 内容 |
| --- | --- |
| `core` | 当前公共异常;未来有真实复用证据的原语 |
| `eth` | 原生 ETH 单位、后续地址 / chain ID / 费用 / 交易与回执语义 |
| 未来 `btc` | BTC 自有单位、网络 / 地址 / UTXO / 手续费 / 交易模型 |
| 调用应用 | 身份权限、业务订单、持久化、调度、审计和部署,当前不实现 |
链与资产分开:原生 ETH 的单位转换不能用于未知精度的代币。
ERC-20 若进入范围,先归于 ETH / EVM 相关能力,不因币种名称拆成重复的链客户端。
其他 EVM 链及共享 `evm` 层只有在需求明确、复用证据充分后再决定。
## 4. 后续操作的安全契约
- 节点 URL、目标网络、预期链标识显式配置并核验节点身份不得隐式落到主网。
- 金额边界使用最小单位整数,展示才转换 Decimal拒绝浮点和静默精度损失。
- 密钥通过专门签名接口管理,不进入普通配置对象、日志、测试夹具或异常输出;不自行实现密码学。
- 交易构建、费用估算、签名、广播、回执查询及最终确认分离。返回交易哈希不等于成功。
- 对外错误需区分输入错误、节点拒绝、传输故障、响应格式错误、执行失败和结果未知。
- 广播超时不自动重发资金操作;引入写能力前明确幂等、并发、费用上限、重试和链重组策略。
- 真实转账或合约授权必须另行明确授权;离线测试不构成资金操作许可。
## 5. 增加 BTC 等链
1. 明确该链首批用例与非目标,验证协议和 SDK 官方资料。
2. 新建 `src/btc/`,独立维护公共接口和链特有模型,不从 `eth` 继承。
3. 新建 `tests/btc/`,验证单位、边界、失败路径;有节点适配器后再提供可选集成测试。
4. SDK 依赖优先用按链可选依赖声明,防止 ETH 用户被迫安装 BTC 全部依赖。
5. 只有已经出现的共同需求才提取到 `core`,同时更新此文档和 README。
## 6. 建议演进顺序(待具体需求确认)
1. ETH 只读接入:节点身份、区块、原生币余额、交易及回执查询。
2. ETH 写能力:构建和估算 → 独立签名器 → 显式广播 → 确认跟踪及异常恢复。
3. 按需求扩展代币、事件查询等能力。
4. BTC 首期只读能力,再按其自身模型补充资金操作。
验收分开记录源码 / 离线测试 / 本地链或测试网 / 真实节点 / 真实资金证据。
目前只提供并验证基础库不声明主网、RPC 或真实交易已验证。

16
main.py Normal file
View File

@ -0,0 +1,16 @@
# This is a sample Python script.
# Press ⌃R to execute it or replace it with your code.
# Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings.
def print_hi(name):
# Use a breakpoint in the code line below to debug your script.
print(f'Hi, {name}') # Press ⌘F8 to toggle the breakpoint.
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
print_hi('PyCharm')
# See PyCharm help at https://www.jetbrains.com/help/pycharm/

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "ituac-crypto-hub"
version = "0.1.0"
description = "Modular cryptocurrency operation capabilities, starting with Ethereum"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
[tool.setuptools.packages.find]
where = ["src"]
namespaces = false
[tool.setuptools.package-data]
core = ["py.typed"]
eth = ["py.typed"]

5
src/core/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""Shared primitives; must not depend on chain-specific packages."""
from .errors import CryptoHubError, InvalidAmountError
__all__ = ["CryptoHubError", "InvalidAmountError"]

9
src/core/errors.py Normal file
View File

@ -0,0 +1,9 @@
"""Public exceptions shared by cryptocurrency capabilities."""
class CryptoHubError(Exception):
"""Base exception for errors raised by this library."""
class InvalidAmountError(CryptoHubError, ValueError):
"""Amount has an invalid type, sign, value, or smallest-unit precision."""

0
src/core/py.typed Normal file
View File

5
src/eth/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""Ethereum-native capabilities (no RPC or signing implementation yet)."""
from .units import WEI_PER_ETH, eth_to_wei, wei_to_eth
__all__ = ["WEI_PER_ETH", "eth_to_wei", "wei_to_eth"]

0
src/eth/py.typed Normal file
View File

37
src/eth/units.py Normal file
View File

@ -0,0 +1,37 @@
"""Exact native ETH conversions, independent of Decimal context precision."""
from decimal import Decimal, InvalidOperation
from core import InvalidAmountError
WEI_PER_ETH = 10**18
def eth_to_wei(amount: str | Decimal) -> int:
"""Convert non-negative ETH to Wei; reject float and fractional Wei.
This is a unit conversion, not validation of transaction protocol limits.
"""
if not isinstance(amount, (str, Decimal)):
raise InvalidAmountError("ETH amount must be a decimal string or Decimal")
try:
value = Decimal(amount)
except InvalidOperation:
raise InvalidAmountError("ETH amount must be a valid decimal") from None
if not value.is_finite() or value < 0:
raise InvalidAmountError("ETH amount must be finite and non-negative")
# Integer arithmetic avoids rounding under the caller's Decimal context.
numerator, denominator = value.as_integer_ratio()
wei, remainder = divmod(numerator * WEI_PER_ETH, denominator)
if remainder:
raise InvalidAmountError("ETH amount must be exactly representable in Wei")
return wei
def wei_to_eth(amount: int) -> Decimal:
"""Convert non-negative integer Wei to exact ETH without rounding."""
if isinstance(amount, bool) or not isinstance(amount, int) or amount < 0:
raise InvalidAmountError("Wei amount must be a non-negative integer")
integer = Decimal(amount).as_tuple()
return Decimal((integer.sign, integer.digits, -18))

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Test namespace, separate from the source packages of the same name."""

1
tests/eth/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Ethereum unit tests, discoverable by standard-library unittest."""

46
tests/eth/test_units.py Normal file
View File

@ -0,0 +1,46 @@
"""Offline behavior and precision checks for native ETH amounts."""
import unittest
from decimal import Decimal, localcontext
from core import CryptoHubError, InvalidAmountError
from eth import WEI_PER_ETH, eth_to_wei, wei_to_eth
class EthUnitsTest(unittest.TestCase):
def test_exact_conversions(self):
for eth, wei in [("0", 0), ("1", WEI_PER_ETH), ("0.1", 10**17),
("0.000000000000000001", 1), ("1.0000000000000000000", WEI_PER_ETH)]:
with self.subTest(eth=eth):
self.assertEqual(eth_to_wei(eth), wei)
self.assertEqual(eth_to_wei(Decimal(eth)), wei)
self.assertEqual(wei_to_eth(wei), Decimal(eth))
def test_invalid_eth_is_rejected(self):
for amount in [0.1, True, 1, None, "", "invalid", "-1", "NaN", "sNaN",
"Infinity", "-Infinity", "0.0000000000000000001"]:
with self.subTest(amount=amount):
with self.assertRaises(InvalidAmountError):
eth_to_wei(amount)
def test_invalid_wei_is_rejected(self):
for amount in [-1, True, False, 1.0, "1", Decimal("1"), None]:
with self.subTest(amount=amount):
with self.assertRaises(InvalidAmountError):
wei_to_eth(amount)
def test_large_amount_round_trip_ignores_decimal_precision(self):
wei = 123456789012345678901234567890123456789
expected = Decimal("123456789012345678901.234567890123456789")
with localcontext() as context:
context.prec = 6
self.assertEqual(wei_to_eth(wei), expected)
self.assertEqual(eth_to_wei(expected), wei)
self.assertEqual(eth_to_wei(wei_to_eth(wei)), wei)
def test_invalid_amount_is_public_library_error(self):
self.assertIsInstance(InvalidAmountError("invalid"), CryptoHubError)
if __name__ == "__main__":
unittest.main()