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.

cli script

+245
+8
cli/.env.example
··· 1 + # Ethereum RPC endpoint (e.g. Infura, Alchemy, or a local node) 2 + RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY_HERE 3 + 4 + # Private key of the controller account (no 0x prefix) 5 + PRIVATE_KEY=your_private_key_here 6 + 7 + # Deployed CowRegistry contract address 8 + CONTRACT_ADDRESS=0x0000000000000000000000000000000000000000
+234
cli/cow.py
··· 1 + #!/usr/bin/env python3 2 + """did:cow registry CLI""" 3 + 4 + import json 5 + import os 6 + import sys 7 + from pathlib import Path 8 + 9 + import click 10 + from dotenv import load_dotenv 11 + from web3 import Web3 12 + 13 + load_dotenv() 14 + 15 + # ABI is generated by `forge build` in the repo root 16 + _ABI_PATH = Path(__file__).parent.parent / "out" / "CowRegistry.sol" / "CowRegistry.json" 17 + 18 + 19 + # --------------------------------------------------------------------------- 20 + # Helpers 21 + # --------------------------------------------------------------------------- 22 + 23 + def _w3(): 24 + url = os.getenv("RPC_URL") 25 + if not url: 26 + raise click.ClickException("RPC_URL not set in .env") 27 + w3 = Web3(Web3.HTTPProvider(url)) 28 + if not w3.is_connected(): 29 + raise click.ClickException(f"Could not connect to {url}") 30 + return w3 31 + 32 + 33 + def _contract(w3): 34 + addr = os.getenv("CONTRACT_ADDRESS") 35 + if not addr: 36 + raise click.ClickException("CONTRACT_ADDRESS not set in .env") 37 + if not _ABI_PATH.exists(): 38 + raise click.ClickException( 39 + f"ABI not found at {_ABI_PATH} — run `forge build` first" 40 + ) 41 + artifact = json.loads(_ABI_PATH.read_text()) 42 + return w3.eth.contract( 43 + address=Web3.to_checksum_address(addr), 44 + abi=artifact["abi"], 45 + ) 46 + 47 + 48 + def _account(w3): 49 + key = os.getenv("PRIVATE_KEY") 50 + if not key: 51 + raise click.ClickException("PRIVATE_KEY not set in .env") 52 + return w3.eth.account.from_key(key) 53 + 54 + 55 + def _parse_cow_did(did): 56 + """ 57 + Parse did:cow:<controller>:<method>:<id...> into (controller_address, wrapped_did). 58 + 59 + The wrapped DID is returned without the leading 'did:' prefix, matching 60 + what the contract stores — e.g. 'plc:7qqsrnkn4moc2jgdxvh6aa3t' or 61 + 'web:example.com'. 62 + """ 63 + if not did.startswith("did:cow:"): 64 + raise click.ClickException(f"Not a did:cow DID: {did}") 65 + # did:cow:<address>:<method>:<id...> 66 + # splitting 'did:cow:ADDR:METHOD:ID' on ':' gives 67 + # ['did', 'cow', 'ADDR', 'METHOD', 'ID...'] 68 + parts = did.split(":") 69 + if len(parts) < 5: 70 + raise click.ClickException(f"Invalid did:cow format: {did}") 71 + controller_hex = parts[2] 72 + wrapped_did = ":".join(parts[3:]) # e.g. 'web:example.com' 73 + return controller_hex, wrapped_did 74 + 75 + 76 + def _controller_address(controller_hex): 77 + """Convert bare hex controller (no 0x) to checksummed address.""" 78 + hex_str = controller_hex if controller_hex.startswith("0x") else f"0x{controller_hex}" 79 + return Web3.to_checksum_address(hex_str) 80 + 81 + 82 + def _strip_did_prefix(wrapped_did): 83 + """Accept 'did:web:example.com' or 'web:example.com', always return the latter.""" 84 + if wrapped_did.startswith("did:"): 85 + return wrapped_did[4:] 86 + return wrapped_did 87 + 88 + 89 + def _send(w3, account, tx): 90 + tx["from"] = account.address 91 + tx["nonce"] = w3.eth.get_transaction_count(account.address) 92 + 93 + latest = w3.eth.get_block("latest") 94 + base_fee = latest["baseFeePerGas"] 95 + priority_fee = w3.to_wei(1, "gwei") 96 + tx["maxPriorityFeePerGas"] = priority_fee 97 + tx["maxFeePerGas"] = base_fee * 2 + priority_fee 98 + 99 + tx["gas"] = w3.eth.estimate_gas(tx) 100 + 101 + signed = account.sign_transaction(tx) 102 + txhash = w3.eth.send_raw_transaction(signed.raw_transaction) 103 + click.echo(f"tx: {txhash.hex()}") 104 + receipt = w3.eth.wait_for_transaction_receipt(txhash) 105 + if receipt["status"] != 1: 106 + raise click.ClickException("Transaction reverted") 107 + click.echo(f"confirmed: block {receipt['blockNumber']}") 108 + return receipt 109 + 110 + 111 + # --------------------------------------------------------------------------- 112 + # Commands 113 + # --------------------------------------------------------------------------- 114 + 115 + @click.group() 116 + def cli(): 117 + """did:cow registry CLI\n 118 + \b 119 + Reads from the chain using RPC_URL. 120 + Writes require PRIVATE_KEY (the current controller account). 121 + See .env.example for configuration. 122 + """ 123 + 124 + 125 + @cli.command() 126 + @click.argument("did") 127 + def resolve(did): 128 + """Resolve a did:cow DID to its current state. 129 + 130 + \b 131 + DID format: did:cow:<controller_address>:<method>:<id> 132 + Example: did:cow:8BC101ABF5BcF8b6209FaaAD4D761C1ED14999Be:plc:7qqsrnkn4moc2jgdxvh6aa3t 133 + """ 134 + controller_hex, initial_wrapped = _parse_cow_did(did) 135 + w3 = _w3() 136 + contract = _contract(w3) 137 + 138 + cow_hash = contract.functions.calculateCowHash( 139 + _controller_address(controller_hex), 140 + initial_wrapped, 141 + ).call() 142 + 143 + controller, wrapped_did = contract.functions.cows(cow_hash).call() 144 + 145 + if wrapped_did == "": 146 + # No on-chain state — initial values from the DID string are authoritative 147 + click.echo(f"status: not yet registered on-chain") 148 + click.echo(f"wrapped: did:{initial_wrapped} (from DID)") 149 + click.echo(f"controller: {_controller_address(controller_hex)} (initial)") 150 + elif wrapped_did == ":": 151 + click.echo(f"status: deactivated") 152 + else: 153 + click.echo(f"status: active") 154 + click.echo(f"wrapped: did:{wrapped_did}") 155 + click.echo(f"controller: {controller}") 156 + 157 + 158 + @cli.command("update-wrapped") 159 + @click.argument("did") 160 + @click.argument("new_wrapped_did") 161 + def update_wrapped(did, new_wrapped_did): 162 + """Update the wrapped DID. 163 + 164 + \b 165 + NEW_WRAPPED_DID: the new wrapped DID, with or without leading 'did:' 166 + e.g. web:example.com or did:web:example.com 167 + """ 168 + controller_hex, initial_wrapped = _parse_cow_did(did) 169 + new_wrapped = _strip_did_prefix(new_wrapped_did) 170 + 171 + w3 = _w3() 172 + contract = _contract(w3) 173 + account = _account(w3) 174 + 175 + tx = contract.functions.updateWrappedDID( 176 + _controller_address(controller_hex), 177 + initial_wrapped, 178 + new_wrapped, 179 + ).build_transaction({}) 180 + 181 + _send(w3, account, tx) 182 + click.echo(f"wrapped: did:{new_wrapped}") 183 + 184 + 185 + @cli.command("update-controller") 186 + @click.argument("did") 187 + @click.argument("new_controller") 188 + def update_controller(did, new_controller): 189 + """Transfer control to a new Ethereum address. 190 + 191 + \b 192 + NEW_CONTROLLER: checksummed Ethereum address of the new controller. 193 + Note: setting this to the zero address is permanent (cow becomes uncontrollable). 194 + """ 195 + controller_hex, initial_wrapped = _parse_cow_did(did) 196 + 197 + w3 = _w3() 198 + contract = _contract(w3) 199 + account = _account(w3) 200 + 201 + tx = contract.functions.updateController( 202 + _controller_address(controller_hex), 203 + initial_wrapped, 204 + Web3.to_checksum_address(new_controller), 205 + ).build_transaction({}) 206 + 207 + _send(w3, account, tx) 208 + click.echo(f"controller: {Web3.to_checksum_address(new_controller)}") 209 + 210 + 211 + @cli.command() 212 + @click.argument("did") 213 + @click.confirmation_option(prompt="Deactivation is permanent and cannot be undone. Continue?") 214 + def deactivate(did): 215 + """Permanently deactivate a did:cow.""" 216 + controller_hex, initial_wrapped = _parse_cow_did(did) 217 + 218 + w3 = _w3() 219 + contract = _contract(w3) 220 + account = _account(w3) 221 + 222 + cow_hash = contract.functions.calculateCowHash( 223 + _controller_address(controller_hex), 224 + initial_wrapped, 225 + ).call() 226 + 227 + tx = contract.functions.deactivate(cow_hash).build_transaction({}) 228 + 229 + _send(w3, account, tx) 230 + click.echo("deactivated.") 231 + 232 + 233 + if __name__ == "__main__": 234 + cli()
+3
cli/requirements.txt
··· 1 + web3>=7.0.0 2 + click>=8.0.0 3 + python-dotenv>=1.0.0