Add "Register" module

This commit is contained in:
Alexis Sellier
2021-03-06 17:22:01 +01:00
parent 7a313f640c
commit 9eded4d24b
6 changed files with 412 additions and 58 deletions
+19 -7
View File
@@ -6,6 +6,7 @@
import { session } from './session.js';
import Vesting from './base/vesting/Vesting.svelte';
import Register from './base/register/Register.svelte';
import Header from './Header.svelte';
export let url = "";
@@ -35,11 +36,22 @@
</style>
<svelte:window on:keydown={handleKeydown} />
<div class="app">
<Header/>
<div class="wrapper">
<Router url="{url}">
<Route path="vesting" component={Vesting} />
</Router>
{#await getConfig()}
<!-- Loading wallet -->
{:then config}
<div class="app">
<Header/>
<div class="wrapper">
<Router url="{url}">
<Route path="vesting">
<Vesting {config} />
</Route>
<Route path="register">
<Register {config} />
</Route>
</Router>
</div>
</div>
</div>
{:catch err}
<!-- Show error -->
{/await}
+129
View File
@@ -0,0 +1,129 @@
<script lang="javascript">
import { ethers } from 'ethers';
import { get } from 'svelte/store';
import { STATE, state } from './state.js';
import { error } from '../../error.js';
import { session } from '../../session.js';
import { registrar, registerName, registrationFee } from './registrar.js';
import RegisterButton from './RegisterButton.svelte';
export let config = null;
let subdomain = "";
async function getFee(cfg) {
let fee = await registrationFee(cfg);
return ethers.utils.formatUnits(fee);
}
</script>
<style>
main {
padding-top: 2rem;
align-self: center;
}
input.subdomain {
margin-right: 0;
border-radius-top-right: 0;
border-radius-bottom-right: 0;
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-right: none;
}
input.subdomain[disabled] {
color: var(--color-secondary);
}
.available {
line-height: 1.75em;
padding: 2rem;
}
div.input-caption {
font-size: 1.25rem;
text-align: left;
margin-left: 1.5rem;
padding-left: 1.5rem;
color: var(--color-secondary);
}
div.input-main {
display: flex;
align-items: center;
flex-direction: row;
margin-left: 1.5rem;
color: var(--color-secondary);
}
.domain {
color: var(--color-secondary);
}
.name {
margin: 1rem;
}
.name div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.name input {
margin: 0;
}
span.root {
line-height: 1.5em;
margin: 1rem;
margin-left: 0;
margin-right: 0;
padding: 1rem 2rem;
color: var(--color-secondary);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border: 1px solid var(--color-secondary);
border-left: none;
}
.register {
margin: 1rem;
}
</style>
<main>
{#if $state === STATE.IDLE || $state === STATE.CHECKING_AVAILABILITY}
<div class="input-caption">
Register a <strong>radicle.eth</strong> name
</div>
<div class="input-main">
<span class="name">
<div>
<input
autofocus
placeholder=""
class="subdomain"
disabled={$state === STATE.CHECKING_AVAILABILITY}
type="text" bind:value={subdomain}
/>
<span class="root">.radicle.eth</span>
</div>
</span>
<RegisterButton {subdomain} {config} />
</div>
{:else}
<div class="modal">
<div class="modal-title">
{subdomain}.radicle.eth
</div>
{#if $state === STATE.REGISTERED}
<div class="available">The name <span class="domain">{subdomain}</span> has been successfully registered to {$session.address}.</div>
{:else if $state === STATE.NAME_AVAILABLE}
<div class="available">The name <span class="domain">{subdomain}</span> is available for registration.</div>
{:else if $state === STATE.APPROVING}
<div class="available">
Approving Radicle for {#await getFee(config)}
?
{:then fee}
{fee}
{/await} <strong>RAD</strong>...
</div>
{:else if $state == STATE.NAME_UNAVAILABLE}
<div class="available">The name <span class="domain">{subdomain}</span> is not available for registration.</div>
{/if}
<div class="modal-actions">
<RegisterButton {subdomain} />
</div>
</div>
{/if}
</main>
+90
View File
@@ -0,0 +1,90 @@
<script lang="javascript">
import {get} from 'svelte/store';
import {STATE, state} from './state.js';
import {registrar, registerName} from './registrar.js';
import {error} from '../../error.js';
import {session} from '../../session.js';
import Connect from '../../Connect.svelte';
export let subdomain = "";
export let config = null;
async function register() {
let sess = get(session);
let oldState = get(state);
try {
await registerName(subdomain, sess.address, config);
} catch (e) {
console.error(e);
state.set(oldState);
error.set(e);
}
}
async function checkAvailability() {
registrar(config).available(subdomain).then(isAvailable => {
if (isAvailable) {
state.set(STATE.NAME_AVAILABLE);
} else {
state.set(STATE.NAME_UNAVAILABLE);
}
});
state.set(STATE.CHECKING_AVAILABILITY);
}
function cancel() {
state.set(STATE.IDLE);
error.set(null);
}
</script>
<style>
.cancel {
margin-left: 1rem;
}
</style>
{#if $state >= STATE.NAME_AVAILABLE && $state < STATE.REGISTERED}
{#if $session.address}
<button on:click={register} disabled={$state > STATE.NAME_AVAILABLE} class="primary register">
{#if $state === STATE.APPROVING}
Approving...
{:else if $state === STATE.COMMITTING}
Committing...
{:else if $state === STATE.WAITING_TO_REGISTER}
Waiting...
{:else if $state === STATE.REGISTERING}
Registering...
{:else}
Begin registration &rarr;
{/if}
</button>
{:else}
<Connect caption="Connect to register" className="primary" />
{/if}
<button on:click={cancel} class="cancel text">
Cancel
</button>
{:else if $state === STATE.REGISTERED}
<button on:click={() => state.set(STATE.IDLE)}>
Done
</button>
{:else if $state === STATE.NAME_UNAVAILABLE}
<button on:click={() => state.set(STATE.IDLE)}>
Back
</button>
{:else if subdomain == ""}
<button disabled class="primary register">
Check
</button>
{:else if $state === STATE.CHECKING_AVAILABILITY}
<button disabled class="primary register" data-waiting>
Check
</button>
{:else}
<button on:click={checkAvailability} class="primary register">
Check
</button>
{/if}
+112
View File
@@ -0,0 +1,112 @@
// TODO: Show "look at your wallet" / "confirm tx" before state change.
// TODO: Two registration actions with same label
import { ethers } from "ethers";
import { STATE, state } from './state.js';
import { approveSpender, updateBalance } from '../../session.js';
import { ERROR } from '../../error.js';
const registrarAbi = [
{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"commitment","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"CommitmentMade","type":"event"},
{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"available","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"bytes32","name":"commitment","type":"bytes32"}],"name":"commit","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"commitments","outputs":[{"internalType":"contract Commitments","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"ens","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"minCommitmentAge","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"bytes32","name":"parent","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"}],"name":"namehash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},
{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"register","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"registrationFeeRad","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"valid","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}
];
export function registrar(config) {
return new ethers.Contract(config.registrar.address, registrarAbi, config.provider);
}
export async function registrationFee(config) {
return await registrar(config).registrationFeeRad();
}
export async function registerName(name, owner, config) {
if (! name) return;
let commitmentJson = window.localStorage.getItem('commitment');
let commitment = commitmentJson && JSON.parse(commitmentJson);
// Try to recover an existing commitment.
if (commitment && commitment.name === name && commitment.owner === owner) {
await register(name, owner, commitment.salt, config);
} else {
await approveRegistrar(owner, config);
await commitAndRegister(name, owner, config);
}
}
async function approveRegistrar(owner, config) {
state.set(STATE.APPROVING);
const amount = await registrationFee(config);
await approveSpender(config.registrar.address, amount, config);
}
async function commitAndRegister(name, owner, config) {
let salt = ethers.utils.randomBytes(32);
let minAge = (await registrar(config).minCommitmentAge()).toNumber();
let fee = await registrationFee(config);
await commit(makeCommitment(name, owner, salt), fee, minAge, config);
// Save commitment in local storage in case the user refreshes or closes
// the page.
window.localStorage.setItem('commitment', JSON.stringify({
name: name,
owner: owner,
salt: ethers.utils.hexlify(salt)
}));
await register(name, owner, salt, config);
}
async function commit(commitment, fee, minAge, config) {
state.set(STATE.COMMITTING);
const signer = config.provider.getSigner();
const tx = await registrar(config)
.connect(signer)
.commit(commitment, { gasLimit: 150000 })
.catch(e => console.error(e));
await tx.wait(1);
updateBalance(fee.mul(-1));
// TODO: Getting "commitment too new"
state.set(STATE.WAITING_TO_REGISTER);
await tx.wait(minAge + 1);
}
async function register(name, owner, salt, config) {
state.set(STATE.REGISTERING);
const signer = config.provider.getSigner();
const tx = await registrar(config).connect(signer).register(
name, owner, ethers.BigNumber.from(salt), { gasLimit: 150000 }
);
console.log("Sent", tx);
try {
await tx.wait();
window.localStorage.clear();
state.set(STATE.REGISTERED);
} catch (e) {
throw { type: ERROR.TRANSACTION_FAILED, hash: tx.hash };
}
}
function makeCommitment(name, owner, salt) {
let bytes = ethers.utils.concat([
ethers.utils.toUtf8Bytes(name),
ethers.utils.getAddress(owner),
ethers.BigNumber.from(salt),
]);
return ethers.utils.keccak256(bytes);
}
+16
View File
@@ -0,0 +1,16 @@
import { derived, writable } from "svelte/store";
export const STATE = {
ERROR: -1,
IDLE: 0,
CHECKING_AVAILABILITY: 1,
NAME_UNAVAILABLE: 2,
NAME_AVAILABLE: 3,
APPROVING: 4,
COMMITTING: 5,
WAITING_TO_REGISTER: 6,
REGISTERING: 7,
REGISTERED: 8,
};
export const state = writable(STATE.IDLE);
+46 -51
View File
@@ -2,9 +2,10 @@
import { ethers } from 'ethers';
import { get } from 'svelte/store';
import { STATE, state } from './state.js';
import { getConfig } from '../../config.js';
import { getInfo } from './vesting.js';
export let config = null;
let contractAddress = "";
let info = null;
@@ -33,57 +34,51 @@
}
</style>
{#await getConfig()}
<!-- Loading wallet -->
{:then config}
<main>
{#if info}
<div class="modal">
<div class="modal-title">
{contractAddress}
</div>
<div class="modal-body">
<table>
<tr><td class="label">Beneficiary</td><td>{info.beneficiary}</td></tr>
<tr><td class="label">Allocation</td><td>{info.totalVesting} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawn</td><td>{info.withdrawn} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawable</td><td>{info.withdrawableBalance} <strong>{info.symbol}</strong></td></tr>
</table>
</div>
<div class="modal-actions">
<button on:click={() => info = null} class="small">
Back
</button>
</div>
<main>
{#if info}
<div class="modal">
<div class="modal-title">
{contractAddress}
</div>
{:else}
<div class="input-caption">
Enter your Radicle <strong>vesting contract</strong> address
<div class="modal-body">
<table>
<tr><td class="label">Beneficiary</td><td>{info.beneficiary}</td></tr>
<tr><td class="label">Allocation</td><td>{info.totalVesting} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawn</td><td>{info.withdrawn} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawable</td><td>{info.withdrawableBalance} <strong>{info.symbol}</strong></td></tr>
</table>
</div>
<div class="input-main">
<span class="name">
<div>
<input
autofocus
size="40"
placeholder=""
class="subdomain"
disabled={$state === STATE.LOADING}
type="text" bind:value={contractAddress}
/>
</div>
</span>
<button
on:click={() => loadContract(config)}
class="primary"
data-waiting={$state === STATE.LOADING || null}
disabled={$state === STATE.LOADING}
>
Load
<div class="modal-actions">
<button on:click={() => info = null} class="small">
Back
</button>
</div>
{/if}
</main>
{:catch error}
Ethereum wallet not available.
{/await}
</div>
{:else}
<div class="input-caption">
Enter your Radicle <strong>vesting contract</strong> address
</div>
<div class="input-main">
<span class="name">
<div>
<input
autofocus
size="40"
placeholder=""
class="subdomain"
disabled={$state === STATE.LOADING}
type="text" bind:value={contractAddress}
/>
</div>
</span>
<button
on:click={() => loadContract(config)}
class="primary"
data-waiting={$state === STATE.LOADING || null}
disabled={$state === STATE.LOADING}
>
Load
</button>
</div>
{/if}
</main>