diff --git a/src/App.svelte b/src/App.svelte
index c7b9da527..dd3d04831 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -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 @@
-
-
-
-
-
-
+{#await getConfig()}
+
+{:then config}
+
-
+{:catch err}
+
+{/await}
diff --git a/src/base/register/Register.svelte b/src/base/register/Register.svelte
new file mode 100644
index 000000000..890ecc128
--- /dev/null
+++ b/src/base/register/Register.svelte
@@ -0,0 +1,129 @@
+
+
+
+
+
+ {#if $state === STATE.IDLE || $state === STATE.CHECKING_AVAILABILITY}
+
+ Register a radicle.eth name
+
+
+
+
+
+ .radicle.eth
+
+
+
+
+ {:else}
+
+
+ {subdomain}.radicle.eth
+
+ {#if $state === STATE.REGISTERED}
+
The name {subdomain} has been successfully registered to {$session.address}.
+ {:else if $state === STATE.NAME_AVAILABLE}
+
The name {subdomain} is available for registration.
+ {:else if $state === STATE.APPROVING}
+
+ Approving Radicle for {#await getFee(config)}
+ ?
+ {:then fee}
+ {fee}
+ {/await} RAD...
+
+ {:else if $state == STATE.NAME_UNAVAILABLE}
+
The name {subdomain} is not available for registration.
+ {/if}
+
+
+
+
+ {/if}
+
diff --git a/src/base/register/RegisterButton.svelte b/src/base/register/RegisterButton.svelte
new file mode 100644
index 000000000..efdee86c4
--- /dev/null
+++ b/src/base/register/RegisterButton.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+{#if $state >= STATE.NAME_AVAILABLE && $state < STATE.REGISTERED}
+ {#if $session.address}
+
+ {:else}
+
+ {/if}
+
+{:else if $state === STATE.REGISTERED}
+
+{:else if $state === STATE.NAME_UNAVAILABLE}
+
+{:else if subdomain == ""}
+
+{:else if $state === STATE.CHECKING_AVAILABILITY}
+
+{:else}
+
+{/if}
diff --git a/src/base/register/registrar.js b/src/base/register/registrar.js
new file mode 100644
index 000000000..6b4b85d19
--- /dev/null
+++ b/src/base/register/registrar.js
@@ -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);
+}
diff --git a/src/base/register/state.js b/src/base/register/state.js
new file mode 100644
index 000000000..5c3c85195
--- /dev/null
+++ b/src/base/register/state.js
@@ -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);
diff --git a/src/base/vesting/Vesting.svelte b/src/base/vesting/Vesting.svelte
index 98a145ff3..0e1dc3fb0 100644
--- a/src/base/vesting/Vesting.svelte
+++ b/src/base/vesting/Vesting.svelte
@@ -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 @@
}
-{#await getConfig()}
-
-{:then config}
-
- {#if info}
-
-
- {contractAddress}
-
-
-
- | Beneficiary | {info.beneficiary} |
- | Allocation | {info.totalVesting} {info.symbol} |
- | Withdrawn | {info.withdrawn} {info.symbol} |
- | Withdrawable | {info.withdrawableBalance} {info.symbol} |
-
-
-
-
-
+
+ {#if info}
+
+
+ {contractAddress}
- {:else}
-