Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
# Compiled bytecode for AddressClaim contract (set via npm run compile:contract)
VITE_ADDRESS_CLAIM_BYTECODE=0x

# Compiled bytecode for AddressHandleRegistry contract (set via npm run compile:handle-registry)
VITE_HANDLE_REGISTRY_BYTECODE=0x

# Compiled bytecode for Bip39Vocabulary contract (auto-generated)
VITE_BIP39_VOCABULARY_BYTECODE=0x

# Word Handle Registry addresses
# Set these to enable word handles per-network
VITE_HANDLE_REGISTRY_ADDRESS_ETHEREUM=0x0000000000000000000000000000000000000000
VITE_HANDLE_REGISTRY_ADDRESS_POLYGON=0xC61D976eF7E66D8c247233daC439Ca06137b0904
VITE_HANDLE_REGISTRY_ADDRESS_BSC=0xde16Caf38e556A12f9f64d0E76bc0EbFc731ac1f
VITE_HANDLE_REGISTRY_ADDRESS_ARBITRUM=0x0000000000000000000000000000000000000000
VITE_HANDLE_REGISTRY_ADDRESS_OPTIMISM=0x0000000000000000000000000000000000000000
VITE_HANDLE_REGISTRY_ADDRESS_AVALANCHE=0x0000000000000000000000000000000000000000
VITE_HANDLE_REGISTRY_ADDRESS_SEPOLIA=0x0000000000000000000000000000000000000000
VITE_HANDLE_REGISTRY_ADDRESS_MUMBAI=0x0000000000000000000000000000000000000000

# Ethereum Mainnet
VITE_CONTRACT_ADDRESS_ETHEREUM=0x0000000000000000000000000000000000000000

Expand Down
29 changes: 19 additions & 10 deletions COMPILATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ The contract requires `viaIR: true` compilation setting because:

## Compilation Methods

### Method 1: Using npm script (Recommended)
### Method 1: Using npm scripts (Recommended)

```bash
npm run compile:contract
npm run compile:contract # AddressClaim.sol only
npm run compile:handle-registry # AddressHandleRegistry.sol only
npm run compile:all-contracts # Executes both jobs sequentially
```

This runs `scripts/compile-contract.js` which:
1. Attempts to download Solidity 0.8.23 compiler
2. Falls back to local solc if network unavailable
3. Compiles with viaIR enabled
4. Generates build artifacts in `build/` directory
5. Updates `.env` with contract bytecode
Each script leverages the shared compiler utility which:
1. Attempts to download Solidity 0.8.23 (via IR) when a local 0.8.x build is missing
2. Falls back to the locally installed `solc` if the download fails
3. Compiles with `viaIR: true`, optimizer enabled (200 runs)
4. Emits artifacts under `build/`
5. Updates `.env` / `.env.local` with the appropriate bytecode env variables:
- `VITE_ADDRESS_CLAIM_BYTECODE` for `AddressClaim`
- `VITE_HANDLE_REGISTRY_BYTECODE` for `AddressHandleRegistry`
- `VITE_BIP39_VOCABULARY_BYTECODE` for `Bip39Vocabulary`

### Method 2: Manual compilation with solc 0.8.23+

Expand Down Expand Up @@ -89,9 +94,13 @@ Successful compilation produces:

```
build/
AddressClaim.json # Contract ABI and bytecode
AddressClaim.json
AddressHandleRegistry.json
Bip39Vocabulary.json
.env (or .env.local)
VITE_ADDRESS_CLAIM_BYTECODE=0x... # Updated with contract bytecode
VITE_ADDRESS_CLAIM_BYTECODE=0x...
VITE_HANDLE_REGISTRY_BYTECODE=0x...
VITE_BIP39_VOCABULARY_BYTECODE=0x...
```

## Verification
Expand Down
16 changes: 16 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Pocketbook is a revolutionary decentralized identity platform built on Ethereum
### Key Features

- 🎯 **Address Claiming**: Claim any Ethereum address you own and attach verifiable metadata
- 🧩 **Word Handles**: Deterministically mint BIP39 phrases that map directly to your address
- 🔐 **Cryptographic Verification**: All claims are secured by cryptographic signatures proving ownership
- 🌐 **Decentralized Network**: Build your web of trust without central authorities
- 🔒 **Privacy Control**: Choose what's public and what's private with whitelist-based access
Expand Down Expand Up @@ -355,8 +356,23 @@ VITE_CONTRACT_ADDRESS_AVALANCHE=0xYourContractAddressHere
# Testnets
VITE_CONTRACT_ADDRESS_SEPOLIA=0xYourContractAddressHere
VITE_CONTRACT_ADDRESS_MUMBAI=0xYourContractAddressHere

# Optional Word Handle Registries
VITE_HANDLE_REGISTRY_ADDRESS_ETHEREUM=0xYourRegistryAddressHere
VITE_HANDLE_REGISTRY_ADDRESS_POLYGON=0xC61D976eF7E66D8c247233daC439Ca06137b0904
VITE_HANDLE_REGISTRY_ADDRESS_BSC=0xde16Caf38e556A12f9f64d0E76bc0EbFc731ac1f
VITE_HANDLE_REGISTRY_ADDRESS_SEPOLIA=0xYourRegistryAddressHere
```

To enable word handles on a network, deploy `AddressHandleRegistry.sol` with the desired vocabulary parameters and set the corresponding `VITE_HANDLE_REGISTRY_ADDRESS_*` value. The UI automatically detects the registry, loads the BIP39 English word list, and exposes handle suggestion/claiming flows.

### Word Handle Registry

- `AddressHandleRegistry.sol` validates deterministic handle encodings and guarantees uniqueness per wallet.
- Handles are encoded as `[length:uint8] + length * uint16` indices pointing into the shared vocabulary (BIP39 by default).
- The front-end loads the vocabulary from `public/wordlists/bip39-english.txt`, suggests phrases via a deterministic SHA-256 PRNG, and calls `multiChainStore.claimHandleOnPrimaryChain()` / `releaseHandleOnPrimaryChain()` for mutations.
- Read helpers (`getHandleForAddress`, `isHandleTakenOnChain`) feed the explorer grid and the address detail view so every handle stays in sync across the multi-chain experience.

### Architecture

The multi-chain system uses two key stores:
Expand Down
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,42 @@ npm run dev

Visit `http://localhost:3000` and connect your MetaMask wallet to get started!

### 🔨 Compile the Contract
### 🔨 Compile the Contracts

```bash
# AddressClaim only
npm run compile:contract

# AddressHandleRegistry only
npm run compile:handle-registry

# Run both back-to-back
npm run compile:all-contracts
```

This command compiles `contracts/AddressClaim.sol`, writes the artifact to `build/AddressClaim.json`, and updates `VITE_ADDRESS_CLAIM_BYTECODE` in your local `.env` file so the admin deploy panel can access the bytecode. Ensure you have Solidity `0.8.x` available (the script fetches it automatically when online).
- `compile:contract` builds `contracts/AddressClaim.sol`, writes `build/AddressClaim.json`, and refreshes `VITE_ADDRESS_CLAIM_BYTECODE` inside your `.env`.
- `compile:handle-registry` auto-generates `contracts/generated/Bip39Vocabulary.sol` from the BIP39 text file, builds both `AddressHandleRegistry` and `Bip39Vocabulary`, and refreshes `VITE_HANDLE_REGISTRY_BYTECODE` / `VITE_BIP39_VOCABULARY_BYTECODE`.
- `compile:all-contracts` is a convenience job that executes both compilers in sequence so CI/CD pipelines can produce every artifact with a single command.

Each script automatically fetches Solidity `0.8.23` (via IR with 200 optimizer runs) if the matching compiler is not already available locally.

### 🔡 Word Handles

Pocketbook now ships with deterministic, human-readable handles backed by the `AddressHandleRegistry` contract and the BIP39 English vocabulary (stored at `public/wordlists/bip39-english.txt`). Each wallet can reserve a unique phrase whose encoded bytes map 1:1 to the registry entry.

1. Deploy `contracts/AddressHandleRegistry.sol` to any network where you want handles enabled. Provide:
- `vocabLength` — number of entries in your vocabulary (2048 for the default BIP39 word list).
- `maxLength` — maximum number of words per handle.
- `vocabHash` — `sha256` hash of the vocabulary file contents.
2. Populate the matching `VITE_HANDLE_REGISTRY_ADDRESS_<NETWORK>` variables in your `.env` file (e.g. `VITE_HANDLE_REGISTRY_ADDRESS_SEPOLIA`).
3. Run `npm run dev` and connect a wallet on a supported chain. The claim form will suggest deterministic handles and let users mint/release them directly from the UI.

The explorer and address detail views automatically display claimed handles when a registry is configured on the active chain.

### ✨ Features

- 🎯 **Address Claiming** - Prove ownership and attach verified metadata to any address
- 🧩 **Word Handles** - Deterministically mint BIP39-based phrases that map directly to addresses
- 🔐 **Cryptographic Security** - All claims signed and verified on-chain
- 🆔 **DID Support** - W3C compliant Decentralized Identifiers (did:ethr) for self-sovereign identity
- 🏷️ **ENS Integration** - Use human-readable names (name.eth) instead of addresses with full ENS resolution and reverse lookup
Expand Down
79 changes: 74 additions & 5 deletions contracts/AddressClaim.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ contract AddressClaim is ReentrancyGuard {

// Mapping to track if address is claimed
mapping(address => bool) public isClaimed;

// Active claim tracking
uint256 public totalActiveClaims;
address[] private activeClaimedAddresses;
mapping(address => uint256) private activeClaimIndex; // 1-based index

// Mapping from DID string to address (for DID resolution)
mapping(string => address) public didToAddress;
Expand Down Expand Up @@ -138,6 +143,35 @@ contract AddressClaim is ReentrancyGuard {
event AttestationCreated(address indexed attester, address indexed subject, uint8 trustLevel, uint256 timestamp);
event AttestationRevoked(address indexed attester, address indexed subject, uint256 timestamp);
event AttestationUpdated(address indexed attester, address indexed subject, uint8 trustLevel, uint256 timestamp);

function _addActiveClaimant(address _address) internal {
require(activeClaimIndex[_address] == 0, "Already tracked");
activeClaimedAddresses.push(_address);
activeClaimIndex[_address] = activeClaimedAddresses.length; // 1-based
totalActiveClaims += 1;
}

function _removeActiveClaimant(address _address) internal {
uint256 index = activeClaimIndex[_address];
if (index == 0) {
return;
}

uint256 arrayIndex = index - 1;
uint256 lastIndex = activeClaimedAddresses.length - 1;
if (arrayIndex != lastIndex) {
address lastAddress = activeClaimedAddresses[lastIndex];
activeClaimedAddresses[arrayIndex] = lastAddress;
activeClaimIndex[lastAddress] = index;
}

activeClaimedAddresses.pop();
activeClaimIndex[_address] = 0;

if (totalActiveClaims > 0) {
totalActiveClaims -= 1;
}
}

/**
* @dev Claim an address with signed metadata
Expand Down Expand Up @@ -184,7 +218,7 @@ contract AddressClaim is ReentrancyGuard {
newClaim.signature = _signature;
newClaim.claimTime = block.timestamp;
newClaim.isActive = true;

// Initialize metadata
newClaim.metadata.name = _name;
newClaim.metadata.avatar = _avatar;
Expand All @@ -204,7 +238,8 @@ contract AddressClaim is ReentrancyGuard {

isClaimed[_address] = true;
didToAddress[did] = _address;

_addActiveClaimant(_address);

emit AddressClaimed(_address, msg.sender, block.timestamp);
emit DIDCreated(_address, did, block.timestamp);

Expand Down Expand Up @@ -346,10 +381,11 @@ contract AddressClaim is ReentrancyGuard {
// [M-01] Delete DID mapping to prevent stale DID resolution
string memory did = claims[msg.sender].didDocument.did;
delete didToAddress[did];

claims[msg.sender].isActive = false;
isClaimed[msg.sender] = false;

_removeActiveClaimant(msg.sender);

emit ClaimRevoked(msg.sender, block.timestamp);
}

Expand All @@ -369,7 +405,7 @@ contract AddressClaim is ReentrancyGuard {
bool isPrivate
) {
require(isClaimed[_address], "Address not claimed");

Claim memory claim = claims[_address];

// Check if caller can view private metadata
Expand All @@ -394,6 +430,39 @@ contract AddressClaim is ReentrancyGuard {
claim.metadata.isPrivate
);
}

function getTotalClaims() public view returns (uint256) {
return totalActiveClaims;
}

function getClaimedAddressesCount() public view returns (uint256) {
return activeClaimedAddresses.length;
}

function getClaimedAddressesPaginated(uint256 offset, uint256 limit) public view returns (address[] memory addresses, uint256 total) {
uint256 totalAddresses = activeClaimedAddresses.length;
total = totalAddresses;

if (limit == 0 || offset >= totalAddresses) {
return (new address[](0), totalAddresses);
}

uint256 end = offset + limit;
if (end > totalAddresses) {
end = totalAddresses;
}

uint256 sliceLength = end - offset;
addresses = new address[](sliceLength);
for (uint256 i = 0; i < sliceLength; i++) {
addresses[i] = activeClaimedAddresses[offset + i];
}
}

function getClaimedAddresses(uint256 offset, uint256 limit) external view returns (address[] memory) {
(address[] memory addresses, ) = getClaimedAddressesPaginated(offset, limit);
return addresses;
}

/**
* @dev Get PGP signature for an address
Expand Down
84 changes: 84 additions & 0 deletions contracts/AddressHandleRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IAddressHandleRegistry.sol";

contract AddressHandleRegistry is IAddressHandleRegistry {
mapping(address => bytes) private _handleOf;
mapping(bytes32 => address) private _ownerOfByHash;

uint16 public immutable override vocabLength;
uint8 public immutable override maxLength;
bytes32 public immutable override vocabHash;

constructor(uint16 vocabLength_, uint8 maxLength_, bytes32 vocabHash_) {
if (vocabLength_ == 0) revert InvalidHandle();
if (maxLength_ == 0) revert InvalidHandle();

vocabLength = vocabLength_;
maxLength = maxLength_;
vocabHash = vocabHash_;
}

function handleOf(address owner)
external
view
override
returns (bytes memory handle)
{
return _handleOf[owner];
}

function ownerOf(bytes calldata handle)
public
view
override
returns (address owner)
{
return _ownerOfByHash[keccak256(handle)];
}

function claim(bytes calldata handle) external override {
if (_handleOf[msg.sender].length != 0) revert AlreadyHasHandle();
if (!_isValidHandle(handle)) revert InvalidHandle();

bytes32 key = keccak256(handle);
if (_ownerOfByHash[key] != address(0)) revert HandleTaken();

_handleOf[msg.sender] = handle;
_ownerOfByHash[key] = msg.sender;

emit HandleClaimed(msg.sender, handle);
}

function release() external override {
bytes memory handle = _handleOf[msg.sender];
if (handle.length == 0) revert NoHandle();

delete _handleOf[msg.sender];
delete _ownerOfByHash[keccak256(handle)];

emit HandleReleased(msg.sender, handle);
}

function _isValidHandle(bytes calldata handle) internal view returns (bool) {
if (handle.length == 0) return false;

uint8 L = uint8(handle[0]);
if (L == 0 || L > maxLength) return false;

if (handle.length != 1 + 2 * uint256(L)) return false;

uint16 vlen = vocabLength;
uint256 offset = 1;

for (uint256 i = 0; i < L; ++i) {
uint16 idx = (uint16(uint8(handle[offset])) << 8) |
uint16(uint8(handle[offset + 1]));
if (idx >= vlen) return false;
offset += 2;
}

return true;
}
}
22 changes: 22 additions & 0 deletions contracts/IAddressHandleRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IAddressHandleRegistry {
event HandleClaimed(address indexed owner, bytes handle);
event HandleReleased(address indexed owner, bytes handle);

error InvalidHandle();
error HandleTaken();
error AlreadyHasHandle();
error NoHandle();

function vocabLength() external view returns (uint16);
function maxLength() external view returns (uint8);
function vocabHash() external view returns (bytes32);

function handleOf(address owner) external view returns (bytes memory handle);
function ownerOf(bytes calldata handle) external view returns (address owner);

function claim(bytes calldata handle) external;
function release() external;
}
Empty file added contracts/generated/.gitkeep
Empty file.
98 changes: 98 additions & 0 deletions contracts/generated/Bip39Vocabulary.sol

Large diffs are not rendered by default.

Loading
Loading