did:cow, a proposal for an ID resolution method with most of the convenience of did:plc/did:web and the robustness of a public blockchain
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

API endpoint for resolving did:cow plus a simple web page (no edit feature yet)

+548
+2
.gitignore
··· 1 1 cache/ 2 2 out/ 3 + __pycache__/ 4 + *.pyc
+101
web/app.py
··· 1 + #!/usr/bin/env python3 2 + """did:cow resolution API""" 3 + 4 + import json 5 + import os 6 + import sys 7 + from pathlib import Path 8 + 9 + from dotenv import load_dotenv 10 + from fastapi import FastAPI, HTTPException 11 + from fastapi.middleware.cors import CORSMiddleware 12 + from fastapi.responses import JSONResponse 13 + from web3 import Web3 14 + 15 + # Load env and import helpers from CLI 16 + load_dotenv(Path(__file__).parent.parent / "cli" / ".env") 17 + sys.path.insert(0, str(Path(__file__).parent.parent / "cli")) 18 + from cow import _controller_address, _parse_cow_did, _resolve_did_doc, _strip_did_prefix 19 + 20 + _ABI_PATH = Path(__file__).parent.parent / "out" / "CowRegistry.sol" / "CowRegistry.json" 21 + 22 + 23 + def _get_contract(): 24 + url = os.getenv("RPC_URL") 25 + addr = os.getenv("CONTRACT_ADDRESS") 26 + if not url or not addr: 27 + raise RuntimeError("RPC_URL and CONTRACT_ADDRESS must be set") 28 + w3 = Web3(Web3.HTTPProvider(url)) 29 + artifact = json.loads(_ABI_PATH.read_text()) 30 + contract = w3.eth.contract( 31 + address=Web3.to_checksum_address(addr), 32 + abi=artifact["abi"], 33 + ) 34 + return w3, contract 35 + 36 + 37 + app = FastAPI(title="did:cow resolver", docs_url="/api/docs", redoc_url=None) 38 + 39 + app.add_middleware( 40 + CORSMiddleware, 41 + allow_origins=["*"], 42 + allow_methods=["GET"], 43 + allow_headers=["*"], 44 + ) 45 + 46 + 47 + def _validate_cow_did(did: str): 48 + """Parse and validate a did:cow DID, raising HTTPException on failure.""" 49 + try: 50 + return _parse_cow_did(did) 51 + except Exception as e: 52 + raise HTTPException(status_code=400, detail=str(e)) 53 + 54 + 55 + @app.get("/{did:path}/describe") 56 + async def describe(did: str): 57 + """Return on-chain state for a did:cow DID without fetching the wrapped DID document.""" 58 + controller_hex, initial_wrapped = _validate_cow_did(did) 59 + w3, contract = _get_contract() 60 + controller_addr = _controller_address(controller_hex) 61 + 62 + wrapped_did, controller = contract.functions.resolve(controller_addr, initial_wrapped).call() 63 + 64 + if wrapped_did == "": 65 + return {"status": "deactivated"} 66 + 67 + cow_hash = contract.functions.calculateHash(controller_addr, initial_wrapped).call() 68 + registered = contract.functions.cows(cow_hash).call()[1] # initialized bool 69 + 70 + return { 71 + "status": "active" if registered else "not registered on-chain", 72 + "wrappedDid": wrapped_did, 73 + "controller": controller, 74 + } 75 + 76 + 77 + @app.get("/{did:path}") 78 + async def resolve_did(did: str): 79 + """Resolve a did:cow DID and return the modified DID document.""" 80 + controller_hex, initial_wrapped = _validate_cow_did(did) 81 + w3, contract = _get_contract() 82 + controller_addr = _controller_address(controller_hex) 83 + 84 + wrapped_did, controller = contract.functions.resolve(controller_addr, initial_wrapped).call() 85 + 86 + if wrapped_did == "": 87 + raise HTTPException(status_code=404, detail="DID is deactivated") 88 + 89 + try: 90 + _, doc = _resolve_did_doc(_strip_did_prefix(wrapped_did)) 91 + except Exception as e: 92 + raise HTTPException(status_code=502, detail=str(e)) 93 + 94 + chain_id = w3.eth.chain_id 95 + doc["id"] = did 96 + doc["did:cow"] = { 97 + "controller": f"did:pkh:eip155:{chain_id}:{controller}", 98 + "wrappedDid": wrapped_did, 99 + } 100 + 101 + return JSONResponse(content=doc)
+5
web/requirements.txt
··· 1 + fastapi>=0.115.0 2 + uvicorn>=0.32.0 3 + web3>=7.0.0 4 + python-dotenv>=1.0.0 5 + requests>=2.31.0
+417
web/static/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>did:cow</title> 7 + <style> 8 + * { box-sizing: border-box; margin: 0; padding: 0; } 9 + 10 + body { 11 + font-family: system-ui, -apple-system, sans-serif; 12 + background: #f5f5f5; 13 + color: #1a1a1a; 14 + padding: 2rem; 15 + } 16 + 17 + header { 18 + display: flex; 19 + justify-content: space-between; 20 + align-items: center; 21 + margin-bottom: 2rem; 22 + } 23 + 24 + .header-left { display: flex; align-items: center; gap: 1.5rem; } 25 + 26 + .cow-ascii { 27 + font-family: monospace; 28 + font-size: 0.7rem; 29 + line-height: 1.3; 30 + color: #6b7280; 31 + white-space: pre; 32 + user-select: none; 33 + } 34 + 35 + h1 { font-size: 1.5rem; font-weight: 700; } 36 + h1 span { color: #6b7280; font-weight: 400; } 37 + 38 + #connect-btn { 39 + background: #1a1a1a; 40 + color: #fff; 41 + border: none; 42 + padding: 0 1rem; 43 + border-radius: 6px; 44 + cursor: pointer; 45 + font-size: 0.9rem; 46 + height: 2.5rem; 47 + } 48 + #connect-btn:hover { background: #333; } 49 + #connect-btn.connected { background: #166534; } 50 + 51 + .card { 52 + background: #fff; 53 + border: 1px solid #e5e7eb; 54 + border-radius: 8px; 55 + padding: 1.5rem; 56 + margin-bottom: 1.5rem; 57 + } 58 + 59 + .card h2 { 60 + font-size: 1rem; 61 + font-weight: 600; 62 + margin-bottom: 1rem; 63 + color: #374151; 64 + } 65 + 66 + .field { margin-bottom: 0.75rem; } 67 + 68 + label { 69 + display: block; 70 + font-size: 0.8rem; 71 + font-weight: 500; 72 + color: #6b7280; 73 + margin-bottom: 0.25rem; 74 + text-transform: uppercase; 75 + letter-spacing: 0.05em; 76 + } 77 + 78 + input[type="text"] { 79 + width: 100%; 80 + padding: 0.5rem 0.75rem; 81 + border: 1px solid #d1d5db; 82 + border-radius: 6px; 83 + font-size: 0.95rem; 84 + font-family: monospace; 85 + height: 2.5rem; 86 + } 87 + input[type="text"]:focus { 88 + outline: none; 89 + border-color: #6b7280; 90 + } 91 + 92 + .row { display: flex; gap: 0.75rem; align-items: flex-end; } 93 + .row .field { margin-bottom: 0; } 94 + .row .field { flex: 1; } 95 + 96 + button.action { 97 + background: #1a1a1a; 98 + color: #fff; 99 + border: none; 100 + padding: 0 1.25rem; 101 + border-radius: 6px; 102 + cursor: pointer; 103 + font-size: 0.95rem; 104 + height: 2.5rem; 105 + white-space: nowrap; 106 + } 107 + button.action:hover { background: #333; } 108 + button.action:disabled { background: #9ca3af; cursor: default; } 109 + 110 + .result { 111 + margin-top: 1rem; 112 + padding: 1rem; 113 + background: #f9fafb; 114 + border: 1px solid #e5e7eb; 115 + border-radius: 6px; 116 + font-family: monospace; 117 + font-size: 0.85rem; 118 + white-space: pre-wrap; 119 + word-break: break-all; 120 + } 121 + 122 + .result.error { background: #fef2f2; border-color: #fecaca; color: #dc2626; } 123 + .result.success { background: #f0fdf4; border-color: #bbf7d0; } 124 + 125 + .did-output { 126 + margin-top: 1rem; 127 + padding: 0.75rem 1rem; 128 + background: #f0fdf4; 129 + border: 1px solid #bbf7d0; 130 + border-radius: 6px; 131 + display: none; 132 + } 133 + 134 + .reveal-track { 135 + position: relative; 136 + display: flex; 137 + align-items: center; 138 + min-height: 5.5rem; 139 + margin-bottom: 0.5rem; 140 + } 141 + 142 + #create-did-value { 143 + font-family: monospace; 144 + font-size: 1.2rem; 145 + word-break: break-all; 146 + width: 100%; 147 + } 148 + 149 + .running-cow { 150 + position: absolute; 151 + top: 0; 152 + display: none; 153 + pointer-events: none; 154 + } 155 + 156 + .running-cow pre { 157 + transform: scaleX(-1); 158 + font-family: monospace; 159 + font-size: 0.85rem; 160 + line-height: 1.3; 161 + white-space: pre; 162 + margin: 0; 163 + } 164 + 165 + .copy-btn { 166 + background: none; 167 + border: 1px solid #6b7280; 168 + border-radius: 4px; 169 + padding: 0.2rem 0.5rem; 170 + font-size: 0.8rem; 171 + cursor: pointer; 172 + white-space: nowrap; 173 + color: #374151; 174 + } 175 + .copy-btn:hover { background: #f3f4f6; } 176 + 177 + .hint { 178 + font-size: 0.8rem; 179 + color: #6b7280; 180 + margin-top: 0.5rem; 181 + } 182 + </style> 183 + </head> 184 + <body> 185 + 186 + <header> 187 + <div class="header-left"> 188 + <pre class="cow-ascii" 189 + > ^__^ 190 + (oo)\_______ 191 + (__)\ )\/\ 192 + ||----w | 193 + || ||</pre> 194 + <h1>did:cow <span>Consensus Ownership Wrapper</span></h1> 195 + </div> 196 + <button id="connect-btn" onclick="connectWallet()">Connect wallet</button> 197 + </header> 198 + 199 + <!-- Resolve --> 200 + <div class="card"> 201 + <h2>Resolve</h2> 202 + <div class="row"> 203 + <div class="field"> 204 + <label>DID</label> 205 + <input type="text" id="resolve-did" placeholder="did:cow:8BC101ABF5BcF8b6209FaaAD4D761C1ED14999Be:plc:pyzlzqt6b2nyrha7smfry6rv"> 206 + </div> 207 + <button class="action" onclick="resolveDid()">Resolve</button> 208 + </div> 209 + <div id="resolve-result" class="result" style="display:none"></div> 210 + </div> 211 + 212 + <!-- Create --> 213 + <div class="card"> 214 + <h2>Create</h2> 215 + <p class="hint" style="margin-bottom:0.75rem">No transaction required — a did:cow identifier is formed from your controller address and wrapped DID.</p> 216 + <div class="field"> 217 + <label>Controller address <span id="controller-hint" style="color:#166534;display:none">(your connected address)</span></label> 218 + <input type="text" id="create-controller" placeholder="8BC101ABF5BcF8b6209FaaAD4D761C1ED14999Be"> 219 + </div> 220 + <div class="field"> 221 + <label>Wrapped DID</label> 222 + <input type="text" id="create-wrapped" placeholder="did:plc:pyzlzqt6b2nyrha7smfry6rv or plc:pyzlzqt6b2nyrha7smfry6rv"> 223 + </div> 224 + <button class="action" onclick="createDid()">Construct DID</button> 225 + <div id="create-output" class="did-output"> 226 + <div class="reveal-track"> 227 + <div class="running-cow" id="running-cow"><pre> ^__^ 228 + (oo)\_______ 229 + (__)\ )\/\ 230 + ||----w | 231 + || ||</pre></div> 232 + <span id="create-did-value"></span> 233 + </div> 234 + <button class="copy-btn" id="copy-btn" onclick="copyDid()" style="display:none">Copy</button> 235 + </div> 236 + <div id="create-error" class="result error" style="display:none"></div> 237 + </div> 238 + 239 + <script type="module"> 240 + const API_BASE = "https://cow.watch"; 241 + 242 + // ── Wallet ────────────────────────────────────────────────────────────────── 243 + 244 + let connectedAddress = null; 245 + 246 + window.connectWallet = async () => { 247 + if (!window.ethereum) { 248 + alert("No wallet detected. Install MetaMask or another browser wallet."); 249 + return; 250 + } 251 + try { 252 + const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); 253 + connectedAddress = accounts[0]; 254 + const btn = document.getElementById("connect-btn"); 255 + btn.textContent = connectedAddress.slice(0, 6) + "…" + connectedAddress.slice(-4); 256 + btn.classList.add("connected"); 257 + 258 + // Pre-fill controller with checksummed address (strip 0x for did:cow format) 259 + const controllerInput = document.getElementById("create-controller"); 260 + if (!controllerInput.value) { 261 + controllerInput.value = toChecksumAddress(connectedAddress).slice(2); 262 + } 263 + document.getElementById("controller-hint").style.display = "inline"; 264 + } catch (e) { 265 + console.error(e); 266 + } 267 + }; 268 + 269 + // EIP-55 checksum — minimal implementation 270 + function toChecksumAddress(address) { 271 + const addr = address.toLowerCase().replace("0x", ""); 272 + // Use the browser's SubtleCrypto to keccak would require a library; 273 + // ethers is loaded below for this — call after ethers loads. 274 + return address; // placeholder; replaced once ethers is available 275 + } 276 + 277 + // ── Resolve ───────────────────────────────────────────────────────────────── 278 + 279 + window.resolveDid = async () => { 280 + const did = document.getElementById("resolve-did").value.trim(); 281 + const el = document.getElementById("resolve-result"); 282 + el.style.display = "block"; 283 + el.className = "result"; 284 + el.textContent = "Resolving…"; 285 + 286 + try { 287 + const resp = await fetch(`${API_BASE}/${encodeURIComponent(did)}`); 288 + const body = await resp.json(); 289 + if (!resp.ok) { 290 + el.className = "result error"; 291 + el.textContent = body.detail ?? JSON.stringify(body, null, 2); 292 + } else { 293 + el.className = "result success"; 294 + el.textContent = JSON.stringify(body, null, 2); 295 + } 296 + } catch (e) { 297 + el.className = "result error"; 298 + el.textContent = `Network error: ${e.message}`; 299 + } 300 + }; 301 + 302 + // ── Create ────────────────────────────────────────────────────────────────── 303 + 304 + window.createDid = () => { 305 + const errorEl = document.getElementById("create-error"); 306 + const outputEl = document.getElementById("create-output"); 307 + errorEl.style.display = "none"; 308 + outputEl.style.display = "none"; 309 + 310 + let controller = document.getElementById("create-controller").value.trim(); 311 + let wrapped = document.getElementById("create-wrapped").value.trim(); 312 + 313 + // Strip leading 0x if present 314 + if (controller.startsWith("0x") || controller.startsWith("0X")) { 315 + controller = controller.slice(2); 316 + } 317 + 318 + // Strip leading did: if present 319 + if (wrapped.startsWith("did:")) { 320 + wrapped = wrapped.slice(4); 321 + } 322 + 323 + if (!controller) { 324 + showCreateError("Controller address is required."); 325 + return; 326 + } 327 + if (!/^[0-9a-fA-F]{40}$/.test(controller)) { 328 + showCreateError("Controller must be a 40-character hex Ethereum address."); 329 + return; 330 + } 331 + if (!wrapped) { 332 + showCreateError("Wrapped DID is required."); 333 + return; 334 + } 335 + if (!wrapped.includes(":")) { 336 + showCreateError("Wrapped DID must include a method, e.g. plc:xxx or web:example.com"); 337 + return; 338 + } 339 + 340 + // EIP-55 checksum via ethers 341 + let checksummed; 342 + try { 343 + checksummed = ethers.getAddress("0x" + controller).slice(2); 344 + } catch (e) { 345 + showCreateError("Invalid Ethereum address."); 346 + return; 347 + } 348 + 349 + const did = `did:cow:${checksummed}:${wrapped}`; 350 + animateCowReveal(did); 351 + }; 352 + 353 + function animateCowReveal(did) { 354 + const outputEl = document.getElementById("create-output"); 355 + const didEl = document.getElementById("create-did-value"); 356 + const cowEl = document.getElementById("running-cow"); 357 + const copyBtn = document.getElementById("copy-btn"); 358 + 359 + didEl.textContent = did; 360 + didEl.style.clipPath = "inset(0 100% 0 0)"; 361 + cowEl.style.display = "block"; 362 + cowEl.style.left = "0px"; 363 + copyBtn.style.display = "none"; 364 + outputEl.style.display = "block"; 365 + 366 + const cowWidth = cowEl.offsetWidth; 367 + const trackWidth = cowEl.parentElement.offsetWidth; 368 + const travel = trackWidth - cowWidth; // tail goes from 0 to right edge 369 + 370 + const duration = 5400; 371 + const start = performance.now(); 372 + 373 + function frame(now) { 374 + const t = Math.min((now - start) / duration, 1); 375 + const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; 376 + const leftPx = eased * travel; 377 + cowEl.style.left = leftPx + "px"; 378 + // reveal tracks the tail (left edge of cow) so text comes out behind it 379 + const revealPct = (leftPx / trackWidth) * 100; 380 + didEl.style.clipPath = `inset(0 ${100 - revealPct}% 0 0)`; 381 + if (t < 1) { 382 + requestAnimationFrame(frame); 383 + } else { 384 + didEl.style.clipPath = ""; // reveal the last bit hidden under the cow 385 + copyBtn.style.display = "inline-block"; 386 + } 387 + } 388 + 389 + requestAnimationFrame(frame); 390 + } 391 + 392 + window.copyDid = () => { 393 + const val = document.getElementById("create-did-value").textContent; 394 + navigator.clipboard.writeText(val).then(() => { 395 + const btn = document.querySelector(".copy-btn"); 396 + btn.textContent = "Copied!"; 397 + setTimeout(() => { btn.textContent = "Copy"; }, 1500); 398 + }); 399 + }; 400 + 401 + function showCreateError(msg) { 402 + const el = document.getElementById("create-error"); 403 + el.textContent = msg; 404 + el.style.display = "block"; 405 + } 406 + 407 + // ── Enter on resolve input ─────────────────────────────────────────────────── 408 + document.getElementById("resolve-did").addEventListener("keydown", e => { 409 + if (e.key === "Enter") window.resolveDid(); 410 + }); 411 + </script> 412 + 413 + <!-- ethers.js for EIP-55 checksum and future MetaMask tx signing --> 414 + <script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.4/ethers.umd.min.js"></script> 415 + 416 + </body> 417 + </html>
+23
web/web.py
··· 1 + #!/usr/bin/env python3 2 + """Static file server for the did:cow web UI (dev use — in prod, copy to nginx webroot).""" 3 + 4 + import http.server 5 + import os 6 + from pathlib import Path 7 + 8 + PORT = 6667 9 + DIRECTORY = Path(__file__).parent / "static" 10 + 11 + 12 + class Handler(http.server.SimpleHTTPRequestHandler): 13 + def __init__(self, *args, **kwargs): 14 + super().__init__(*args, directory=str(DIRECTORY), **kwargs) 15 + 16 + def log_message(self, format, *args): 17 + print(f"{self.address_string()} {format % args}") 18 + 19 + 20 + if __name__ == "__main__": 21 + with http.server.HTTPServer(("", PORT), Handler) as httpd: 22 + print(f"Serving {DIRECTORY} at http://localhost:{PORT}") 23 + httpd.serve_forever()