barter-sdk 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- barter_sdk-1.0.0/LICENSE +21 -0
- barter_sdk-1.0.0/PKG-INFO +56 -0
- barter_sdk-1.0.0/README.md +33 -0
- barter_sdk-1.0.0/barter_sdk.egg-info/PKG-INFO +56 -0
- barter_sdk-1.0.0/barter_sdk.egg-info/SOURCES.txt +21 -0
- barter_sdk-1.0.0/barter_sdk.egg-info/dependency_links.txt +1 -0
- barter_sdk-1.0.0/barter_sdk.egg-info/requires.txt +8 -0
- barter_sdk-1.0.0/barter_sdk.egg-info/top_level.txt +1 -0
- barter_sdk-1.0.0/btr/__init__.py +19 -0
- barter_sdk-1.0.0/btr/client.py +68 -0
- barter_sdk-1.0.0/btr/compliance.py +64 -0
- barter_sdk-1.0.0/btr/constants.py +32 -0
- barter_sdk-1.0.0/btr/credit.py +29 -0
- barter_sdk-1.0.0/btr/exceptions.py +27 -0
- barter_sdk-1.0.0/btr/trade.py +185 -0
- barter_sdk-1.0.0/btr/trust.py +75 -0
- barter_sdk-1.0.0/btr/types.py +71 -0
- barter_sdk-1.0.0/pyproject.toml +30 -0
- barter_sdk-1.0.0/setup.cfg +4 -0
- barter_sdk-1.0.0/tests/test_compliance.py +44 -0
- barter_sdk-1.0.0/tests/test_integration.py +28 -0
- barter_sdk-1.0.0/tests/test_trade.py +44 -0
- barter_sdk-1.0.0/tests/test_trust.py +64 -0
barter_sdk-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 TheBarmaEffect
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: barter-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Proof of Trade Protocol — Python SDK for BTR-Trust soulbound reputation
|
|
5
|
+
Author: TheBarmaEffect
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: barter,bitcoin,lightning,trust,reputation,web3
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: web3>=6.0.0
|
|
16
|
+
Requires-Dist: pydantic>=2.0.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# barter-sdk
|
|
25
|
+
|
|
26
|
+
Python SDK for the BTR Proof of Trade Protocol.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install barter-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import btr
|
|
38
|
+
|
|
39
|
+
client = btr.BarterClient(network="sepolia")
|
|
40
|
+
|
|
41
|
+
# Get trust score
|
|
42
|
+
score = client.trust.get_score("0x742d...3F9a")
|
|
43
|
+
|
|
44
|
+
# Get full profile
|
|
45
|
+
profile = client.trust.get_profile("0x742d...3F9a")
|
|
46
|
+
print(f"Score: {profile.score}")
|
|
47
|
+
print(f"Trades: {profile.trade_count}")
|
|
48
|
+
|
|
49
|
+
# Compliance check
|
|
50
|
+
report = client.compliance.full_report("0x742d...3F9a")
|
|
51
|
+
print(f"Risk: {report.risk_level}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# barter-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the BTR Proof of Trade Protocol.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install barter-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import btr
|
|
15
|
+
|
|
16
|
+
client = btr.BarterClient(network="sepolia")
|
|
17
|
+
|
|
18
|
+
# Get trust score
|
|
19
|
+
score = client.trust.get_score("0x742d...3F9a")
|
|
20
|
+
|
|
21
|
+
# Get full profile
|
|
22
|
+
profile = client.trust.get_profile("0x742d...3F9a")
|
|
23
|
+
print(f"Score: {profile.score}")
|
|
24
|
+
print(f"Trades: {profile.trade_count}")
|
|
25
|
+
|
|
26
|
+
# Compliance check
|
|
27
|
+
report = client.compliance.full_report("0x742d...3F9a")
|
|
28
|
+
print(f"Risk: {report.risk_level}")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: barter-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Proof of Trade Protocol — Python SDK for BTR-Trust soulbound reputation
|
|
5
|
+
Author: TheBarmaEffect
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: barter,bitcoin,lightning,trust,reputation,web3
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: web3>=6.0.0
|
|
16
|
+
Requires-Dist: pydantic>=2.0.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# barter-sdk
|
|
25
|
+
|
|
26
|
+
Python SDK for the BTR Proof of Trade Protocol.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install barter-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import btr
|
|
38
|
+
|
|
39
|
+
client = btr.BarterClient(network="sepolia")
|
|
40
|
+
|
|
41
|
+
# Get trust score
|
|
42
|
+
score = client.trust.get_score("0x742d...3F9a")
|
|
43
|
+
|
|
44
|
+
# Get full profile
|
|
45
|
+
profile = client.trust.get_profile("0x742d...3F9a")
|
|
46
|
+
print(f"Score: {profile.score}")
|
|
47
|
+
print(f"Trades: {profile.trade_count}")
|
|
48
|
+
|
|
49
|
+
# Compliance check
|
|
50
|
+
report = client.compliance.full_report("0x742d...3F9a")
|
|
51
|
+
print(f"Risk: {report.risk_level}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
barter_sdk.egg-info/PKG-INFO
|
|
5
|
+
barter_sdk.egg-info/SOURCES.txt
|
|
6
|
+
barter_sdk.egg-info/dependency_links.txt
|
|
7
|
+
barter_sdk.egg-info/requires.txt
|
|
8
|
+
barter_sdk.egg-info/top_level.txt
|
|
9
|
+
btr/__init__.py
|
|
10
|
+
btr/client.py
|
|
11
|
+
btr/compliance.py
|
|
12
|
+
btr/constants.py
|
|
13
|
+
btr/credit.py
|
|
14
|
+
btr/exceptions.py
|
|
15
|
+
btr/trade.py
|
|
16
|
+
btr/trust.py
|
|
17
|
+
btr/types.py
|
|
18
|
+
tests/test_compliance.py
|
|
19
|
+
tests/test_integration.py
|
|
20
|
+
tests/test_trade.py
|
|
21
|
+
tests/test_trust.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
btr
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""BTR — Proof of Trade Protocol SDK"""
|
|
2
|
+
from btr.client import BarterClient
|
|
3
|
+
from btr.types import (
|
|
4
|
+
TrustProfile, Trade, TradeStatus, ComplianceReport,
|
|
5
|
+
AnomalyFlag, VelocityReport, ClusterReport, ScoreEvent
|
|
6
|
+
)
|
|
7
|
+
from btr.exceptions import (
|
|
8
|
+
BarterError, InvalidAddressError, TradeNotFoundError,
|
|
9
|
+
PreimageMismatchError, InsufficientAmountError, SelfTradeError
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BarterClient", "TrustProfile", "Trade", "TradeStatus",
|
|
15
|
+
"ComplianceReport", "AnomalyFlag", "VelocityReport",
|
|
16
|
+
"ClusterReport", "ScoreEvent", "BarterError",
|
|
17
|
+
"InvalidAddressError", "TradeNotFoundError",
|
|
18
|
+
"PreimageMismatchError", "InsufficientAmountError", "SelfTradeError",
|
|
19
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from web3 import Web3
|
|
4
|
+
from btr.trust import TrustClient
|
|
5
|
+
from btr.trade import TradeClient
|
|
6
|
+
from btr.credit import CreditClient
|
|
7
|
+
from btr.compliance import ComplianceClient
|
|
8
|
+
from btr.constants import CONTRACTS, ABIS, DEFAULT_RPC
|
|
9
|
+
|
|
10
|
+
class BarterClient:
|
|
11
|
+
"""Main client for the BTR Proof of Trade Protocol.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
import btr
|
|
15
|
+
client = btr.BarterClient(network="sepolia")
|
|
16
|
+
score = client.trust.get_score("0x...")
|
|
17
|
+
profile = client.trust.get_profile("0x...")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
rpc_url: Optional[str] = None,
|
|
22
|
+
network: str = "sepolia",
|
|
23
|
+
private_key: Optional[str] = None):
|
|
24
|
+
self.network = network
|
|
25
|
+
|
|
26
|
+
# Resolve RPC URL
|
|
27
|
+
url = rpc_url or os.environ.get("ALCHEMY_RPC_URL") or DEFAULT_RPC
|
|
28
|
+
self._w3 = Web3(Web3.HTTPProvider(url))
|
|
29
|
+
|
|
30
|
+
# Resolve private key
|
|
31
|
+
pk = private_key or os.environ.get("DEPLOYER_PRIVATE_KEY")
|
|
32
|
+
|
|
33
|
+
# Load contracts
|
|
34
|
+
addresses = CONTRACTS.get(network, {})
|
|
35
|
+
contracts = {}
|
|
36
|
+
|
|
37
|
+
for name in ["BarterCore", "BTRTrust", "BTRCredit"]:
|
|
38
|
+
addr = addresses.get(name)
|
|
39
|
+
abi = ABIS.get(name, [])
|
|
40
|
+
if addr and abi and self._w3.is_connected():
|
|
41
|
+
try:
|
|
42
|
+
contracts[name] = self._w3.eth.contract(
|
|
43
|
+
address=self._w3.to_checksum_address(addr),
|
|
44
|
+
abi=abi
|
|
45
|
+
)
|
|
46
|
+
except Exception:
|
|
47
|
+
contracts[name] = None
|
|
48
|
+
else:
|
|
49
|
+
contracts[name] = None
|
|
50
|
+
|
|
51
|
+
# Initialize sub-clients
|
|
52
|
+
self.trust = TrustClient(self._w3, contracts)
|
|
53
|
+
self.trade = TradeClient(self._w3, contracts, pk)
|
|
54
|
+
self.credit = CreditClient(self._w3, contracts)
|
|
55
|
+
self.compliance = ComplianceClient(self._w3, contracts)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def w3(self) -> Web3:
|
|
59
|
+
"""Access the underlying Web3 instance."""
|
|
60
|
+
return self._w3
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_connected(self) -> bool:
|
|
64
|
+
"""Check if connected to the network."""
|
|
65
|
+
try:
|
|
66
|
+
return self._w3.is_connected()
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List
|
|
3
|
+
from btr.types import AnomalyFlag, VelocityReport, ClusterReport, ComplianceReport
|
|
4
|
+
from btr.exceptions import InvalidAddressError
|
|
5
|
+
|
|
6
|
+
class ComplianceClient:
|
|
7
|
+
VELOCITY_THRESHOLD_24H = 20
|
|
8
|
+
VELOCITY_THRESHOLD_7D = 100
|
|
9
|
+
CONCENTRATION_THRESHOLD = 0.9
|
|
10
|
+
|
|
11
|
+
def __init__(self, w3, contracts):
|
|
12
|
+
self._w3 = w3
|
|
13
|
+
self._core = contracts.get("BarterCore")
|
|
14
|
+
self._trust = contracts.get("BTRTrust")
|
|
15
|
+
|
|
16
|
+
def _validate_address(self, address: str) -> str:
|
|
17
|
+
if not address or not address.startswith("0x") or len(address) != 42:
|
|
18
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
19
|
+
return self._w3.to_checksum_address(address)
|
|
20
|
+
|
|
21
|
+
def get_flags(self, address: str) -> List[AnomalyFlag]:
|
|
22
|
+
"""Analyze address for behavioral anomalies."""
|
|
23
|
+
self._validate_address(address)
|
|
24
|
+
flags = []
|
|
25
|
+
# Would analyze on-chain data for anomalies:
|
|
26
|
+
# - Rapid cycling (many trades in short time)
|
|
27
|
+
# - Reciprocal washing (A->B->A pattern)
|
|
28
|
+
# - Score inflation attempts
|
|
29
|
+
return flags
|
|
30
|
+
|
|
31
|
+
def get_velocity(self, address: str) -> VelocityReport:
|
|
32
|
+
"""Get trading velocity metrics."""
|
|
33
|
+
addr = self._validate_address(address)
|
|
34
|
+
return VelocityReport(
|
|
35
|
+
address=addr,
|
|
36
|
+
trades_24h=0,
|
|
37
|
+
trades_7d=0,
|
|
38
|
+
trades_30d=0,
|
|
39
|
+
avg_amount_sats=0,
|
|
40
|
+
is_elevated=False
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def detect_isolation(self, address: str, depth: int = 2) -> ClusterReport:
|
|
44
|
+
"""Detect graph isolation / cluster analysis."""
|
|
45
|
+
addr = self._validate_address(address)
|
|
46
|
+
return ClusterReport(
|
|
47
|
+
address=addr,
|
|
48
|
+
cluster_size=0,
|
|
49
|
+
isolation_score=0.0,
|
|
50
|
+
connected_addresses=[],
|
|
51
|
+
depth_analyzed=depth
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def full_report(self, address: str) -> ComplianceReport:
|
|
55
|
+
"""Generate full compliance report for an address."""
|
|
56
|
+
addr = self._validate_address(address)
|
|
57
|
+
return ComplianceReport(
|
|
58
|
+
address=addr,
|
|
59
|
+
flags=self.get_flags(addr),
|
|
60
|
+
velocity=self.get_velocity(addr),
|
|
61
|
+
cluster=self.detect_isolation(addr),
|
|
62
|
+
risk_level="low",
|
|
63
|
+
generated_at=datetime.utcnow()
|
|
64
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
SEPOLIA_CHAIN_ID = 11155111
|
|
5
|
+
DEFAULT_RPC = "https://eth-sepolia.g.alchemy.com/v2/demo"
|
|
6
|
+
MIN_TRADE_SATS = 1000
|
|
7
|
+
MAX_PAIR_TRUST = 5
|
|
8
|
+
TRADE_EXPIRY_SECONDS = 7 * 24 * 3600
|
|
9
|
+
|
|
10
|
+
# Contract addresses (Sepolia deployment)
|
|
11
|
+
CONTRACTS = {
|
|
12
|
+
"sepolia": {
|
|
13
|
+
"BarterCore": "0x0000000000000000000000000000000000000001",
|
|
14
|
+
"BTRTrust": "0x0000000000000000000000000000000000000002",
|
|
15
|
+
"BTRCredit": "0x0000000000000000000000000000000000000003",
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def _load_abi(name: str) -> list:
|
|
20
|
+
abi_dir = os.path.join(os.path.dirname(__file__), "abis")
|
|
21
|
+
path = os.path.join(abi_dir, f"{name}.json")
|
|
22
|
+
if os.path.exists(path):
|
|
23
|
+
with open(path) as f:
|
|
24
|
+
data = json.load(f)
|
|
25
|
+
return data if isinstance(data, list) else data.get("abi", [])
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
ABIS = {
|
|
29
|
+
"BarterCore": _load_abi("BarterCore"),
|
|
30
|
+
"BTRTrust": _load_abi("BTRTrust"),
|
|
31
|
+
"BTRCredit": _load_abi("BTRCredit"),
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from btr.exceptions import InvalidAddressError
|
|
2
|
+
|
|
3
|
+
class CreditClient:
|
|
4
|
+
def __init__(self, w3, contracts):
|
|
5
|
+
self._w3 = w3
|
|
6
|
+
self._credit = contracts.get("BTRCredit")
|
|
7
|
+
|
|
8
|
+
def _validate_address(self, address: str) -> str:
|
|
9
|
+
if not address or not address.startswith("0x") or len(address) != 42:
|
|
10
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
11
|
+
return self._w3.to_checksum_address(address)
|
|
12
|
+
|
|
13
|
+
def balance_of(self, address: str) -> int:
|
|
14
|
+
"""Get BTR-C balance in wei."""
|
|
15
|
+
addr = self._validate_address(address)
|
|
16
|
+
if self._credit is None:
|
|
17
|
+
return 0
|
|
18
|
+
return self._credit.functions.balanceOf(addr).call()
|
|
19
|
+
|
|
20
|
+
def balance_formatted(self, address: str) -> float:
|
|
21
|
+
"""Get BTR-C balance in human-readable format."""
|
|
22
|
+
raw = self.balance_of(address)
|
|
23
|
+
return raw / 1e18
|
|
24
|
+
|
|
25
|
+
def total_supply(self) -> int:
|
|
26
|
+
"""Get total BTR-C supply."""
|
|
27
|
+
if self._credit is None:
|
|
28
|
+
return 0
|
|
29
|
+
return self._credit.functions.totalSupply().call()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class BarterError(Exception):
|
|
2
|
+
"""Base exception for all BTR SDK errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class InvalidAddressError(BarterError):
|
|
6
|
+
"""Raised when an Ethereum address is invalid."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class TradeNotFoundError(BarterError):
|
|
10
|
+
"""Raised when a trade ID does not exist on-chain."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class PreimageMismatchError(BarterError):
|
|
14
|
+
"""Raised when SHA256(preimage) != payment_hash."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class InsufficientAmountError(BarterError):
|
|
18
|
+
"""Raised when trade amount is below minimum (1000 sats)."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class SelfTradeError(BarterError):
|
|
22
|
+
"""Raised when attempting to trade with yourself."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class NetworkError(BarterError):
|
|
26
|
+
"""Raised on RPC/network connectivity issues."""
|
|
27
|
+
pass
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List
|
|
4
|
+
from btr.types import Trade, TradeStatus
|
|
5
|
+
from btr.exceptions import (
|
|
6
|
+
InvalidAddressError, TradeNotFoundError,
|
|
7
|
+
PreimageMismatchError, InsufficientAmountError, SelfTradeError
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class TradeClient:
|
|
11
|
+
MIN_TRADE_SATS = 1000
|
|
12
|
+
|
|
13
|
+
def __init__(self, w3, contracts, private_key=None):
|
|
14
|
+
self._w3 = w3
|
|
15
|
+
self._core = contracts.get("BarterCore")
|
|
16
|
+
self._pk = private_key
|
|
17
|
+
self._account = None
|
|
18
|
+
if private_key and w3:
|
|
19
|
+
self._account = w3.eth.account.from_key(private_key)
|
|
20
|
+
|
|
21
|
+
def _validate_address(self, address: str) -> str:
|
|
22
|
+
if not address or not isinstance(address, str):
|
|
23
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
24
|
+
if not address.startswith("0x") or len(address) != 42:
|
|
25
|
+
raise InvalidAddressError(f"Invalid address format: {address}")
|
|
26
|
+
try:
|
|
27
|
+
return self._w3.to_checksum_address(address)
|
|
28
|
+
except Exception:
|
|
29
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
30
|
+
|
|
31
|
+
def _get_sender(self) -> str:
|
|
32
|
+
if self._account:
|
|
33
|
+
return self._account.address
|
|
34
|
+
accounts = self._w3.eth.accounts
|
|
35
|
+
if accounts:
|
|
36
|
+
return accounts[0]
|
|
37
|
+
raise Exception("No account available. Provide a private_key.")
|
|
38
|
+
|
|
39
|
+
def propose(self, counterparty: str, payment_hash: str,
|
|
40
|
+
amount_sats: int, category: str = "", description: str = "") -> str:
|
|
41
|
+
"""Propose a new trade. Returns trade ID."""
|
|
42
|
+
sender = self._get_sender()
|
|
43
|
+
cp = self._validate_address(counterparty)
|
|
44
|
+
|
|
45
|
+
if cp.lower() == sender.lower():
|
|
46
|
+
raise SelfTradeError("Cannot trade with yourself")
|
|
47
|
+
if amount_sats < self.MIN_TRADE_SATS:
|
|
48
|
+
raise InsufficientAmountError(f"Amount {amount_sats} below minimum {self.MIN_TRADE_SATS} sats")
|
|
49
|
+
if len(description) > 200:
|
|
50
|
+
raise ValueError("Description too long (max 200 chars)")
|
|
51
|
+
|
|
52
|
+
if self._core is None:
|
|
53
|
+
raise Exception("Contract not connected")
|
|
54
|
+
|
|
55
|
+
tx = self._core.functions.proposeTrade(
|
|
56
|
+
cp, bytes.fromhex(payment_hash[2:]) if payment_hash.startswith("0x") else bytes.fromhex(payment_hash),
|
|
57
|
+
amount_sats, category, description
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self._pk:
|
|
61
|
+
built = tx.build_transaction({
|
|
62
|
+
"from": sender,
|
|
63
|
+
"nonce": self._w3.eth.get_transaction_count(sender),
|
|
64
|
+
"gas": 300000,
|
|
65
|
+
})
|
|
66
|
+
signed = self._w3.eth.account.sign_transaction(built, self._pk)
|
|
67
|
+
tx_hash = self._w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
68
|
+
receipt = self._w3.eth.wait_for_transaction_receipt(tx_hash)
|
|
69
|
+
else:
|
|
70
|
+
receipt = tx.transact({"from": sender})
|
|
71
|
+
receipt = self._w3.eth.wait_for_transaction_receipt(receipt)
|
|
72
|
+
|
|
73
|
+
# Extract tradeId from event logs
|
|
74
|
+
logs = self._core.events.TradeProposed().process_receipt(receipt)
|
|
75
|
+
if logs:
|
|
76
|
+
return "0x" + logs[0]["args"]["tradeId"].hex()
|
|
77
|
+
return receipt["transactionHash"].hex()
|
|
78
|
+
|
|
79
|
+
def accept(self, trade_id: str) -> dict:
|
|
80
|
+
"""Accept a proposed trade."""
|
|
81
|
+
tid = bytes.fromhex(trade_id[2:]) if trade_id.startswith("0x") else bytes.fromhex(trade_id)
|
|
82
|
+
sender = self._get_sender()
|
|
83
|
+
tx = self._core.functions.acceptTrade(tid)
|
|
84
|
+
|
|
85
|
+
if self._pk:
|
|
86
|
+
built = tx.build_transaction({
|
|
87
|
+
"from": sender,
|
|
88
|
+
"nonce": self._w3.eth.get_transaction_count(sender),
|
|
89
|
+
"gas": 200000,
|
|
90
|
+
})
|
|
91
|
+
signed = self._w3.eth.account.sign_transaction(built, self._pk)
|
|
92
|
+
tx_hash = self._w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
93
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
94
|
+
else:
|
|
95
|
+
tx_hash = tx.transact({"from": sender})
|
|
96
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
97
|
+
|
|
98
|
+
def settle(self, trade_id: str, preimage: str) -> dict:
|
|
99
|
+
"""Settle a trade with Lightning preimage. Verifies SHA256."""
|
|
100
|
+
tid = bytes.fromhex(trade_id[2:]) if trade_id.startswith("0x") else bytes.fromhex(trade_id)
|
|
101
|
+
pre = bytes.fromhex(preimage[2:]) if preimage.startswith("0x") else bytes.fromhex(preimage)
|
|
102
|
+
|
|
103
|
+
# Local verification before submitting
|
|
104
|
+
trade = self.get(trade_id)
|
|
105
|
+
expected_hash = trade.payment_hash
|
|
106
|
+
# The contract uses sha256(abi.encode(preimage))
|
|
107
|
+
# abi.encode for bytes32 is just the 32 bytes padded
|
|
108
|
+
computed = hashlib.sha256(pre.rjust(32, b'\x00')).hexdigest()
|
|
109
|
+
if not expected_hash.lower().endswith(computed.lower()):
|
|
110
|
+
raise PreimageMismatchError("SHA256(preimage) does not match payment_hash")
|
|
111
|
+
|
|
112
|
+
sender = self._get_sender()
|
|
113
|
+
tx = self._core.functions.settleTrade(tid, pre)
|
|
114
|
+
|
|
115
|
+
if self._pk:
|
|
116
|
+
built = tx.build_transaction({
|
|
117
|
+
"from": sender,
|
|
118
|
+
"nonce": self._w3.eth.get_transaction_count(sender),
|
|
119
|
+
"gas": 400000,
|
|
120
|
+
})
|
|
121
|
+
signed = self._w3.eth.account.sign_transaction(built, self._pk)
|
|
122
|
+
tx_hash = self._w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
123
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
124
|
+
else:
|
|
125
|
+
tx_hash = tx.transact({"from": sender})
|
|
126
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
127
|
+
|
|
128
|
+
def dispute(self, trade_id: str) -> dict:
|
|
129
|
+
"""Dispute a trade."""
|
|
130
|
+
tid = bytes.fromhex(trade_id[2:]) if trade_id.startswith("0x") else bytes.fromhex(trade_id)
|
|
131
|
+
sender = self._get_sender()
|
|
132
|
+
tx = self._core.functions.disputeTrade(tid)
|
|
133
|
+
|
|
134
|
+
if self._pk:
|
|
135
|
+
built = tx.build_transaction({
|
|
136
|
+
"from": sender,
|
|
137
|
+
"nonce": self._w3.eth.get_transaction_count(sender),
|
|
138
|
+
"gas": 200000,
|
|
139
|
+
})
|
|
140
|
+
signed = self._w3.eth.account.sign_transaction(built, self._pk)
|
|
141
|
+
tx_hash = self._w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
142
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
143
|
+
else:
|
|
144
|
+
tx_hash = tx.transact({"from": sender})
|
|
145
|
+
return dict(self._w3.eth.wait_for_transaction_receipt(tx_hash))
|
|
146
|
+
|
|
147
|
+
def get(self, trade_id: str) -> Trade:
|
|
148
|
+
"""Get trade details by ID."""
|
|
149
|
+
tid = bytes.fromhex(trade_id[2:]) if trade_id.startswith("0x") else bytes.fromhex(trade_id)
|
|
150
|
+
if self._core is None:
|
|
151
|
+
raise TradeNotFoundError(f"Trade {trade_id} not found")
|
|
152
|
+
result = self._core.functions.trades(tid).call()
|
|
153
|
+
if result[6] == 0: # createdAt == 0
|
|
154
|
+
raise TradeNotFoundError(f"Trade {trade_id} not found")
|
|
155
|
+
|
|
156
|
+
status_map = {0: TradeStatus.PROPOSED, 1: TradeStatus.ACCEPTED,
|
|
157
|
+
2: TradeStatus.SETTLED, 3: TradeStatus.DISPUTED, 4: TradeStatus.EXPIRED}
|
|
158
|
+
|
|
159
|
+
return Trade(
|
|
160
|
+
trade_id="0x" + result[0].hex(),
|
|
161
|
+
party_a=result[1],
|
|
162
|
+
party_b=result[2],
|
|
163
|
+
payment_hash="0x" + result[3].hex(),
|
|
164
|
+
amount_sats=result[4],
|
|
165
|
+
status=status_map.get(result[5], TradeStatus.PROPOSED),
|
|
166
|
+
created_at=datetime.fromtimestamp(result[6]),
|
|
167
|
+
accepted_at=datetime.fromtimestamp(result[7]) if result[7] > 0 else None,
|
|
168
|
+
settled_at=datetime.fromtimestamp(result[8]) if result[8] > 0 else None,
|
|
169
|
+
category=result[9],
|
|
170
|
+
description=result[10],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def list_by_address(self, address: str) -> List[Trade]:
|
|
174
|
+
"""Get all trades for an address."""
|
|
175
|
+
addr = self._validate_address(address)
|
|
176
|
+
if self._core is None:
|
|
177
|
+
return []
|
|
178
|
+
trade_ids = self._core.functions.getTradesByAddress(addr).call()
|
|
179
|
+
trades = []
|
|
180
|
+
for tid in trade_ids:
|
|
181
|
+
try:
|
|
182
|
+
trades.append(self.get("0x" + tid.hex()))
|
|
183
|
+
except TradeNotFoundError:
|
|
184
|
+
continue
|
|
185
|
+
return trades
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from btr.types import TrustProfile, ScoreEvent
|
|
4
|
+
from btr.exceptions import InvalidAddressError
|
|
5
|
+
|
|
6
|
+
class TrustClient:
|
|
7
|
+
def __init__(self, w3, contracts):
|
|
8
|
+
self._w3 = w3
|
|
9
|
+
self._trust = contracts.get("BTRTrust")
|
|
10
|
+
self._core = contracts.get("BarterCore")
|
|
11
|
+
|
|
12
|
+
def _validate_address(self, address: str) -> str:
|
|
13
|
+
if not address or not isinstance(address, str):
|
|
14
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
15
|
+
if not address.startswith("0x") or len(address) != 42:
|
|
16
|
+
raise InvalidAddressError(f"Invalid address format: {address}")
|
|
17
|
+
try:
|
|
18
|
+
return self._w3.to_checksum_address(address)
|
|
19
|
+
except Exception:
|
|
20
|
+
raise InvalidAddressError(f"Invalid address: {address}")
|
|
21
|
+
|
|
22
|
+
def get_score(self, address: str) -> int:
|
|
23
|
+
"""Get the BTR-Trust score for a wallet address."""
|
|
24
|
+
addr = self._validate_address(address)
|
|
25
|
+
if self._trust is None:
|
|
26
|
+
return 0
|
|
27
|
+
return self._trust.functions.getScore(addr).call()
|
|
28
|
+
|
|
29
|
+
def get_profile(self, address: str) -> TrustProfile:
|
|
30
|
+
"""Get the full trust profile for a wallet address."""
|
|
31
|
+
addr = self._validate_address(address)
|
|
32
|
+
if self._trust is None:
|
|
33
|
+
return TrustProfile(
|
|
34
|
+
address=addr, score=0, unique_counterparties=0,
|
|
35
|
+
trade_count=0, completion_rate=0.0
|
|
36
|
+
)
|
|
37
|
+
result = self._trust.functions.getTrustProfile(addr).call()
|
|
38
|
+
score, unique, member_since_ts, trade_count, last_trade_ts = result
|
|
39
|
+
|
|
40
|
+
# Calculate completion rate from trade data
|
|
41
|
+
settled = 0
|
|
42
|
+
total = 0
|
|
43
|
+
if self._core:
|
|
44
|
+
trade_ids = self._core.functions.getTradesByAddress(addr).call()
|
|
45
|
+
total = len(trade_ids)
|
|
46
|
+
for tid in trade_ids:
|
|
47
|
+
trade = self._core.functions.trades(tid).call()
|
|
48
|
+
if trade[5] == 2: # Settled status
|
|
49
|
+
settled += 1
|
|
50
|
+
|
|
51
|
+
completion_rate = (settled / total * 100) if total > 0 else 0.0
|
|
52
|
+
|
|
53
|
+
return TrustProfile(
|
|
54
|
+
address=addr,
|
|
55
|
+
score=score,
|
|
56
|
+
unique_counterparties=unique,
|
|
57
|
+
member_since=datetime.fromtimestamp(member_since_ts) if member_since_ts > 0 else None,
|
|
58
|
+
trade_count=trade_count,
|
|
59
|
+
completion_rate=round(completion_rate, 1),
|
|
60
|
+
last_trade_at=datetime.fromtimestamp(last_trade_ts) if last_trade_ts > 0 else None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def get_history(self, address: str, from_date: Optional[datetime] = None, to_date: Optional[datetime] = None) -> List[ScoreEvent]:
|
|
64
|
+
"""Get score change history for an address."""
|
|
65
|
+
addr = self._validate_address(address)
|
|
66
|
+
# Would query ScoreUpdated events from the contract
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
def get_pair_count(self, addr_a: str, addr_b: str) -> int:
|
|
70
|
+
"""Get the number of trust-counted trades between two addresses."""
|
|
71
|
+
a = self._validate_address(addr_a)
|
|
72
|
+
b = self._validate_address(addr_b)
|
|
73
|
+
if self._trust is None:
|
|
74
|
+
return 0
|
|
75
|
+
return self._trust.functions.pairCount(a, b).call()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
class TradeStatus(str, Enum):
|
|
7
|
+
PROPOSED = "proposed"
|
|
8
|
+
ACCEPTED = "accepted"
|
|
9
|
+
SETTLED = "settled"
|
|
10
|
+
DISPUTED = "disputed"
|
|
11
|
+
EXPIRED = "expired"
|
|
12
|
+
|
|
13
|
+
class TrustProfile(BaseModel):
|
|
14
|
+
address: str
|
|
15
|
+
score: int
|
|
16
|
+
unique_counterparties: int
|
|
17
|
+
member_since: Optional[datetime] = None
|
|
18
|
+
trade_count: int = 0
|
|
19
|
+
completion_rate: float = 0.0
|
|
20
|
+
last_trade_at: Optional[datetime] = None
|
|
21
|
+
|
|
22
|
+
class Trade(BaseModel):
|
|
23
|
+
trade_id: str
|
|
24
|
+
party_a: str
|
|
25
|
+
party_b: str
|
|
26
|
+
payment_hash: str
|
|
27
|
+
amount_sats: int
|
|
28
|
+
status: TradeStatus
|
|
29
|
+
created_at: datetime
|
|
30
|
+
accepted_at: Optional[datetime] = None
|
|
31
|
+
settled_at: Optional[datetime] = None
|
|
32
|
+
category: str = ""
|
|
33
|
+
description: str = ""
|
|
34
|
+
|
|
35
|
+
class ScoreEvent(BaseModel):
|
|
36
|
+
block_number: int
|
|
37
|
+
timestamp: datetime
|
|
38
|
+
delta: int
|
|
39
|
+
new_score: int
|
|
40
|
+
counterparty: str
|
|
41
|
+
amount_sats: int
|
|
42
|
+
|
|
43
|
+
class AnomalyFlag(BaseModel):
|
|
44
|
+
flag_type: str
|
|
45
|
+
severity: str # "low", "medium", "high"
|
|
46
|
+
description: str
|
|
47
|
+
detected_at: datetime
|
|
48
|
+
details: dict = Field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
class VelocityReport(BaseModel):
|
|
51
|
+
address: str
|
|
52
|
+
trades_24h: int
|
|
53
|
+
trades_7d: int
|
|
54
|
+
trades_30d: int
|
|
55
|
+
avg_amount_sats: int
|
|
56
|
+
is_elevated: bool
|
|
57
|
+
|
|
58
|
+
class ClusterReport(BaseModel):
|
|
59
|
+
address: str
|
|
60
|
+
cluster_size: int
|
|
61
|
+
isolation_score: float
|
|
62
|
+
connected_addresses: List[str]
|
|
63
|
+
depth_analyzed: int
|
|
64
|
+
|
|
65
|
+
class ComplianceReport(BaseModel):
|
|
66
|
+
address: str
|
|
67
|
+
flags: List[AnomalyFlag]
|
|
68
|
+
velocity: VelocityReport
|
|
69
|
+
cluster: ClusterReport
|
|
70
|
+
risk_level: str # "low", "medium", "high"
|
|
71
|
+
generated_at: datetime
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "barter-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Proof of Trade Protocol — Python SDK for BTR-Trust soulbound reputation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{name = "TheBarmaEffect"}]
|
|
13
|
+
keywords = ["barter", "bitcoin", "lightning", "trust", "reputation", "web3"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"web3>=6.0.0",
|
|
22
|
+
"pydantic>=2.0.0",
|
|
23
|
+
"python-dotenv>=1.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=7.0", "pytest-cov", "pytest-asyncio"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["btr*"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from btr.compliance import ComplianceClient
|
|
3
|
+
from btr.types import ComplianceReport, VelocityReport, ClusterReport
|
|
4
|
+
from btr.exceptions import InvalidAddressError
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
VALID_ADDR = "0x742d35Cc6634C0532925a3b844Bc9e7595f3F9a0"
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def compliance_client():
|
|
11
|
+
w3 = MagicMock()
|
|
12
|
+
w3.to_checksum_address = lambda x: x
|
|
13
|
+
contracts = {"BarterCore": None, "BTRTrust": None}
|
|
14
|
+
return ComplianceClient(w3, contracts)
|
|
15
|
+
|
|
16
|
+
class TestGetFlags:
|
|
17
|
+
def test_returns_list(self, compliance_client):
|
|
18
|
+
flags = compliance_client.get_flags(VALID_ADDR)
|
|
19
|
+
assert isinstance(flags, list)
|
|
20
|
+
|
|
21
|
+
def test_invalid_address_raises(self, compliance_client):
|
|
22
|
+
with pytest.raises(InvalidAddressError):
|
|
23
|
+
compliance_client.get_flags("bad")
|
|
24
|
+
|
|
25
|
+
class TestFullReport:
|
|
26
|
+
def test_returns_compliance_report(self, compliance_client):
|
|
27
|
+
report = compliance_client.full_report(VALID_ADDR)
|
|
28
|
+
assert isinstance(report, ComplianceReport)
|
|
29
|
+
|
|
30
|
+
def test_has_all_fields(self, compliance_client):
|
|
31
|
+
report = compliance_client.full_report(VALID_ADDR)
|
|
32
|
+
assert hasattr(report, "flags")
|
|
33
|
+
assert hasattr(report, "velocity")
|
|
34
|
+
assert hasattr(report, "cluster")
|
|
35
|
+
assert hasattr(report, "risk_level")
|
|
36
|
+
assert hasattr(report, "generated_at")
|
|
37
|
+
|
|
38
|
+
def test_velocity_report(self, compliance_client):
|
|
39
|
+
velocity = compliance_client.get_velocity(VALID_ADDR)
|
|
40
|
+
assert isinstance(velocity, VelocityReport)
|
|
41
|
+
|
|
42
|
+
def test_cluster_report(self, compliance_client):
|
|
43
|
+
cluster = compliance_client.detect_isolation(VALID_ADDR)
|
|
44
|
+
assert isinstance(cluster, ClusterReport)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from btr.client import BarterClient
|
|
3
|
+
|
|
4
|
+
class TestBarterClient:
|
|
5
|
+
def test_init_default(self):
|
|
6
|
+
client = BarterClient()
|
|
7
|
+
assert client.network == "sepolia"
|
|
8
|
+
assert client.trust is not None
|
|
9
|
+
assert client.trade is not None
|
|
10
|
+
assert client.credit is not None
|
|
11
|
+
assert client.compliance is not None
|
|
12
|
+
|
|
13
|
+
def test_subclients_accessible(self):
|
|
14
|
+
client = BarterClient()
|
|
15
|
+
assert hasattr(client, "trust")
|
|
16
|
+
assert hasattr(client, "trade")
|
|
17
|
+
assert hasattr(client, "credit")
|
|
18
|
+
assert hasattr(client, "compliance")
|
|
19
|
+
|
|
20
|
+
def test_version(self):
|
|
21
|
+
import btr
|
|
22
|
+
assert btr.__version__ == "1.0.0"
|
|
23
|
+
|
|
24
|
+
def test_imports(self):
|
|
25
|
+
from btr import BarterClient, TrustProfile, Trade, TradeStatus
|
|
26
|
+
from btr import BarterError, InvalidAddressError
|
|
27
|
+
assert BarterClient is not None
|
|
28
|
+
assert TrustProfile is not None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from btr.trade import TradeClient
|
|
3
|
+
from btr.types import Trade
|
|
4
|
+
from btr.exceptions import SelfTradeError, InsufficientAmountError, InvalidAddressError
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
ALICE = "0x742d35Cc6634C0532925a3b844Bc9e7595f3F9a0"
|
|
8
|
+
BOB = "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def trade_client():
|
|
12
|
+
w3 = MagicMock()
|
|
13
|
+
w3.to_checksum_address = lambda x: x
|
|
14
|
+
w3.eth.accounts = [ALICE]
|
|
15
|
+
contracts = {"BarterCore": None}
|
|
16
|
+
return TradeClient(w3, contracts)
|
|
17
|
+
|
|
18
|
+
class TestPropose:
|
|
19
|
+
def test_self_trade_raises(self, trade_client):
|
|
20
|
+
with pytest.raises(SelfTradeError):
|
|
21
|
+
trade_client.propose(ALICE, "0x" + "ab" * 32, 5000)
|
|
22
|
+
|
|
23
|
+
def test_below_min_raises(self, trade_client):
|
|
24
|
+
with pytest.raises(InsufficientAmountError):
|
|
25
|
+
trade_client.propose(BOB, "0x" + "ab" * 32, 999)
|
|
26
|
+
|
|
27
|
+
def test_invalid_address_raises(self, trade_client):
|
|
28
|
+
with pytest.raises(InvalidAddressError):
|
|
29
|
+
trade_client.propose("bad", "0x" + "ab" * 32, 5000)
|
|
30
|
+
|
|
31
|
+
def test_long_description_raises(self, trade_client):
|
|
32
|
+
with pytest.raises(ValueError):
|
|
33
|
+
trade_client.propose(BOB, "0x" + "ab" * 32, 5000, description="x" * 201)
|
|
34
|
+
|
|
35
|
+
class TestGet:
|
|
36
|
+
def test_not_found_raises(self, trade_client):
|
|
37
|
+
from btr.exceptions import TradeNotFoundError
|
|
38
|
+
with pytest.raises(TradeNotFoundError):
|
|
39
|
+
trade_client.get("0x" + "00" * 32)
|
|
40
|
+
|
|
41
|
+
class TestListByAddress:
|
|
42
|
+
def test_returns_list(self, trade_client):
|
|
43
|
+
result = trade_client.list_by_address(BOB)
|
|
44
|
+
assert isinstance(result, list)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from btr.trust import TrustClient
|
|
3
|
+
from btr.types import TrustProfile
|
|
4
|
+
from btr.exceptions import InvalidAddressError
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
VALID_ADDR = "0x742d35Cc6634C0532925a3b844Bc9e7595f3F9a0"
|
|
8
|
+
ZERO_ADDR = "0x0000000000000000000000000000000000000000"
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def trust_client():
|
|
12
|
+
w3 = MagicMock()
|
|
13
|
+
w3.to_checksum_address = lambda x: x
|
|
14
|
+
contracts = {"BTRTrust": None, "BarterCore": None}
|
|
15
|
+
return TrustClient(w3, contracts)
|
|
16
|
+
|
|
17
|
+
class TestGetScore:
|
|
18
|
+
def test_valid_address_returns_int(self, trust_client):
|
|
19
|
+
score = trust_client.get_score(VALID_ADDR)
|
|
20
|
+
assert isinstance(score, int)
|
|
21
|
+
assert score == 0
|
|
22
|
+
|
|
23
|
+
def test_invalid_address_raises(self, trust_client):
|
|
24
|
+
with pytest.raises(InvalidAddressError):
|
|
25
|
+
trust_client.get_score("invalid")
|
|
26
|
+
|
|
27
|
+
def test_empty_address_raises(self, trust_client):
|
|
28
|
+
with pytest.raises(InvalidAddressError):
|
|
29
|
+
trust_client.get_score("")
|
|
30
|
+
|
|
31
|
+
def test_short_address_raises(self, trust_client):
|
|
32
|
+
with pytest.raises(InvalidAddressError):
|
|
33
|
+
trust_client.get_score("0x123")
|
|
34
|
+
|
|
35
|
+
class TestGetProfile:
|
|
36
|
+
def test_returns_trust_profile(self, trust_client):
|
|
37
|
+
profile = trust_client.get_profile(VALID_ADDR)
|
|
38
|
+
assert isinstance(profile, TrustProfile)
|
|
39
|
+
|
|
40
|
+
def test_all_required_fields(self, trust_client):
|
|
41
|
+
profile = trust_client.get_profile(VALID_ADDR)
|
|
42
|
+
assert hasattr(profile, "address")
|
|
43
|
+
assert hasattr(profile, "score")
|
|
44
|
+
assert hasattr(profile, "unique_counterparties")
|
|
45
|
+
assert hasattr(profile, "trade_count")
|
|
46
|
+
assert hasattr(profile, "completion_rate")
|
|
47
|
+
|
|
48
|
+
def test_invalid_address_raises(self, trust_client):
|
|
49
|
+
with pytest.raises(InvalidAddressError):
|
|
50
|
+
trust_client.get_profile("bad")
|
|
51
|
+
|
|
52
|
+
class TestGetHistory:
|
|
53
|
+
def test_returns_list(self, trust_client):
|
|
54
|
+
result = trust_client.get_history(VALID_ADDR)
|
|
55
|
+
assert isinstance(result, list)
|
|
56
|
+
|
|
57
|
+
def test_with_date_filters(self, trust_client):
|
|
58
|
+
from datetime import datetime
|
|
59
|
+
result = trust_client.get_history(
|
|
60
|
+
VALID_ADDR,
|
|
61
|
+
from_date=datetime(2024, 1, 1),
|
|
62
|
+
to_date=datetime(2024, 12, 31)
|
|
63
|
+
)
|
|
64
|
+
assert isinstance(result, list)
|