mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-03 18:50:30 +08:00
Add "Register" module
This commit is contained in:
+19
-7
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 →
|
||||
{/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}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user