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.

Update functionality

+123
+10
web/app.py
··· 44 44 ) 45 45 46 46 47 + @app.get("/api/config") 48 + async def get_config(): 49 + """Return contract address and chain ID for use by the web UI.""" 50 + w3, _ = _get_contract() 51 + return { 52 + "contractAddress": os.getenv("CONTRACT_ADDRESS"), 53 + "chainId": w3.eth.chain_id, 54 + } 55 + 56 + 47 57 def _validate_cow_did(did: str): 48 58 """Parse and validate a did:cow DID, raising HTTPException on failure.""" 49 59 try:
+113
web/static/index.html
··· 179 179 color: #6b7280; 180 180 margin-top: 0.5rem; 181 181 } 182 + 183 + .radio-group { display: flex; gap: 1.5rem; padding: 0.25rem 0; } 184 + .radio-label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.95rem; cursor: pointer; } 182 185 </style> 183 186 </head> 184 187 <body> ··· 236 239 <div id="create-error" class="result error" style="display:none"></div> 237 240 </div> 238 241 242 + <!-- Manage (visible when connected) --> 243 + <div class="card" id="manage-card" style="display:none"> 244 + <h2>Manage</h2> 245 + <p class="hint" style="margin-bottom:0.75rem">Update a did:cow you control. The connected wallet must be the current controller.</p> 246 + <div class="field"> 247 + <label>DID</label> 248 + <input type="text" id="manage-did" placeholder="did:cow:8BC101ABF5BcF8b6209FaaAD4D761C1ED14999Be:plc:pyzlzqt6b2nyrha7smfry6rv"> 249 + </div> 250 + <div class="field"> 251 + <label>Update</label> 252 + <div class="radio-group"> 253 + <label class="radio-label"><input type="radio" name="update-type" value="controller" checked> Controller</label> 254 + <label class="radio-label"><input type="radio" name="update-type" value="wrapped"> Wrapped DID</label> 255 + </div> 256 + </div> 257 + <div class="field" id="new-controller-field"> 258 + <label>New controller address</label> 259 + <input type="text" id="new-controller" placeholder="8BC101ABF5BcF8b6209FaaAD4D761C1ED14999Be"> 260 + </div> 261 + <div class="field" id="new-wrapped-field" style="display:none"> 262 + <label>New wrapped DID</label> 263 + <input type="text" id="new-wrapped" placeholder="did:plc:pyzlzqt6b2nyrha7smfry6rv or plc:pyzlzqt6b2nyrha7smfry6rv"> 264 + </div> 265 + <button class="action" onclick="sendUpdate()">Send transaction</button> 266 + <div id="manage-status" class="result" style="display:none"></div> 267 + </div> 268 + 239 269 <script type="module"> 240 270 const API_BASE = "https://cow.watch"; 271 + 272 + const CONTRACT_ABI = [ 273 + "function updateController(address _controller, string _wrappedDID, address _newController)", 274 + "function updateWrappedDID(address _controller, string _wrappedDID, string _newWrappedDID)", 275 + ]; 276 + 277 + let contractConfig = null; 278 + 279 + async function loadConfig() { 280 + if (contractConfig) return contractConfig; 281 + const resp = await fetch(`${API_BASE}/api/config`); 282 + contractConfig = await resp.json(); 283 + return contractConfig; 284 + } 241 285 242 286 // ── Wallet ────────────────────────────────────────────────────────────────── 243 287 ··· 261 305 controllerInput.value = toChecksumAddress(connectedAddress).slice(2); 262 306 } 263 307 document.getElementById("controller-hint").style.display = "inline"; 308 + document.getElementById("manage-card").style.display = "block"; 264 309 } catch (e) { 265 310 console.error(e); 266 311 } ··· 388 433 389 434 requestAnimationFrame(frame); 390 435 } 436 + 437 + // ── Manage ────────────────────────────────────────────────────────────────── 438 + 439 + document.querySelectorAll('input[name="update-type"]').forEach(radio => { 440 + radio.addEventListener("change", () => { 441 + document.getElementById("new-controller-field").style.display = radio.value === "controller" ? "" : "none"; 442 + document.getElementById("new-wrapped-field").style.display = radio.value === "wrapped" ? "" : "none"; 443 + }); 444 + }); 445 + 446 + function parseCowDid(did) { 447 + if (!did.startsWith("did:cow:")) throw new Error("Not a did:cow DID"); 448 + const rest = did.slice("did:cow:".length); 449 + const colonIdx = rest.indexOf(":"); 450 + if (colonIdx === -1) throw new Error("Invalid did:cow format"); 451 + return { 452 + controller: "0x" + rest.slice(0, colonIdx), 453 + wrapped: rest.slice(colonIdx + 1), 454 + }; 455 + } 456 + 457 + window.sendUpdate = async () => { 458 + const statusEl = document.getElementById("manage-status"); 459 + statusEl.style.display = "block"; 460 + statusEl.className = "result"; 461 + statusEl.textContent = "Preparing transaction…"; 462 + 463 + try { 464 + const did = document.getElementById("manage-did").value.trim(); 465 + const { controller, wrapped } = parseCowDid(did); 466 + const updateType = document.querySelector('input[name="update-type"]:checked').value; 467 + 468 + const config = await loadConfig(); 469 + const provider = new ethers.BrowserProvider(window.ethereum); 470 + 471 + const network = await provider.getNetwork(); 472 + if (Number(network.chainId) !== config.chainId) { 473 + statusEl.textContent = "Switching network…"; 474 + await window.ethereum.request({ 475 + method: "wallet_switchEthereumChain", 476 + params: [{ chainId: "0x" + config.chainId.toString(16) }], 477 + }); 478 + } 479 + 480 + const signer = await provider.getSigner(); 481 + const contract = new ethers.Contract(config.contractAddress, CONTRACT_ABI, signer); 482 + 483 + let tx; 484 + if (updateType === "controller") { 485 + const raw = document.getElementById("new-controller").value.trim().replace(/^0x/i, ""); 486 + if (!/^[0-9a-fA-F]{40}$/.test(raw)) throw new Error("Invalid controller address"); 487 + tx = await contract.updateController(controller, wrapped, "0x" + raw); 488 + } else { 489 + let newWrapped = document.getElementById("new-wrapped").value.trim(); 490 + if (newWrapped.startsWith("did:")) newWrapped = newWrapped.slice(4); 491 + if (!newWrapped.includes(":")) throw new Error("Invalid wrapped DID"); 492 + tx = await contract.updateWrappedDID(controller, wrapped, newWrapped); 493 + } 494 + 495 + statusEl.textContent = `Transaction sent: ${tx.hash}\nWaiting for confirmation…`; 496 + const receipt = await tx.wait(); 497 + statusEl.className = "result success"; 498 + statusEl.textContent = `Confirmed in block ${receipt.blockNumber}\nTx: ${tx.hash}`; 499 + } catch (e) { 500 + statusEl.className = "result error"; 501 + statusEl.textContent = e.reason ?? e.message ?? String(e); 502 + } 503 + }; 391 504 392 505 window.copyDid = () => { 393 506 const val = document.getElementById("create-did-value").textContent;