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.
@@ -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,8 @@
1
+ web3>=6.0.0
2
+ pydantic>=2.0.0
3
+ python-dotenv>=1.0.0
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ pytest-cov
8
+ pytest-asyncio
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)