Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
127
scripts/workers/crypto_offramp/shkeeper_client.py
Normal file
127
scripts/workers/crypto_offramp/shkeeper_client.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""SHKeeper client — thin wrapper over the self-hosted SHKeeper REST API.
|
||||
|
||||
SHKeeper (https://github.com/vsys-host/shkeeper.io) is our crypto payment
|
||||
gateway. It exposes one REST endpoint per coin at
|
||||
{SHKEEPER_URL}/api/v1/{coin}/...
|
||||
with bearer auth via X-Shkeeper-Api-Key header.
|
||||
|
||||
We use it for two things post-webhook:
|
||||
1. payout() — withdraw coins from the SHKeeper hot wallet to an
|
||||
external address (either a Coinbase Prime deposit
|
||||
address for offramp, or the cold wallet for sweeps).
|
||||
2. balance() — read the current hot-wallet balance for sweep sizing.
|
||||
|
||||
SHKeeper exposes a /payout endpoint; exact shape varies by coin but
|
||||
follows the pattern documented in the project's API README. Confirm
|
||||
shape against the running instance during first real test — we stub
|
||||
with sensible defaults and will iterate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SHKEEPER_URL = os.environ.get("SHKEEPER_URL", "http://127.0.0.1:5000")
|
||||
SHKEEPER_API_KEY = os.environ.get("SHKEEPER_API_KEY", "")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PayoutResult:
|
||||
withdraw_id: str
|
||||
tx_hash: Optional[str]
|
||||
fee_coin: Decimal = Decimal("0")
|
||||
raw: dict = None # type: ignore[assignment]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BalanceResult:
|
||||
balance_coin: Decimal
|
||||
balance_usd: Decimal # SHKeeper returns fiat equivalent on balance
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
|
||||
class SHKeeperError(Exception):
|
||||
"""Any SHKeeper API failure."""
|
||||
|
||||
|
||||
class SHKeeperClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
):
|
||||
self.base = base_url or SHKEEPER_URL
|
||||
self.api_key = api_key or SHKEEPER_API_KEY
|
||||
if not self.api_key:
|
||||
logger.warning(
|
||||
"SHKeeperClient: SHKEEPER_API_KEY empty — calls will fail",
|
||||
)
|
||||
|
||||
async def payout(
|
||||
self,
|
||||
*, coin: str, destination: str, amount: Decimal,
|
||||
) -> PayoutResult:
|
||||
"""POST /api/v1/{coin}/payout — withdraw to an external address.
|
||||
|
||||
Request body (per SHKeeper docs):
|
||||
{"destination": "<addr>", "amount": "<decimal>"}
|
||||
Response includes a withdraw_id + eventually a tx_hash.
|
||||
"""
|
||||
path = f"/api/v1/{coin.lower()}/payout"
|
||||
body = {
|
||||
"destination": destination,
|
||||
"amount": str(amount),
|
||||
}
|
||||
resp = await self._request("POST", path, json_body=body)
|
||||
return PayoutResult(
|
||||
withdraw_id=str(resp.get("withdraw_id") or resp.get("id") or ""),
|
||||
tx_hash=resp.get("tx_hash") or resp.get("txid"),
|
||||
fee_coin=Decimal(str(resp.get("fee") or "0")),
|
||||
raw=resp,
|
||||
)
|
||||
|
||||
async def balance(self, coin: str) -> BalanceResult:
|
||||
"""GET /api/v1/{coin}/balance — current hot-wallet balance."""
|
||||
path = f"/api/v1/{coin.lower()}/balance"
|
||||
resp = await self._request("GET", path)
|
||||
return BalanceResult(
|
||||
balance_coin=Decimal(str(resp.get("balance") or "0")),
|
||||
balance_usd=Decimal(str(resp.get("balance_usd") or resp.get("fiat_balance") or "0")),
|
||||
updated_at=resp.get("updated_at"),
|
||||
)
|
||||
|
||||
async def withdrawal_status(self, coin: str, withdraw_id: str) -> dict:
|
||||
"""GET /api/v1/{coin}/payout/{id} — poll for confirmation."""
|
||||
path = f"/api/v1/{coin.lower()}/payout/{withdraw_id}"
|
||||
return await self._request("GET", path)
|
||||
|
||||
async def _request(
|
||||
self, method: str, path: str,
|
||||
*, json_body: Optional[dict] = None,
|
||||
) -> dict:
|
||||
url = f"{self.base}{path}"
|
||||
headers = {
|
||||
"X-Shkeeper-Api-Key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.request(method, url, headers=headers, json=json_body)
|
||||
except httpx.HTTPError as exc:
|
||||
raise SHKeeperError(f"SHKeeper request failed: {exc}") from exc
|
||||
if resp.status_code >= 400:
|
||||
raise SHKeeperError(
|
||||
f"SHKeeper {method} {path} → {resp.status_code}: {resp.text[:300]}",
|
||||
)
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError:
|
||||
return {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue