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
1 change: 1 addition & 0 deletions internal/contracts/resolver/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var errorNotAvailableOnChainID = errors.New("not available for chainID")
var contractAddressByChainID = map[uint64]common.Address{
1: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // mainnet
11155111: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // sepolia testnet
31337: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // anvil (copied from ens-usernames deploy)
}

func ContractAddress(chainID uint64) (common.Address, error) {
Expand Down
3 changes: 2 additions & 1 deletion services/ens/ensresolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ func (e *EnsResolver) OwnerOf(ctx context.Context, chainID uint64, username stri
// Get the NameWrapper contract address for the given chain ID
nameWrapperAddress, err := NameWrapperContractAddress(chainID)
if err != nil {
return nil, err
// No NameWrapper on this chain — name can't be wrapped, return registry owner
return &owner, nil
Comment on lines +104 to +105
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure of the consequences of this change.
@saledjenic is this safe?

}

if owner != nameWrapperAddress {
Expand Down
4 changes: 4 additions & 0 deletions services/wallet/requests/router_input_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ func (i *RouteInputParams) Validate() error {
if i.Username == "" || i.PublicKey == "" {
return ErrENSRegisterRequiresUsernameAndPubKey
}
if i.FromChainID == walletCommon.AnvilMainnet {
// On Anvil, the token address is determined dynamically from the registrar contract
return nil
}
if i.TestnetMode {
// available only on sepolia testnet
sntToken := tokentypes.Token{
Expand Down
20 changes: 18 additions & 2 deletions services/wallet/router/pathprocessor/processor_ens_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ func (s *ENSRegisterProcessor) GetPriceForRegisteringEnsName(chainID uint64) (*b
}

func (s *ENSRegisterProcessor) AvailableFor(params ProcessorInputParams) (bool, error) {
return params.FromChain.ChainID == walletCommon.EthereumMainnet || params.FromChain.ChainID == walletCommon.EthereumSepolia, nil
return params.FromChain.ChainID == walletCommon.EthereumMainnet ||
params.FromChain.ChainID == walletCommon.EthereumSepolia ||
params.FromChain.ChainID == walletCommon.AnvilMainnet, nil
}

func (s *ENSRegisterProcessor) CalculateFees(params ProcessorInputParams) (*big.Int, *big.Int, error) {
Expand Down Expand Up @@ -144,5 +146,19 @@ func (s *ENSRegisterProcessor) CalculateAmountOut(params ProcessorInputParams) (
}

func (s *ENSRegisterProcessor) GetContractAddress(params ProcessorInputParams) (common.Address, error) {
return snt.ContractAddress(params.FromChain.ChainID)
addr, err := snt.ContractAddress(params.FromChain.ChainID)
if err == nil {
return addr, nil
}

// Fallback: resolve token address dynamically from the registrar contract
registrarAddr, err := s.ensResolver.GetRegistrarAddress(context.Background(), params.FromChain.ChainID)
if err != nil {
return common.Address{}, createENSRegisterProcessorErrorResponse(err)
}
reg, err := s.contractMaker.NewUsernameRegistrar(params.FromChain.ChainID, registrarAddr)
if err != nil {
return common.Address{}, createENSRegisterProcessorErrorResponse(err)
}
return reg.Token(&bind.CallOpts{Context: context.Background(), Pending: false})
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func (s *ENSReleaseProcessor) Name() string {
}

func (s *ENSReleaseProcessor) AvailableFor(params ProcessorInputParams) (bool, error) {
return params.FromChain.ChainID == walletCommon.EthereumMainnet || params.FromChain.ChainID == walletCommon.EthereumSepolia, nil
return params.FromChain.ChainID == walletCommon.EthereumMainnet ||
params.FromChain.ChainID == walletCommon.EthereumSepolia ||
params.FromChain.ChainID == walletCommon.AnvilMainnet, nil
}

func (s *ENSReleaseProcessor) CalculateFees(params ProcessorInputParams) (*big.Int, *big.Int, error) {
Expand Down
2 changes: 1 addition & 1 deletion services/wallet/router/sendtype/send_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (s SendType) IsAvailableFor(chainID uint64) bool {
}

if s.IsEnsTransfer() || s.IsStickersTransfer() {
return chainID == walletCommon.EthereumMainnet || chainID == walletCommon.EthereumSepolia
return chainID == walletCommon.EthereumMainnet || chainID == walletCommon.EthereumSepolia || chainID == walletCommon.AnvilMainnet
}

return true
Expand Down
4 changes: 1 addition & 3 deletions tests-functional/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ WORKDIR /app
RUN mkdir -p /app/contracts
COPY contracts/Multicall3.sol /app/contracts/

COPY --chmod=0755 clone_and_run.sh /app
COPY --chmod=0755 deploy_contracts.sh /app
COPY --chmod=0755 entrypoint.sh /app
COPY --chmod=0755 *.sh /app

ENTRYPOINT [ "/app/entrypoint.sh" ]
45 changes: 45 additions & 0 deletions tests-functional/clients/contract_deployers/ens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from clients.foundry import Foundry
from resources.constants import DEPLOYER_ACCOUNT


class ENSDeployer:

def __init__(self, foundry: Foundry):
self.deploy_output = foundry.clone_and_run(
github_org="status-im",
github_repo="ens-usernames",
smart_contract_dir="script",
smart_contract_filename="Deploy.s.sol",
private_key=DEPLOYER_ACCOUNT.private_key,
sender_address=DEPLOYER_ACCOUNT.address,
)

self.registry_address = self._find_contract("ENSRegistry")
self.resolver_address = self._find_contract("PublicResolver")
self.token_address = self._find_contract("MiniMeToken")
self.registrar_address = self._find_contract("UsernameRegistrar")

@classmethod
def from_file(cls, foundry: Foundry, container_file_path: str):
"""Load ENS addresses from a JSON file in the foundry container."""
import json

instance = cls.__new__(cls)
instance.deploy_output = None

host_file_path = foundry.get_archive(container_file_path)
with open(host_file_path, "r") as f:
addresses = json.load(f)

instance.registry_address = addresses["registry"]
instance.resolver_address = addresses["resolver"]
instance.token_address = addresses["token"]
instance.registrar_address = addresses["registrar"]

return instance

def _find_contract(self, contract_name):
for deployment in self.deploy_output.values():
if f"contract {contract_name}" in deployment.get("internal_type", ""):
return deployment["value"]
raise Exception(f"{contract_name} contract not found in deploy output")
36 changes: 36 additions & 0 deletions tests-functional/clients/services/ens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from clients.services.service import Service


class EnsService(Service):
def __init__(self, client):
super().__init__(client, "ens")

def add(self, chain_id, username):
return self.rpc_request("add", [chain_id, username])

def remove(self, chain_id, username):
return self.rpc_request("remove", [chain_id, username])

def get_ens_usernames(self):
return self.rpc_request("getEnsUsernames")

def get_registrar_address(self, chain_id):
return self.rpc_request("getRegistrarAddress", [chain_id])

def resolver(self, chain_id, username):
return self.rpc_request("resolver", [chain_id, username])

def owner_of(self, chain_id, username):
return self.rpc_request("ownerOf", [chain_id, username])

def public_key_of(self, chain_id, username):
return self.rpc_request("publicKeyOf", [chain_id, username])

def address_of(self, chain_id, username):
return self.rpc_request("addressOf", [chain_id, username])

def expire_at(self, chain_id, username):
return self.rpc_request("expireAt", [chain_id, username])

def price(self, chain_id):
return self.rpc_request("price", [chain_id])
2 changes: 2 additions & 0 deletions tests-functional/clients/status_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from clients.services.accounts import AccountService
from clients.services.appgeneral import AppgeneralService
from clients.services.connector import ConnectorService
from clients.services.ens import EnsService
from clients.services.eth import EthService
from clients.services.linkpreview import LinkPreviewService
from clients.services.multiaccounts import MultiAccountsService
Expand Down Expand Up @@ -106,6 +107,7 @@ def __init__(self, privileged=False, ipv6=USE_IPV6, **kwargs):
self.sharedurls_service = SharedURLsService(self)
self.connector_service = ConnectorService(self)
self.appgeneral_service = AppgeneralService(self)
self.ens_service = EnsService(self)
self.eth_service = EthService(self)
self.linkpreview_service = LinkPreviewService(self)
self.expvar_client = ExpvarClient(self.base_url)
Expand Down
57 changes: 57 additions & 0 deletions tests-functional/deploy_contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,61 @@ else
exit 1
fi

# Deploy ENS contracts
echo "Deploying ENS contracts..."
/app/clone_and_run.sh status-im ens-usernames script Deploy.s.sol $DEPLOYER_PRIVATE_KEY $DEPLOYER_ADDRESS

ENS_BROADCAST_FILE="/app/ens-usernames/broadcast/Deploy.s.sol/31337/run-latest.json"
if [ -f "$ENS_BROADCAST_FILE" ]; then
REGISTRY_ADDR=$(grep -A1 '"contractName": "ENSRegistry"' "$ENS_BROADCAST_FILE" | grep '"contractAddress"' | head -1 | sed 's/.*"contractAddress": "\([^"]*\)".*/\1/')
RESOLVER_ADDR=$(grep -A1 '"contractName": "PublicResolver"' "$ENS_BROADCAST_FILE" | grep '"contractAddress"' | head -1 | sed 's/.*"contractAddress": "\([^"]*\)".*/\1/')
TOKEN_ADDR=$(grep -A1 '"contractName": "MiniMeToken"' "$ENS_BROADCAST_FILE" | grep '"contractAddress"' | head -1 | sed 's/.*"contractAddress": "\([^"]*\)".*/\1/')
REGISTRAR_ADDR=$(grep -A1 '"contractName": "UsernameRegistrar"' "$ENS_BROADCAST_FILE" | grep '"contractAddress"' | head -1 | sed 's/.*"contractAddress": "\([^"]*\)".*/\1/')

echo "ENS Registry (deployed): $REGISTRY_ADDR"
echo "ENS Resolver: $RESOLVER_ADDR"
echo "ENS Token: $TOKEN_ADDR"
echo "ENS Registrar: $REGISTRAR_ADDR"

# Domain setup on deployed registry — registrar and resolver reference it via immutables
echo "Setting up stateofus.eth domain..."
ROOT_NODE="0x0000000000000000000000000000000000000000000000000000000000000000"
ETH_NAMEHASH=$(cast namehash "eth")
ETH_LABELHASH=$(cast keccak "eth")
STATEOFUS_LABELHASH=$(cast keccak "stateofus")

cast send $REGISTRY_ADDR "setSubnodeOwner(bytes32,bytes32,address)" \
$ROOT_NODE $ETH_LABELHASH $DEPLOYER_ADDRESS \
--rpc-url $ANVIL_URL --private-key $DEPLOYER_PRIVATE_KEY

cast send $REGISTRY_ADDR "setSubnodeOwner(bytes32,bytes32,address)" \
$ETH_NAMEHASH $STATEOFUS_LABELHASH $REGISTRAR_ADDR \
--rpc-url $ANVIL_URL --private-key $DEPLOYER_PRIVATE_KEY

# Activate registrar (required before it accepts registrations)
echo "Activating registrar..."
cast send $REGISTRAR_ADDR "activate(uint256)" 1000000000000000000 \
--rpc-url $ANVIL_URL --private-key $DEPLOYER_PRIVATE_KEY

# Go code (go-ens, resolver/address.go) queries the well-known ENS registry.
# Copy deployed registry code + storage to well-known address so Go can read it.
WELL_KNOWN_REGISTRY="0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
echo "Syncing deployed registry to well-known address..."
/app/sync_ens_registry.sh $REGISTRY_ADDR $WELL_KNOWN_REGISTRY $ANVIL_URL

cat > $CONTRACTS_PATH/ens_addresses.json << EOF
{
"registry": "$REGISTRY_ADDR",
"resolver": "$RESOLVER_ADDR",
"token": "$TOKEN_ADDR",
"registrar": "$REGISTRAR_ADDR"
}
EOF

echo "ENS contracts deployed and configured successfully"
else
echo "Error: ENS broadcast file not found!"
exit 1
fi

echo "All contracts deployed successfully!"
3 changes: 2 additions & 1 deletion tests-functional/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ markers =
activity
assets
benchmark
connector
connector
ens
1 change: 1 addition & 0 deletions tests-functional/resources/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,4 @@ class Account:
# Container paths for pre-deployed contracts
SNT_ADDRESSES_CONTAINER_PATH = "/app/contracts/snt_addresses.json"
COMMUNITIES_ADDRESSES_CONTAINER_PATH = "/app/contracts/communities_addresses.json"
ENS_ADDRESSES_CONTAINER_PATH = "/app/contracts/ens_addresses.json"
35 changes: 35 additions & 0 deletions tests-functional/sync_ens_registry.sh
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename it to sync_ens_registry.sh for clarity

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to sync_ens_registry.sh and updated all references

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh
# Sync ENS registry storage from source to destination address.
# Usage: sync_ens_registry.sh <source_addr> <dest_addr> <rpc_url> [node1 node2 ...]
# If no nodes specified, syncs root, eth, and stateofus.eth nodes.

set -e

SOURCE=$1
DEST=$2
RPC_URL=$3
shift 3

# Default nodes to sync
if [ $# -eq 0 ]; then
set -- \
"0x0000000000000000000000000000000000000000000000000000000000000000" \
"$(cast namehash 'eth')" \
"$(cast namehash 'stateofus.eth')"
fi

# Copy bytecode
CODE=$(cast code $SOURCE --rpc-url $RPC_URL)
cast rpc anvil_setCode $DEST $CODE --rpc-url $RPC_URL > /dev/null

# Copy storage for each node: owner (base+0), resolver (base+1), ttl (base+2)
for NODE in "$@"; do
BASE_SLOT=$(cast index bytes32 $NODE 0)
for OFFSET in 0 1 2; do
SLOT=$(perl -e "use Math::BigInt; print '0x', Math::BigInt->new('$BASE_SLOT')->badd($OFFSET)->as_hex() =~ s/^0x//r;")
# Pad to 66 chars (0x + 64 hex)
SLOT=$(printf "0x%064s" "$(echo $SLOT | sed 's/0x//')")
VALUE=$(cast storage $SOURCE $SLOT --rpc-url $RPC_URL)
cast rpc anvil_setStorageAt $DEST $SLOT $VALUE --rpc-url $RPC_URL > /dev/null
done
done
26 changes: 24 additions & 2 deletions tests-functional/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
import pytest_asyncio
from requests import ReadTimeout
from tenacity import retry, stop_after_attempt, wait_fixed
from web3 import Web3

from clients.anvil import Anvil
Expand All @@ -16,13 +17,19 @@
USE_IPV6,
SNT_ADDRESSES_CONTAINER_PATH,
COMMUNITIES_ADDRESSES_CONTAINER_PATH,
ENS_ADDRESSES_CONTAINER_PATH,
)
from utils import fake
from utils.config import Config

logger = logging.getLogger(__name__)


@retry(stop=stop_after_attempt(30), wait=wait_fixed(2), reraise=True)
def _load_contract_json(foundry_client, path):
return foundry_client.load_json(path)


@pytest.fixture(scope="function", autouse=False)
def backend_factory(request):
"""
Expand Down Expand Up @@ -197,7 +204,7 @@ def multicall3_deployer(foundry_client):
@pytest.fixture(scope="session")
def snt_addresses(foundry_client):
try:
data = foundry_client.load_json(SNT_ADDRESSES_CONTAINER_PATH)
data = _load_contract_json(foundry_client, SNT_ADDRESSES_CONTAINER_PATH)
logger.info(f"Using pre-deployed SNT contracts: token={data['snt']}, controller={data['controller']}")
return data
except Exception as e:
Expand All @@ -212,7 +219,7 @@ def snt_addresses(foundry_client):
@pytest.fixture(scope="session")
def communities_addresses(foundry_client):
try:
data = foundry_client.load_json(COMMUNITIES_ADDRESSES_CONTAINER_PATH)
data = _load_contract_json(foundry_client, COMMUNITIES_ADDRESSES_CONTAINER_PATH)
logger.info("Using pre-deployed Communities contracts")
return data
except Exception as e:
Expand All @@ -224,6 +231,21 @@ def communities_addresses(foundry_client):
) from e


@pytest.fixture(scope="session")
def ens_addresses(foundry_client):
try:
data = _load_contract_json(foundry_client, ENS_ADDRESSES_CONTAINER_PATH)
logger.info(f"Using pre-deployed ENS contracts: registry={data['registry']}, " f"registrar={data['registrar']}, token={data['token']}")
return data
except Exception as e:
logger.error(f"Failed to load ENS addresses from container: {e}")
logger.error("ENS contracts should be deployed as part of docker-compose startup")
raise RuntimeError(
"ENS contracts not found. Make sure the foundry container has deployed contracts during startup. "
"This should happen automatically in entrypoint.sh"
) from e


@pytest.fixture(scope="function")
def snt_token_overrides(snt_addresses):
return [
Expand Down
Loading
Loading