walletdna 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,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: walletdna
3
+ Version: 1.0.0
4
+ Summary: Official WalletDNA SDK — wallet screening (allow / review / block), batch screening, and signed webhook verification.
5
+ Author: WalletDNA
6
+ License: MIT
7
+ Project-URL: Homepage, https://walletdna.com/developer
8
+ Project-URL: Documentation, https://walletdna.com/docs/api
9
+ Project-URL: Source, https://github.com/wallet-dna/walletdna
10
+ Keywords: walletdna,wallet,screening,sanctions,ofac,aml,crypto,compliance,webhooks
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+
14
+ # walletdna (Python)
15
+
16
+ Official Python SDK for the [WalletDNA](https://walletdna.com) API — screen any wallet for sanctions and high‑risk exposure, get a clear **allow / review / block** decision, and verify signed webhooks. Standard library only, no dependencies.
17
+
18
+ API access requires an **Advanced** or **Enterprise** plan. Create an API key at <https://walletdna.com/developer>.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pip install walletdna
24
+ ```
25
+
26
+ Requires Python 3.8+.
27
+
28
+ ## Screen a wallet
29
+
30
+ ```python
31
+ import os
32
+ from walletdna import WalletDNA
33
+
34
+ wdna = WalletDNA(os.environ["WALLETDNA_API_KEY"])
35
+
36
+ verdict = wdna.screen("0x722122dF12D4e14e13Ac3b6895a86e84145b6967")
37
+ print(verdict["decision"]) # "allow" | "review" | "block"
38
+
39
+ if verdict["decision"] == "block":
40
+ ... # reject the deposit / withdrawal
41
+ ```
42
+
43
+ ### Custom thresholds & auto‑monitoring
44
+
45
+ ```python
46
+ verdict = wdna.screen(
47
+ address,
48
+ thresholds={"review": 40, "block": 75}, # map riskScore -> decision (sanctioned always blocks)
49
+ monitor=True, # also watch this wallet and webhook me on changes
50
+ )
51
+ ```
52
+
53
+ ### Batch (up to 25 addresses)
54
+
55
+ ```python
56
+ resp = wdna.screen_batch([addr_a, addr_b, addr_c])
57
+ blocked = [r for r in resp["results"] if r.get("decision") == "block"]
58
+ ```
59
+
60
+ ## Webhooks
61
+
62
+ Register an endpoint (the signing secret is returned **once** — store it):
63
+
64
+ ```python
65
+ hook = wdna.create_webhook(
66
+ "https://api.acme.com/walletdna",
67
+ events=["wallet.risk_changed", "wallet.sanctioned"],
68
+ )
69
+ secret = hook["secret"]
70
+ ```
71
+
72
+ Verify incoming events with the raw request body and the `WalletDNA-Signature` header (Flask example):
73
+
74
+ ```python
75
+ from flask import Flask, request, abort
76
+ from walletdna import verify_webhook
77
+
78
+ app = Flask(__name__)
79
+
80
+ @app.post("/walletdna")
81
+ def walletdna_webhook():
82
+ raw = request.get_data(as_text=True)
83
+ if not verify_webhook(raw, request.headers.get("WalletDNA-Signature", ""), WDNA_WEBHOOK_SECRET):
84
+ abort(400)
85
+ event = request.get_json()
86
+ # event["type"], event["data"] (a screening verdict + previousScore/label/reportUrl)
87
+ return "", 200
88
+ ```
89
+
90
+ ## Errors
91
+
92
+ Failed requests raise `WalletDNAError` with `.status`, `.code`, and `.body`. A `429` means you've hit your monthly API limit — add credits in the dashboard.
93
+
94
+ ## Reference
95
+
96
+ Full OpenAPI spec: <https://walletdna.com/api/openapi> · Docs: <https://walletdna.com/docs/api>
@@ -0,0 +1,83 @@
1
+ # walletdna (Python)
2
+
3
+ Official Python SDK for the [WalletDNA](https://walletdna.com) API — screen any wallet for sanctions and high‑risk exposure, get a clear **allow / review / block** decision, and verify signed webhooks. Standard library only, no dependencies.
4
+
5
+ API access requires an **Advanced** or **Enterprise** plan. Create an API key at <https://walletdna.com/developer>.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install walletdna
11
+ ```
12
+
13
+ Requires Python 3.8+.
14
+
15
+ ## Screen a wallet
16
+
17
+ ```python
18
+ import os
19
+ from walletdna import WalletDNA
20
+
21
+ wdna = WalletDNA(os.environ["WALLETDNA_API_KEY"])
22
+
23
+ verdict = wdna.screen("0x722122dF12D4e14e13Ac3b6895a86e84145b6967")
24
+ print(verdict["decision"]) # "allow" | "review" | "block"
25
+
26
+ if verdict["decision"] == "block":
27
+ ... # reject the deposit / withdrawal
28
+ ```
29
+
30
+ ### Custom thresholds & auto‑monitoring
31
+
32
+ ```python
33
+ verdict = wdna.screen(
34
+ address,
35
+ thresholds={"review": 40, "block": 75}, # map riskScore -> decision (sanctioned always blocks)
36
+ monitor=True, # also watch this wallet and webhook me on changes
37
+ )
38
+ ```
39
+
40
+ ### Batch (up to 25 addresses)
41
+
42
+ ```python
43
+ resp = wdna.screen_batch([addr_a, addr_b, addr_c])
44
+ blocked = [r for r in resp["results"] if r.get("decision") == "block"]
45
+ ```
46
+
47
+ ## Webhooks
48
+
49
+ Register an endpoint (the signing secret is returned **once** — store it):
50
+
51
+ ```python
52
+ hook = wdna.create_webhook(
53
+ "https://api.acme.com/walletdna",
54
+ events=["wallet.risk_changed", "wallet.sanctioned"],
55
+ )
56
+ secret = hook["secret"]
57
+ ```
58
+
59
+ Verify incoming events with the raw request body and the `WalletDNA-Signature` header (Flask example):
60
+
61
+ ```python
62
+ from flask import Flask, request, abort
63
+ from walletdna import verify_webhook
64
+
65
+ app = Flask(__name__)
66
+
67
+ @app.post("/walletdna")
68
+ def walletdna_webhook():
69
+ raw = request.get_data(as_text=True)
70
+ if not verify_webhook(raw, request.headers.get("WalletDNA-Signature", ""), WDNA_WEBHOOK_SECRET):
71
+ abort(400)
72
+ event = request.get_json()
73
+ # event["type"], event["data"] (a screening verdict + previousScore/label/reportUrl)
74
+ return "", 200
75
+ ```
76
+
77
+ ## Errors
78
+
79
+ Failed requests raise `WalletDNAError` with `.status`, `.code`, and `.body`. A `429` means you've hit your monthly API limit — add credits in the dashboard.
80
+
81
+ ## Reference
82
+
83
+ Full OpenAPI spec: <https://walletdna.com/api/openapi> · Docs: <https://walletdna.com/docs/api>
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "walletdna"
7
+ version = "1.0.0"
8
+ description = "Official WalletDNA SDK — wallet screening (allow / review / block), batch screening, and signed webhook verification."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["walletdna", "wallet", "screening", "sanctions", "ofac", "aml", "crypto", "compliance", "webhooks"]
13
+ authors = [{ name = "WalletDNA" }]
14
+ dependencies = []
15
+
16
+ [project.urls]
17
+ Homepage = "https://walletdna.com/developer"
18
+ Documentation = "https://walletdna.com/docs/api"
19
+ Source = "https://github.com/wallet-dna/walletdna"
20
+
21
+ [tool.setuptools]
22
+ packages = ["walletdna"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """Official WalletDNA Python SDK.
2
+
3
+ Wallet screening (allow / review / block), batch screening, and signed
4
+ webhook verification. API access requires an Advanced or Enterprise plan —
5
+ create a key at https://walletdna.com/developer.
6
+ """
7
+
8
+ from .client import WalletDNA, WalletDNAError, verify_webhook
9
+
10
+ __all__ = ["WalletDNA", "WalletDNAError", "verify_webhook"]
11
+ __version__ = "1.0.0"
@@ -0,0 +1,141 @@
1
+ """WalletDNA API client — dependency-free (standard library only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import time
9
+ import urllib.error
10
+ import urllib.parse
11
+ import urllib.request
12
+ from typing import Any, Dict, List, Optional, Sequence
13
+
14
+ DEFAULT_BASE_URL = "https://walletdna.com"
15
+
16
+
17
+ class WalletDNAError(Exception):
18
+ """Raised when the API returns a non-2xx response or the request fails."""
19
+
20
+ def __init__(self, message: str, status: Optional[int] = None,
21
+ code: Optional[str] = None, body: Any = None):
22
+ super().__init__(message)
23
+ self.status = status
24
+ self.code = code
25
+ self.body = body
26
+
27
+
28
+ class WalletDNA:
29
+ """Thin client for the WalletDNA API.
30
+
31
+ Example:
32
+ wdna = WalletDNA(os.environ["WALLETDNA_API_KEY"])
33
+ verdict = wdna.screen("0x722122dF12D4e14e13Ac3b6895a86e84145b6967")
34
+ if verdict["decision"] == "block":
35
+ ...
36
+ """
37
+
38
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL,
39
+ timeout: float = 60.0):
40
+ if not api_key or not isinstance(api_key, str):
41
+ raise WalletDNAError(
42
+ "An API key is required. Get one at https://walletdna.com/developer"
43
+ )
44
+ self.api_key = api_key
45
+ self.base_url = base_url.rstrip("/")
46
+ self.timeout = timeout
47
+
48
+ def _request(self, method: str, path: str,
49
+ body: Optional[Dict[str, Any]] = None) -> Any:
50
+ data = None if body is None else json.dumps(body).encode("utf-8")
51
+ req = urllib.request.Request(
52
+ f"{self.base_url}{path}",
53
+ data=data,
54
+ method=method,
55
+ headers={
56
+ "Authorization": f"Bearer {self.api_key}",
57
+ "Content-Type": "application/json",
58
+ },
59
+ )
60
+ try:
61
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
62
+ raw = resp.read().decode("utf-8")
63
+ return json.loads(raw) if raw else None
64
+ except urllib.error.HTTPError as exc:
65
+ raw = exc.read().decode("utf-8", "replace")
66
+ parsed: Any = None
67
+ try:
68
+ parsed = json.loads(raw) if raw else None
69
+ except json.JSONDecodeError:
70
+ parsed = raw
71
+ msg = (parsed.get("message") or parsed.get("error")) if isinstance(parsed, dict) else f"HTTP {exc.code}"
72
+ raise WalletDNAError(
73
+ msg or f"HTTP {exc.code}",
74
+ status=exc.code,
75
+ code=parsed.get("error") if isinstance(parsed, dict) else None,
76
+ body=parsed,
77
+ ) from None
78
+ except urllib.error.URLError as exc:
79
+ raise WalletDNAError(f"Request failed: {exc.reason}") from None
80
+
81
+ def screen(self, address: str, thresholds: Optional[Dict[str, int]] = None,
82
+ monitor: bool = False) -> Dict[str, Any]:
83
+ """Screen a single address. Returns the verdict dict."""
84
+ out = self._request("POST", "/api/v1/screen", {
85
+ "address": address,
86
+ "thresholds": thresholds,
87
+ "monitor": monitor,
88
+ })
89
+ return out["result"]
90
+
91
+ def screen_batch(self, addresses: Sequence[str],
92
+ thresholds: Optional[Dict[str, int]] = None,
93
+ monitor: bool = False) -> Dict[str, Any]:
94
+ """Screen up to 25 addresses. Returns the full response dict."""
95
+ return self._request("POST", "/api/v1/screen", {
96
+ "addresses": list(addresses),
97
+ "thresholds": thresholds,
98
+ "monitor": monitor,
99
+ })
100
+
101
+ def list_webhooks(self) -> Dict[str, Any]:
102
+ return self._request("GET", "/api/webhooks")
103
+
104
+ def create_webhook(self, url: str,
105
+ events: Optional[List[str]] = None) -> Dict[str, Any]:
106
+ """Register a webhook endpoint. The signing secret is returned once."""
107
+ return self._request("POST", "/api/webhooks", {"url": url, "events": events})
108
+
109
+ def delete_webhook(self, webhook_id: str) -> Dict[str, Any]:
110
+ return self._request("DELETE", f"/api/webhooks/{urllib.parse.quote(webhook_id)}")
111
+
112
+
113
+ def verify_webhook(raw_body: str, signature_header: str, secret: str,
114
+ tolerance_sec: int = 300, now: Optional[int] = None) -> bool:
115
+ """Verify a WalletDNA-Signature header (format ``t=<unix>,v1=<hex>``).
116
+
117
+ Returns True only if the HMAC-SHA256 over ``f"{t}.{raw_body}"`` matches and
118
+ the timestamp is within ``tolerance_sec``. Pass the exact request body string.
119
+ """
120
+ if not signature_header or not secret:
121
+ return False
122
+ parts = {}
123
+ for piece in signature_header.split(","):
124
+ if "=" in piece:
125
+ key, _, val = piece.partition("=")
126
+ parts[key.strip()] = val.strip()
127
+ try:
128
+ ts = int(parts.get("t", ""))
129
+ except ValueError:
130
+ return False
131
+ sig = parts.get("v1")
132
+ if not sig:
133
+ return False
134
+
135
+ current = int(time.time()) if now is None else now
136
+ if abs(current - ts) > tolerance_sec:
137
+ return False
138
+
139
+ expected = hmac.new(secret.encode("utf-8"), f"{ts}.{raw_body}".encode("utf-8"),
140
+ hashlib.sha256).hexdigest()
141
+ return hmac.compare_digest(expected, sig)
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: walletdna
3
+ Version: 1.0.0
4
+ Summary: Official WalletDNA SDK — wallet screening (allow / review / block), batch screening, and signed webhook verification.
5
+ Author: WalletDNA
6
+ License: MIT
7
+ Project-URL: Homepage, https://walletdna.com/developer
8
+ Project-URL: Documentation, https://walletdna.com/docs/api
9
+ Project-URL: Source, https://github.com/wallet-dna/walletdna
10
+ Keywords: walletdna,wallet,screening,sanctions,ofac,aml,crypto,compliance,webhooks
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+
14
+ # walletdna (Python)
15
+
16
+ Official Python SDK for the [WalletDNA](https://walletdna.com) API — screen any wallet for sanctions and high‑risk exposure, get a clear **allow / review / block** decision, and verify signed webhooks. Standard library only, no dependencies.
17
+
18
+ API access requires an **Advanced** or **Enterprise** plan. Create an API key at <https://walletdna.com/developer>.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pip install walletdna
24
+ ```
25
+
26
+ Requires Python 3.8+.
27
+
28
+ ## Screen a wallet
29
+
30
+ ```python
31
+ import os
32
+ from walletdna import WalletDNA
33
+
34
+ wdna = WalletDNA(os.environ["WALLETDNA_API_KEY"])
35
+
36
+ verdict = wdna.screen("0x722122dF12D4e14e13Ac3b6895a86e84145b6967")
37
+ print(verdict["decision"]) # "allow" | "review" | "block"
38
+
39
+ if verdict["decision"] == "block":
40
+ ... # reject the deposit / withdrawal
41
+ ```
42
+
43
+ ### Custom thresholds & auto‑monitoring
44
+
45
+ ```python
46
+ verdict = wdna.screen(
47
+ address,
48
+ thresholds={"review": 40, "block": 75}, # map riskScore -> decision (sanctioned always blocks)
49
+ monitor=True, # also watch this wallet and webhook me on changes
50
+ )
51
+ ```
52
+
53
+ ### Batch (up to 25 addresses)
54
+
55
+ ```python
56
+ resp = wdna.screen_batch([addr_a, addr_b, addr_c])
57
+ blocked = [r for r in resp["results"] if r.get("decision") == "block"]
58
+ ```
59
+
60
+ ## Webhooks
61
+
62
+ Register an endpoint (the signing secret is returned **once** — store it):
63
+
64
+ ```python
65
+ hook = wdna.create_webhook(
66
+ "https://api.acme.com/walletdna",
67
+ events=["wallet.risk_changed", "wallet.sanctioned"],
68
+ )
69
+ secret = hook["secret"]
70
+ ```
71
+
72
+ Verify incoming events with the raw request body and the `WalletDNA-Signature` header (Flask example):
73
+
74
+ ```python
75
+ from flask import Flask, request, abort
76
+ from walletdna import verify_webhook
77
+
78
+ app = Flask(__name__)
79
+
80
+ @app.post("/walletdna")
81
+ def walletdna_webhook():
82
+ raw = request.get_data(as_text=True)
83
+ if not verify_webhook(raw, request.headers.get("WalletDNA-Signature", ""), WDNA_WEBHOOK_SECRET):
84
+ abort(400)
85
+ event = request.get_json()
86
+ # event["type"], event["data"] (a screening verdict + previousScore/label/reportUrl)
87
+ return "", 200
88
+ ```
89
+
90
+ ## Errors
91
+
92
+ Failed requests raise `WalletDNAError` with `.status`, `.code`, and `.body`. A `429` means you've hit your monthly API limit — add credits in the dashboard.
93
+
94
+ ## Reference
95
+
96
+ Full OpenAPI spec: <https://walletdna.com/api/openapi> · Docs: <https://walletdna.com/docs/api>
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ walletdna/__init__.py
4
+ walletdna/client.py
5
+ walletdna.egg-info/PKG-INFO
6
+ walletdna.egg-info/SOURCES.txt
7
+ walletdna.egg-info/dependency_links.txt
8
+ walletdna.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ walletdna