init
This commit is contained in:
commit
4c38d67a23
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
7
AGENT.md
Normal 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
46
AGENTS.md
Normal 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
59
README.md
Normal 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
85
docs/architecture.md
Normal 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
16
main.py
Normal 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
19
pyproject.toml
Normal 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
5
src/core/__init__.py
Normal 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
9
src/core/errors.py
Normal 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
0
src/core/py.typed
Normal file
5
src/eth/__init__.py
Normal file
5
src/eth/__init__.py
Normal 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
0
src/eth/py.typed
Normal file
37
src/eth/units.py
Normal file
37
src/eth/units.py
Normal 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
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Test namespace, separate from the source packages of the same name."""
|
||||||
1
tests/eth/__init__.py
Normal file
1
tests/eth/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Ethereum unit tests, discoverable by standard-library unittest."""
|
||||||
46
tests/eth/test_units.py
Normal file
46
tests/eth/test_units.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user