mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
Button to transfer org ownership
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import Modal from '@app/Modal.svelte';
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// TODO: Fix this when there's a modal.
|
||||
if (event.key === 'Enter') {
|
||||
(document.querySelector('button.primary') as HTMLElement).click();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
margin: auto 0;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
cursor: wait;
|
||||
}
|
||||
.spinner.center {
|
||||
margin: auto auto;
|
||||
|
||||
+9
-2
@@ -2,6 +2,7 @@
|
||||
export let floating = false;
|
||||
export let error = false;
|
||||
export let subtle = false;
|
||||
export let small = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -57,6 +58,9 @@
|
||||
}
|
||||
.modal-subtitle {
|
||||
color: var(--color-secondary);
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.modal-body {
|
||||
overflow-x: hidden;
|
||||
@@ -68,6 +72,9 @@
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-small .modal-subtitle {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if floating}
|
||||
@@ -75,14 +82,14 @@
|
||||
{/if}
|
||||
|
||||
<div class:modal-floating={floating}>
|
||||
<div class="modal" class:error class:modal-subtle={subtle}>
|
||||
<div class="modal" class:error class:modal-subtle={subtle} class:modal-small={small}>
|
||||
<div class="modal-title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="modal-subtitle">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>
|
||||
{#if $$slots.body}
|
||||
{#if $$slots.body && !small}
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ const orgFactoryAbi = [
|
||||
|
||||
const orgAbi = [
|
||||
"function owner() view returns (address)",
|
||||
"function setOwner(address)",
|
||||
"function setName(string, address) returns (bytes32)",
|
||||
];
|
||||
|
||||
@@ -43,6 +44,17 @@ export class Org {
|
||||
{ gasLimit: 200_000 });
|
||||
}
|
||||
|
||||
async setOwner(address: string, config: Config): Promise<TransactionResponse> {
|
||||
assert(config.signer);
|
||||
|
||||
const org = new ethers.Contract(
|
||||
this.address,
|
||||
orgAbi,
|
||||
config.signer
|
||||
);
|
||||
return org.setOwner(address);
|
||||
}
|
||||
|
||||
static fromReceipt(receipt: ContractReceipt): Org | null {
|
||||
let event = receipt.events?.find(e => e.event === 'OrgCreated');
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="typescript">
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import Modal from '@app/Modal.svelte';
|
||||
import type { Config } from '@app/config';
|
||||
import { formatAddress } from '@app/utils';
|
||||
import Loading from '@app/Loading.svelte';
|
||||
import { assert } from '@app/error';
|
||||
import * as utils from '@app/utils';
|
||||
|
||||
import type { Org } from './Org';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let org: Org;
|
||||
export let config: Config;
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
Signing,
|
||||
Pending,
|
||||
Success,
|
||||
Failed,
|
||||
}
|
||||
|
||||
let newOwner: string | null = null;
|
||||
let state = State.Idle;
|
||||
let error: string | null = null;
|
||||
let input: HTMLInputElement | null = null;
|
||||
|
||||
onMount(() => {
|
||||
input && input.focus();
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
state = State.Idle;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
assert(newOwner);
|
||||
|
||||
if (! utils.isAddress(newOwner)) {
|
||||
state = State.Failed;
|
||||
error = `"${newOwner}" is not a valid Ethereum address.`;
|
||||
return;
|
||||
}
|
||||
|
||||
state = State.Signing;
|
||||
try {
|
||||
let tx = await org.setOwner(newOwner, config);
|
||||
state = State.Pending;
|
||||
await tx.wait();
|
||||
state = State.Success;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
state = State.Failed;
|
||||
error = e.message;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if state === State.Success}
|
||||
<Modal floating small>
|
||||
<div slot="title">
|
||||
✅
|
||||
</div>
|
||||
|
||||
<div slot="subtitle">
|
||||
The ownership of <strong>{formatAddress(org.address)}</strong> was
|
||||
successfully transfered to <strong>{newOwner}</strong>.
|
||||
</div>
|
||||
|
||||
<div slot="actions">
|
||||
<button class="small" on:click={() => dispatch('close')}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
{:else}
|
||||
<Modal floating error={state == State.Failed} small={state == State.Failed}>
|
||||
<div slot="title">
|
||||
🔑
|
||||
<div>Transfer ownership</div>
|
||||
</div>
|
||||
|
||||
<div slot="subtitle">
|
||||
{#if state == State.Signing}
|
||||
Please confirm the transaction in your wallet.
|
||||
{:else if state == State.Pending}
|
||||
Transaction is being processed by the network...
|
||||
{:else if state == State.Idle}
|
||||
Transfer the ownership of Org <strong>{formatAddress(org.address)}</strong> to a new address.
|
||||
{:else if state == State.Failed}
|
||||
<div class="error">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div slot="body">
|
||||
{#if state == State.Idle}
|
||||
<input type="text" size="40" disabled={state !== State.Idle} bind:this={input} bind:value={newOwner} />
|
||||
{:else if state == State.Pending || state == State.Signing}
|
||||
<Loading small center />
|
||||
{:else if state == State.Failed}
|
||||
<!-- ... -->
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div slot="actions">
|
||||
{#if state == State.Signing}
|
||||
<button class="small" on:click={() => dispatch('close')}>
|
||||
Cancel
|
||||
</button>
|
||||
{:else if state == State.Pending}
|
||||
<button class="small" on:click={() => dispatch('close')}>
|
||||
Close
|
||||
</button>
|
||||
{:else if state == State.Failed}
|
||||
<button class="small" on:click={resetForm}>
|
||||
Back
|
||||
</button>
|
||||
{:else}
|
||||
<button class="primary" on:click={onSubmit} disabled={!newOwner || state !== State.Idle}>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<button class="text" on:click={() => dispatch('close')}>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
@@ -7,7 +7,6 @@
|
||||
import type { Registration } from '@app/base/registrations/registrar';
|
||||
import { getRegistration } from '@app/base/registrations/registrar';
|
||||
import { parseEnsLabel, explorerLink } from '@app/utils';
|
||||
import { Org } from './Org';
|
||||
import { session } from '@app/session';
|
||||
import Loading from '@app/Loading.svelte';
|
||||
import Modal from '@app/Modal.svelte';
|
||||
@@ -16,6 +15,9 @@
|
||||
import SetName from '@app/ens/SetName.svelte';
|
||||
import * as utils from '@app/utils';
|
||||
|
||||
import { Org } from './Org';
|
||||
import TransferOwnership from './TransferOwnership.svelte';
|
||||
|
||||
export let address: string;
|
||||
export let config: Config;
|
||||
|
||||
@@ -34,6 +36,11 @@
|
||||
setNameForm = SetName;
|
||||
};
|
||||
|
||||
let transferOwnerForm: typeof SvelteComponent | null = null;
|
||||
const transferOwnership = () => {
|
||||
transferOwnerForm = TransferOwnership;
|
||||
};
|
||||
|
||||
$: label = name && parseEnsLabel(name, config);
|
||||
$: isOwner = (org: Org): boolean => {
|
||||
return org.safe === ($session && $session.address);
|
||||
@@ -61,8 +68,8 @@
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8fr;
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: 1fr 4fr 2fr;
|
||||
grid-gap: 1rem 2rem;
|
||||
}
|
||||
.fields > div {
|
||||
justify-self: start;
|
||||
@@ -119,8 +126,18 @@
|
||||
</header>
|
||||
|
||||
<div class="fields">
|
||||
<div class="label">Address</div><div>{org.address}</div>
|
||||
<!-- Address -->
|
||||
<div class="label">Address</div><div>{org.address}</div><div></div>
|
||||
<!-- Owner -->
|
||||
<div class="label">Owner</div><div>{org.safe}</div>
|
||||
<div>
|
||||
{#if isOwner(org)}
|
||||
<button class="tiny secondary" on:click={transferOwnership}>
|
||||
Transfer
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Name -->
|
||||
<div class="label">Name</div>
|
||||
<div>
|
||||
{#await org.lookupAddress(config)}
|
||||
@@ -128,15 +145,18 @@
|
||||
{:then name}
|
||||
{#if name}
|
||||
<Link to={`/registrations/${label}`}>{name}</Link>
|
||||
{:else if isOwner(org)}
|
||||
<button class="tiny primary" on:click={setName}>
|
||||
Set
|
||||
</button>
|
||||
{:else}
|
||||
<span class="subtle">Not set</span>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div>
|
||||
{#if isOwner(org)}
|
||||
<button class="tiny primary" on:click={setName}>
|
||||
Set
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{:else}
|
||||
@@ -160,6 +180,7 @@
|
||||
</Modal>
|
||||
{/if}
|
||||
<svelte:component this={setNameForm} {org} {config} on:close={() => setNameForm = null} />
|
||||
<svelte:component this={transferOwnerForm} {org} {config} on:close={() => transferOwnerForm = null} />
|
||||
{:catch err}
|
||||
<Error error={err} />
|
||||
{/await}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<div slot="subtitle">
|
||||
{#if mismatchError}
|
||||
<div class="rror">
|
||||
<div class="error">
|
||||
The name <strong>{name}.{config.registrar.domain}</strong> does not
|
||||
resolve to <strong>{formatAddress(org.address)}</strong>. Please update
|
||||
The ENS record for {name}.{config.registrar.domain} to
|
||||
|
||||
Reference in New Issue
Block a user