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.

A bit of extra validation

+53 -21
+53 -21
web/static/index.html
··· 215 215 An ID can be created for free without a blockchain transaction. You only need to send a blockchain transaction if you want to point it at a different did:web or did:plc ID, or change the blockchain address that controls it. 216 216 <br /> 217 217 <br /> 218 + <a style="float:right" href="/#!/did:cow:B6aaa1DAd9D09d689dc6111dcc6EA2A0d641b406:plc:pyzlzqt6b2nyrha7smfry6rv">Example</a> 218 219 <a href="https://tangled.org/goat.navy/did-cow">Specification</a> 219 - | 220 - <a href="/#!/did:cow:B6aaa1DAd9D09d689dc6111dcc6EA2A0d641b406:plc:pyzlzqt6b2nyrha7smfry6rv">Example</a> 221 220 | 222 221 <a href="https://api.cow.watch/did:cow:B6aaa1DAd9D09d689dc6111dcc6EA2A0d641b406:plc:pyzlzqt6b2nyrha7smfry6rv">API</a> 223 222 | ··· 291 290 const CONTRACT_ABI = [ 292 291 "function updateController(address _controller, string _wrappedDID, address _newController)", 293 292 "function updateWrappedDID(address _controller, string _wrappedDID, string _newWrappedDID)", 293 + "error NotController()", 294 + "error NotInitialized()", 295 + "error AlreadyDeactivated()", 296 + "error EmptyWrappedDID()", 294 297 ]; 298 + 299 + const CONTRACT_ERROR_MESSAGES = { 300 + "0x23019e67": "The connected wallet is not the current controller of this DID.", 301 + "0x87138d5c": "This DID has not been registered on-chain yet.", 302 + "0x5deafd90": "This DID has been permanently deactivated.", 303 + "0x8a98f156": "Wrapped DID cannot be empty.", 304 + }; 295 305 296 306 let contractConfig = null; 297 307 ··· 327 337 // Pre-fill controller with checksummed address (strip 0x for did:cow format) 328 338 const controllerInput = document.getElementById("create-controller"); 329 339 if (!controllerInput.value) { 330 - controllerInput.value = toChecksumAddress(connectedAddress).slice(2); 340 + controllerInput.value = ethers.getAddress(connectedAddress).slice(2); 331 341 } 332 342 document.getElementById("controller-hint").style.display = "inline"; 333 343 // Enable edit fields if a DID has already been resolved ··· 339 349 } 340 350 }; 341 351 342 - // EIP-55 checksum — minimal implementation 343 - function toChecksumAddress(address) { 344 - const addr = address.toLowerCase().replace("0x", ""); 345 - // Use the browser's SubtleCrypto to keccak would require a library; 346 - // ethers is loaded below for this — call after ethers loads. 347 - return address; // placeholder; replaced once ethers is available 348 - } 349 - 350 352 // ── Resolve ───────────────────────────────────────────────────────────────── 351 353 352 354 let resolvedDid = null; // the DID last successfully resolved, for contract calls ··· 358 360 document.getElementById("edit-wrapped-btn").disabled = !enabled; 359 361 } 360 362 363 + function extractDid(input) { 364 + // Strip a full cow.watch URL if pasted, e.g. https://cow.watch/#!/did:cow:... 365 + input = input.trim().replace(/^https?:\/\/[^/]+\/#!\//, ""); 366 + // Also handle bare hash e.g. #!/did:cow:... 367 + input = input.replace(/^#!\//, ""); 368 + return input; 369 + } 370 + 361 371 window.resolveDid = async () => { 362 - const did = document.getElementById("resolve-did").value.trim(); 372 + const did = extractDid(document.getElementById("resolve-did").value); 373 + if (!did) { 374 + const statusEl = document.getElementById("resolve-status"); 375 + statusEl.style.display = "block"; 376 + statusEl.className = "result error"; 377 + statusEl.textContent = "Please enter a did:cow DID."; 378 + return; 379 + } 380 + document.getElementById("resolve-did").value = did; // normalise the field 363 381 window.location.hash = "!/" + did; 364 382 const statusEl = document.getElementById("resolve-status"); 365 383 const resultEl = document.getElementById("resolve-result"); ··· 416 434 controller = controller.slice(2); 417 435 } 418 436 419 - // Strip leading did: if present 420 - if (wrapped.startsWith("did:")) { 421 - wrapped = wrapped.slice(4); 422 - } 437 + wrapped = stripDidPrefix(wrapped); 423 438 424 439 if (!controller) { 425 440 showCreateError("Controller address is required."); ··· 490 505 requestAnimationFrame(frame); 491 506 } 492 507 508 + function stripDidPrefix(s) { 509 + // Remove one or more leading "did:" repetitions 510 + while (s.startsWith("did:")) s = s.slice(4); 511 + return s; 512 + } 513 + 493 514 // ── Edit ──────────────────────────────────────────────────────────────────── 494 515 495 516 function parseCowDid(did) { ··· 530 551 let tx; 531 552 if (updateType === "controller") { 532 553 const raw = document.getElementById("edit-controller").value.trim().replace(/^0x/i, ""); 533 - if (!/^[0-9a-fA-F]{40}$/.test(raw)) throw new Error("Invalid controller address"); 554 + if (!raw) throw new Error("Controller address is required."); 555 + if (!/^[0-9a-fA-F]{40}$/.test(raw)) throw new Error("Invalid controller address — expected a 40-character hex Ethereum address."); 534 556 tx = await contract.updateController(controller, wrapped, "0x" + raw); 535 557 } else { 536 - let newWrapped = document.getElementById("edit-wrapped").value.trim(); 537 - if (newWrapped.startsWith("did:")) newWrapped = newWrapped.slice(4); 538 - if (!newWrapped.includes(":")) throw new Error("Invalid wrapped DID"); 558 + let newWrapped = stripDidPrefix(document.getElementById("edit-wrapped").value.trim()); 559 + if (!newWrapped) throw new Error("Wrapped DID is required."); 560 + if (!newWrapped.includes(":")) throw new Error("Wrapped DID must include a method, e.g. plc:xxx or web:example.com"); 539 561 tx = await contract.updateWrappedDID(controller, wrapped, newWrapped); 540 562 } 541 563 ··· 545 567 statusEl.textContent = `Confirmed in block ${receipt.blockNumber}\nTx: ${tx.hash}`; 546 568 } catch (e) { 547 569 statusEl.className = "result error"; 548 - statusEl.textContent = e.reason ?? e.message ?? String(e); 570 + if (e.code === "CALL_EXCEPTION") { 571 + const selector = typeof e.data === "string" ? e.data.slice(0, 10) : null; 572 + statusEl.textContent = CONTRACT_ERROR_MESSAGES[selector] ?? `Contract reverted${selector ? ` (${selector})` : ""}`; 573 + } else if (e.code === "UNKNOWN_ERROR" || e.message?.includes("coalesce")) { 574 + // MetaMask sometimes returns a malformed RPC response after broadcasting 575 + // even though the tx went through. 576 + statusEl.className = "result success"; 577 + statusEl.textContent = "Transaction sent — check your wallet for confirmation status."; 578 + } else { 579 + statusEl.textContent = e.reason ?? e.message ?? String(e); 580 + } 549 581 } 550 582 }; 551 583