0.5.3 • Published 1 year ago

@le7el/web3_crs v0.5.3

Weekly downloads
-
License
BSD 2
Repository
gitlab
Last release
1 year ago

CRS

Domain-based record system to store essential metadata on-chain. An ERC721 NFT is issued for the owner of domain record, allowing on-chain governance for a system which uses CRS as a canonic source of truth for it's data. It's an adapted fork of Ethereum Name Service.

JS

All web3 interactions are using default window.ethereum web3 provider. It also proxies ethers so you can use the same version in your project.

Installation

`npm install @le7el/web3_crs`

Usage

`import { ethers, controller, nft, resolver, utils, artifacts } from "@le7el/web3_crs"`

Controller

The main interface for interaction with CRS NFTs.

available(string name, Web3Provider provider = null) returns (boolean promise)

Checks if specific name wasn't registered already.

```js
await controller.available("VeVerse", CONTROLLER_ADDRESS)
```

makeCommitment(string name, address owner, bytes32 secret, address resolver, address addr, string contract_address, Web3Provider provider = null) returns (bytes32 promise)

Generates commitment hash for the provided secret. Default values for resolver and addr whould be default NFT resolver and owner.

```js
const owner = "0x0000000000000000000000000000000000000000"
const secret = ethers.utils.randomBytes(32)
const commitment = await controller.makeCommitment("VeVerse", owner, secret, null, null, CONTROLLER_ADDRESS)
```

commit(bytes32 commitment, string contract_address, Web3Provider provider = null) returns (transaction promise)

Start registration of CRS name, there should be 60 seconds delay between commitment and registration to prevent race conditions.

```js
await controller.commit(commitment, CONTROLLER_ADDRESS)
```

whitelistedCommit(string name, bytes32 commitment, bytes pass, string contract_address, Web3Provider provider = null) returns (transaction promise)

Commitment version with whitelisted pass.

```js
await controller.commit(name, commitment, pass, CONTROLLER_ADDRESS)
```

Pass can be generated using the following code:

```js
const permit = async function(privKey, address, name) {
    const whitelistMessage = ethers.utils.keccak256(
        ethers.utils.defaultAbiCoder.encode(
            ["bytes32", "address", "string"], [
                await controller.getDomainSeparator(CONTROLLER_ADDRESS),
                address,
                name
            ],
        ),
    )

    const signer = new ethers.Wallet(privKey)
    const whitelistSignature = await signer.signMessage(ethers.utils.arrayify(whitelistMessage))
    const sig = ethers.utils.splitSignature(
        ethers.utils.arrayify(whitelistSignature)
    );
    const pass = ethers.utils.defaultAbiCoder.encode(
        ["uint8", "bytes32", "bytes32"], [sig.v, sig.r, sig.s],
    )
    return pass
}
```

Domain separator can be also generated off-chain using the following code:

```js
function domain(chainId, verifyingContract) {
    return {
        name: "IdentityWhitelist",
        version: "v1",
        chainId,
        verifyingContract
    }
}

const chainId = 1 // Ethereum Mainnet
const contractDomain = ethers.utils._TypedDataEncoder.hashDomain(
    domain(chainId, CONTROLLER_ADDRESS)
)
```

register(string name, address owner, integer duration, bytes32 secret, address resolver, address addr, string contract_address, Web3Provider provider = null) returns (transaction promise)

All variables should be the same as used on makeCommitment step. Some CRS may require approval of a relevant ERC20 token, before running this transaction. This can be done with utils.erc20Approve call. Default values for resolver and addr whould be default NFT resolver and owner, should be the same values as in makeCommitment.

```js
const owner = "0x0000000000000000000000000000000000000000"
const duration = 31536000 // 1 year in seconds
await controller.register("VeVerse", owner, duration, secret, null, null, CONTROLLER_ADDRESS)
```

rentPrice(string name, integer duration, string contract_address, Web3Provider provider = null) returns (address, BigNumber promise)

Checks if there is any fee to register / renew this CRS record, returns contract address of ERC20 token and the fee amount. In case address is zero, it means that fee should be paid in gas coin, which is managed by register automatically, call erc20Approve if there is non-zero fee in ERC20 token.

NFT

Interface to fetch data related to specific NFT.

gasFee(integer duration, string contract_address, Web3Provider provider = null) returns (address, BigNumber promise)

Checks if there is any fee to register / renew this NFT, returns contract address of ERC20 token and the fee amount. In case address is zero, it means that fee should be paid in gas coin, which is managed by register automatically, call erc20Approve if there is non-zero fee in ERC20 token.

The difference with the rentPrice is that it's paid to the LE7EL DAO, instead of the root domain owner.

getName(integer id, string contract_address, Web3Provider provider = null) returns (string promise)

Get human-readable name e.g. johndoe.l7l associated with this NFT id.

getMetadata(integer id, string contract_address, Web3Provider provider = null) returns (object promise)

Get full metadata for the NFT by it's id. It includes the name given by getName but is more heavy RPC operation, so if you need just name, use getName.

Sample metadata would look like this:

```json
{
    "name":"johndoe.l7l",
    "description":"Ownership of identity record on le7el.com",
    "image_data":"<svg>...</svg>",
    "attributes":[{"trait_type":"Type","value":"Identity"}]
}
```

getNameExpires(string name, string contract_address, Web3Provider provider = null) returns (BigNumber promise)

Get expire date in timestamp for name registered as CRS record associated with NFT.

getNftsForWallet(address wallet, string chainId, integer from_block = 0, integer|string to_block = 'latest', integer max_blocks_per_query = 3450, Web3Provider provider = null) returns (string promise)

Returns the list of NFT ids owned by specific wallet. Ids are returned as strings in a hex form e.g. 0x27cf60eaf7291bc5004051bead7742d568b25692edcfee3d2ee7e1b047c49382

```js
const nftIds = await getNftsForWallet('0x42b3830ac0781c9dc46d94a2a5a830bd10797aec', '4')
```

Resolver

API to get / set values associated with specific NFT.

setText(bytes32 node, string key, string value, string resolver_address, Web3Provider provider = null) returns (transaction promise)

Set an arbitary string value associated with specific CRS record, by a string key.

Here we assign "CUSTOM_AVATAR_URL" to johndoe.l7l CRS record:

```js
await resolver.setText(
    0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d,
    "CUSTOM_AVATAR_URL",
    "ipfs://bafybeigkgx3gq5yrrsyxpna2czlq3bc2ish2gk6yqh7v57kugehlq6qoly"
)
```

getText(bytes32 node, string key, string resolver_address, Web3Provider provider = null) returns (string promise)

Get an arbitary string value associated with specific CRS record, by a string key.

To get "CUSTOM_AVATAR_URL" value for johndoe.l7l CRS record:

```js
const custom_avatar = await resolver.getText(
    0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d,
    "CUSTOM_AVATAR_URL"
)
```

setProxyConfig(bytes32 node, address controller, bytes4 selector, address proxy, string resolver_address, Web3Provider provider = null) returns (transaction promise)

Set proxy contract associated with specific CRS record. Such proxy contracts are used by 3rd parties to attach metadata associated with their project to CRS record without a special user permission.

In the following example CUSTOM_CONTROLLER_ADDRESS is assigning a proxy contract CUSTOM_PROXY_ADDRESS with a method tokenURI to johndoe.l7l CRS record. Only CUSTOM_CONTROLLER_ADDRESS will be able to change CUSTOM_PROXY_ADDRESS in future.

```js
await resolver.setProxyConfig(
    0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d,
    CUSTOM_CONTROLLER_ADDRESS, 
    bytes4(keccak256("tokenURI(uint256,string,string)")),
    CUSTOM_PROXY_ADDRESS,
    RESOLVER_ADDRESS
)
```

getProxyConfig(bytes32 node, address controller, bytes4 selector, string resolver_address, Web3Provider provider = null) returns (string promise)

Get proxy contract associated with specific CRS record. Such proxy contracts are used by 3rd parties to attach metadata associated with their project to CRS record without a special user permission.

To a get custom JSON metadata for NFT in scope of CUSTOM_CONTROLLER_ADDRESS the following code can be used.

```js
const proxy_address = await resolver.getProxyConfig(
    0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d,
    CUSTOM_CONTROLLER_ADDRESS, 
    bytes4(keccak256("tokenURI(uint256,string,string)"))
)
const provider = new ethers.providers.Web3Provider(window.ethereum)
const contract = new ethers.Contract(proxy_address, PROXY_ABI, provider)
const myProjectMetadata = await contract.tokenURI(1, "johndoe.l7l", "")
```

Utils

initContract(address contract_address, object abi, boolean readOnly, Web3Provider provider = null) returns (ethers.Contract promise)

Initialise contract by address and abi. Pass false to readOnly if you need to publish transactions.

```js
initContract(controller_address, controllerConfig.abi)
    .then(contract => contract.getDomainSeparator())
```

erc20Approve(address token_address, address spender_address, BigNumber amount, Web3Provider provider = null) returns (transaction promise)

Registration may require fees to be paid in native ERC20 token, use this method to authorize such payment before registration. Check BigNumber to pass token amount. You only need this if, controller.rentPrice(...) returns non-zero address as first variable and greater than 0 on second variable e.g.

```js
controller.rentPrice(name, duration, CONTROLLER_ADDRESS)
  .then((response) => {
    const token = response[0]
    const amount = response[1]
    if (token === ADDRESS_ZERO || response[1].isZero()) {
      return register(name, accounts[0], duration, secret, null, null, CONTROLLER_ADDRESS)
    } else {
      return erc20Approve(token, CONTROLLER_ADDRESS, amount)
        .then(tr => provider.waitForTransaction(tr.hash))
        .then(_ => register(name, accounts[0], duration, secret, null, null, CONTROLLER_ADDRESS))
    }
  })
```

nameToNode(string name) returns (string)

Convert human-readable name e.g. johndoe.l7l into a node used in CRS resolver.

```js
const node = nameToNode("johndoe.l7l")
node === "0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d"
```

nameToId(string name) returns (string)

Convert human-readable name e.g. johndoe.l7l into an id used for the related NFT. A reserve operation is not possible off-chain, use getName from nft module for that.

```js
const id = nameToId("johndoe.l7l")
node === "2871587377811694866171220606258498541438397112258729693763861304963081030941"
```

nameToNftId(string name) returns (string)

Convert human-readable name e.g. johndoe into the related NFT id

```js
const id = nameToNftId(`johndoe`)
id === "93179736082580775654606992995456773265844288993353207761363709104151049046407"
```

nodeToId(string name) returns (string)

Convert CRS node e.g. 0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d into an id used for the related NFT.

```js
const id = nodeToId("0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d")
node === "2871587377811694866171220606258498541438397112258729693763861304963081030941"
```

idToNode(string name) returns (string)

Convert NFT id e.g. 2871587377811694866171220606258498541438397112258729693763861304963081030941 into CRS node.

```js
const id = idToNode("2871587377811694866171220606258498541438397112258729693763861304963081030941")
node === "0x065942c7a3e13329f007894615b14cf2e9026dc7f015dbd561fbb753c6e5cd1d"
```

Artifacts

Provides access to low level data (abi, bytecode etc) for the core smart contracts with the following keys:

  • controller
  • gatedController
  • registry
  • nft
  • resolver

For instance, to access controller abi, you can use artifacts.controller.abi.

Smart contracts

CRSRegistry.sol

Implementation of the CRS Registry, the central contract used to look up resolvers and owners for domains.

BaseNFT.sol

Implementation of ERC721 standard for CRS NFTs, this smart contract is also an owner of CRSRegistry. Protected functions are called through Controller contract, check it's public interface(#NFT controller interface).

CRS Registry interface

The CRS registry is a single central contract that provides a mapping from world names to owners and resolvers, as described in EIP 137.

The CRS operates on 'nodes' instead of human-readable names; a human readable name is converted to a node using the namehash algorithm, which is as follows:

def namehash(name):
  if name == '':
    return '\0' * 32
  else:
    label, _, remainder = name.partition('.')
    return sha3(namehash(remainder) + sha3(label))

The registry's interface is as follows:

owner(bytes32 node) constant returns (address)

Returns the owner of the specified node.

resolver(bytes32 node) constant returns (address)

Returns the resolver for the specified node.

setOwner(bytes32 node, address owner)

Updates the owner of a node. Only the current owner may call this function.

setSubnodeOwner(bytes32 node, bytes32 label, address owner)

Updates the owner of a subnode. For instance, the owner of "foo.com" may change the owner of "bar.foo.com" by calling setSubnodeOwner(namehash("foo.com"), sha3("bar"), newowner). Only callable by the owner of node.

setResolver(bytes32 node, address resolver)

Sets the resolver address for the specified node.

NFT controller interface

Controller smart contract is used to change global NFT settings.

available(string memory _name)

Checks availability of name in namespace.

rentPrice(string memory _name, uint _duration)

Calculate price in ERC20 or gas coin for registering CRS record.

makeCommitment(string memory _name, address _owner, bytes32 _secret)

Generate commitment hash to reserve name for registration, having hidden secret to confirm ownership.

commit(bytes32 _commitment)

Start registration process, with commitment hash returned by makeCommitment.

register(string calldata _name, address _owner, uint _duration, bytes32 _secret)

Finish registration process after commitment maturation, use secret hash from makeCommitment.

renew(string calldata _name, uint _duration)

Extend lease for a currently owned NFT for CRS name.

withdraw(address _paymentToken)

Withdraw registration fees in ERC20 token or gas coin (address(0) as _paymentToken).

adminMaxLeasePeriod(uint256 _maxLeasePeriod)

It's possible to limit the lease period for which registrations are allowed by this controller. By default it's 0, which means unlimited leases. To limit registrations and renewals with 1 year period, you should pass 31536000 a number of seconds in a year as a new _maxLeasePeriod.

Gated controller interface

Extension of NFT controller interface where commitments require validation (whitelisted registration).

commit(string memory _name, bytes32 _commitment, bytes calldata _pass)

Start registration process, with commitment hash returned by makeCommitment and validator signature for your name.

getDomainSeparator()

Read-only constant to generate whitelisting passes.

toggleWhitelist(bool _status)

Pass true to enable commits with a default interface commit(bytes32 _commitment) without whitelisting requirements.

addValidator(address _validator)

Add address whose signature would allow registration of a new names.

removeValidator(address _validator)

Remove previously approved validator address.

Resolver interface

Resolver smart contract is modular, with DefaultResolver having all modules enabled. Projects can implement own resolves using modules from contracts/resolver/profile. It's used as on-chain storage for different kind of data related to specific CRS record.

setApprovalForAll(address operator, bool approved)

Allow / Revoke 3rd party address to change resolver data records, keep in mind that it doesn't affect ProxyConfigResolver which uses permissionless scoped storage.

ProxyConfigResolver

ProxyConfig can be used by any 3rd party to assign proxy smart contract which will resolve into content attached to specific CRS record. It's main difference from the rest of resolvers modules, is that it has scoped storage which doesn't require approval by the CRS owner.

Web3 games can use this module methods to store their own metadata for CRS record. Proxy contract can be as simple as definition of specific getter / setter methods (e.g. tokenURI(uint256), setTokenURI(uint256,string), avatar(uint256), setAvatar(uint256,string)) with local on-chain storage for this data.

proxyConfig(bytes32 node, address controller, bytes4 selector)

Get proxy smart contract for controller and selector. Methods of this contract should be called for final data resolution. When proxy smart contract defines only one getter method, it's recommended to use that method selector (e.g. bytes4(keccak256("tokenURI(uint256)"))). Default proxy contracts should use bytes4(0) selector.

setProxyConfig(bytes32 node, address controller, bytes4 selector, address proxy)

Change address of proxy smart contract with specific metadata attached to CRS record by controller party. Individual proxy contracts can be separated by selector which are either solidity method selectors or by using any other system suitable for controller party. Default proxy contracts should use bytes4(0) selector.

ManagedResolver

Is used for the role management associated with a CRS record. It's a good candidate for managing permissions in a system operated by CRS record.

hasRole(bytes32 _node, bytes4 _roleSig, address _manager) returns (bool)

Check if specific _manager was granted role with _roleSig, signature is generated like bytes4(keccak256("ROLE_NAME")).

setRole(bytes32 _node, bytes4 _roleSig, address _manager, bool _active)

Grant or revoke role with _roleSig to _manager address by passing true or false in _active. Role signature is generated like bytes4(keccak256("ROLE_NAME")).

KeyHashResolver

Is a more flexible version of ContenthashResolver, which supports setting separate hashes for different keys. It's useful when you need to attach several external data pieces to one CRS record and manage them separetely.

keyHash(bytes32 _node, bytes4 _key) returns (bytes32)

Fetch hash associated with the _key. Check setKeyHash for details about _key generation and hash decoding.

setKeyHash(bytes32 _node, bytes4 _key, bytes32 _keyhash)

Associate specific _key with a _keyhash. Key is generated like bytes4(keccak256("KEY_NAME")). Keep in mind that it supports only bytes32 hashes, so you may need to trim static prefixes if IPFS cid is used as a key hash, see examples below:

  • CID v0: QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz -> base58.decode() -> 12207d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89 -> 0x7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89.
  • CID v1: bafybeibozpulxtpv5nhfa2ue3dcjx23ndh3gwr5vwllk7ptoyfwnfjjr4q -> base32.decode(afybeibozpulxtpv5nhfa2ue3dcjx23ndh3gwr5vwllk7ptoyfwnfjjr4q) -> 017012202ecbe8bbcdf5eb4e506a84d8c49beb6d19f66b47b5b2d6afbe6ec16cd2a531e400000000 -> 0x2ecbe8bbcdf5eb4e506a84d8c49beb6d19f66b47b5b2d6afbe6ec16cd2a531e4

NFT interface

Extends ERC721 and Ownable standard contracts. Owner can assign / revoke controllers, change minting fee and update the default resolver contract address.

addController(address _controller)

Add smart contract which can register, renew and use other restricted methods related to CRS management.

removeController(address _controller)

Revoke controller access from smart contract.

setResolver(address _resolver)

Owner can change default resolver smart contract.

setBasenodeResolverSettings(bytes calldata _callData)

Owner can change root domain settings on resolver smart contract.

To change royalties (registration/renew) charged in ERC20 or gas coin, use address(0) for token for gas coin. Individual royalties can be set with forAddress setting, pass address(0) for defaults. callData can be generated in a following way.

```
abi.encodeWithSignature(
    "setRoyalties(address,uint256,address,address)",
    beneficiary,
    amount,
    token,
    forAddress
)
```

To change NFT tokenURI proxy, use the following callData:

```
abi.encodeWithSignature(
    "setProxyConfig(bytes32,address,bytes4,address)",
    baseNode,
    nftContractAddress,
    bytes4(keccak256("tokenURI(uint256,string,string)")), // TOKEN_URI_SELECTOR
    proxyContractAddress
)
```

Getting started

Install packages

$ npm install --dev

Install Hardhat

$ npm install --save-dev hardhat

Launch the local Ethereum client e.g. Ganache or Anvil (recommended):

Integration

Run webpack development server: npx webpack serve --open

Check http://localhost:8080/ for CRS registration UX.

Implementation example entrypoints can be found here: src/index.js and dist/index.html.

Testing

Install anvil as local ethereum node as decribed here.

Run it in cli: anvil;

Run tests with truffle: yarn test.

Verification

To try out Etherscan verification, you first need to deploy a contract to an Ethereum network that's supported by Etherscan, such as Rinkeby.

In this project, copy the .example file to a file named .secret, and then edit it to fill in the details. Enter your Etherscan API key, your Rinkeby node URL (eg from Infura), and the private key of the account which will send the deployment transaction. With a valid .secret file in place, first deploy your contract:

npx hardhat run --network live_goerli scripts/1_deploy_all.js

Then, copy the deployment address and paste it in to replace DEPLOYED_CONTRACT_ADDRESS in this command:

npx hardhat verify --network live_goerli DEPLOYED_CONTRACT_ADDRESS ...CONSTRUCTOR_ARGS

Deployments

Rinkeby

  • CRSRegistry deployed to: 0x2aa24E928d430C7F6a6036129aEf59bA0E107228
  • DefaultResolver deployed to: 0x9D50504b430cc39130AC676695bFE87C5dE09836
  • ReferenceImplementation deployed to: 0x2A7Fd64815176A92E4301665bCA8728452961512
  • ReferenceController deployed to: 0x88bc277E183B3EB196dd341c256Ee67548469a20

Whitelisting private key for ReferenceController: 888fa71d782f31e9d1c952ab74d23a0f8f3f4dc189b8165a94810cf62c805af8

Goerli

  • CRSRegistry deployed to: 0x7372CFF0cCf75db8abC2dEE2A9D86Fd5DADfc372
  • DefaultResolver deployed to: 0x59ad65a908a32D08270B532DfA91D23dcE6d4fe2
  • ReferenceImplementation deployed to: 0x51B2aFC06bee4D5F6a24c3694e2510f326E95E33
  • ReferenceController deployed to: 0x68a2A721CadC85eFa971aD6bC52a8a47B7b3701a

Whitelisting private key for ReferenceController: 888fa71d782f31e9d1c952ab74d23a0f8f3f4dc189b8165a94810cf62c805af8

Polygon

  • CRSRegistry deployed to: 0xDF4D2C1d3F9B960501deFDBDB9f7D32361E4C72F
  • Resolver: 0xc53D1853A87860B8CF9D14dC302A75E6198697B8
0.5.3

1 year ago

0.5.0

1 year ago

0.4.1

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.3.0

2 years ago

0.2.0

2 years ago

0.1.9

2 years ago

0.4.0

2 years ago

0.3.1

2 years ago

0.1.8

2 years ago

0.1.7

2 years ago

0.1.6

2 years ago

0.1.5

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago