Users can specify anchors address in ENS

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier
2021-10-11 18:42:33 +02:00
parent 08d7ee6a29
commit 18b28010bb
11 changed files with 102 additions and 41 deletions
+13 -7
View File
@@ -27,9 +27,9 @@
export let config: Config;
export let path: string;
export let profile: Profile | null = null;
export let anchors: string | null = null;
export let org: string | null = null;
const orgAddress = org ?? undefined;
export let user: string | null = null;
// When the component is loaded the first time, the blob is yet to be loaded.
let state: State = { status: Status.Idle };
@@ -61,7 +61,13 @@
const blob = await loadBlob(path);
getBlob = new Promise(resolve => resolve(blob));
navigate(proj.path({ urn, org: orgAddress, commit, path }));
if (org) {
navigate(proj.path({ urn, org, commit, path }));
} else if (user) {
navigate(proj.path({ urn, user, commit, path }));
} else {
navigate(proj.path({ urn, commit, path }));
}
};
const fetchTree = async (path: string) => {
@@ -71,7 +77,7 @@
// This is reactive to respond to path changes that don't originate from this
// component, eg. when using the browser's "back" button.
$: getBlob = loadBlob(path);
$: getAnchor = orgAddress ? Org.getAnchor(orgAddress, urn, config) : null;
$: getAnchor = anchors ? Org.getAnchor(anchors, urn, config) : null;
$: loadingPath = state.status == Status.Loading ? state.path : null;
</script>
@@ -244,16 +250,16 @@
{:then tree}
<header>
<div class="commit">
commit {commit}
{commit}
</div>
<div class="anchor">
{#if orgAddress}
{#if anchors}
{#await getAnchor}
<Loading small margins />
{:then anchor}
{#if anchor === commit}
<span class="anchor-widget">
<span class="anchor-label" title="{orgAddress}">anchored 🔒</span>
<span class="anchor-label" title="{anchors}">anchored 🔒</span>
</span>
{:else}
<span class="anchor-widget not-anchored">
+14
View File
@@ -31,3 +31,17 @@
<Route path="/orgs/:org/projects/:urn" let:params>
<View {config} org={params.org} urn={params.urn} path="/" />
</Route>
<!-- With a User context -->
<Route path="/users/:user/projects/:urn/head/*" let:params>
<View {config} user={params.user} urn={params.urn} path={params['*'] || "/"} />
</Route>
<Route path="/users/:user/projects/:urn/:commit/*" let:params>
<View {config} user={params.user} urn={params.urn} commit={params.commit} path={params["*"] || "/"} />
</Route>
<Route path="/users/:user/projects/:urn" let:params>
<View {config} user={params.user} urn={params.urn} path="/" />
</Route>
+15 -10
View File
@@ -6,7 +6,7 @@
import Modal from '@app/Modal.svelte';
import Avatar from '@app/Avatar.svelte';
import { Org } from '@app/base/orgs/Org';
import type { Profile } from '@app/profile';
import { Profile, ProfileType } from '@app/profile';
import type { Info } from '@app/project';
import { formatOrg, isAddressEqual } from '@app/utils';
@@ -14,6 +14,7 @@
export let urn: string;
export let org = "";
export let user = "";
export let commit = "";
export let config: Config;
export let path: string;
@@ -24,22 +25,24 @@
let getProject = new Promise<Profile | null>(resolve => {
if (org) {
Org.getProjectProfile(org, config).then(p => resolve(p));
} else if (user) {
Profile.get(user, ProfileType.Project, config).then(p => resolve(p));
} else {
resolve(null);
}
}).then(async (orgProfile) => {
const seedHost = orgProfile?.seedHost;
const seedId = orgProfile?.seedId || undefined;
}).then(async (profile) => {
const seedHost = profile?.seedHost;
const seedId = profile?.seedId || undefined;
const cfg = seedHost ? config.withSeed(seedHost, seedId) : config;
const info = await proj.getInfo(urn, cfg);
projectInfo = info;
return { project: info, config: cfg, org: orgProfile };
return { project: info, config: cfg, profile };
});
const parentUrl = (profile: Profile) => {
return org === profile.name || isAddressEqual(org, profile.address)
return org
? `/orgs/${profile.nameOrAddress}`
: `/users/${profile.nameOrAddress}`;
};
@@ -111,9 +114,9 @@
{:then result}
<header>
<div class="title bold">
{#if result.org}
<a class="org-avatar" title={result.org.nameOrAddress} href={parentUrl(result.org)}>
<Avatar source={result.org.avatar || result.org.address} address={result.org.address}/>
{#if result.profile}
<a class="org-avatar" title={result.profile.nameOrAddress} href={parentUrl(result.profile)}>
<Avatar source={result.profile.avatar || result.profile.address} address={result.profile.address}/>
</a>
<span class="divider">/</span>
{/if}
@@ -122,7 +125,9 @@
<div class="urn">{urn}</div>
<div class="description">{result.project.meta.description}</div>
</header>
<Browser {urn} {org} profile={result.org} {path}
<Browser {urn} {org} {user} {path}
anchors={result.profile?.projectAnchors ?? org}
profile={result.profile}
commit={commit || result.project.head}
config={result.config} />
{:catch}
+3 -1
View File
@@ -15,7 +15,8 @@
export let project: proj.Project;
export let config: Config;
export let org: string | undefined;
export let org: string | undefined = undefined;
export let user: string | undefined = undefined;
let state: State = { status: Status.Loading };
let info: proj.Info | null = null;
@@ -36,6 +37,7 @@
proj.path({
urn: project.id,
org,
user,
commit: project.anchor.stateHash,
})
);
+3
View File
@@ -86,6 +86,9 @@
{ name: "seed.id", label: "Seed ID", validate: "id", placeholder: "hynkyndc6w3p8urucakobzncqny7xxtw88...",
description: "The Device ID of a Radicle Link node that hosts entities associated with this name.",
value: r.profile.seedId, editable: true },
{ name: "project.anchors", label: "Anchors", validate: "address", placeholder: "Ethereum address, eg. 0x4a9cf21...bc91...",
description: "The alternative Ethereum address under which associated project anchors can be found.",
value: r.profile.projectAnchors, editable: true },
];
state = { status: Status.Found, registration: r, owner };
} else {
+14 -1
View File
@@ -20,6 +20,7 @@ export interface EnsProfile {
address: string | null;
seedId: string | null;
seedHost: string | null;
projectAnchors: string | null;
url: string | null;
avatar: string | null;
twitter: string | null;
@@ -74,11 +75,12 @@ export async function getRegistration(name: string, config: Config, resolver?: E
resolver.getText('url'),
resolver.getText('eth.radicle.seed.id'),
resolver.getText('eth.radicle.seed.host'),
resolver.getText('eth.radicle.project.anchors'),
resolver.getText('com.twitter'),
resolver.getText('com.github'),
]);
const [address, avatar, url, seedId, seedHost, twitter, github] =
const [address, avatar, url, seedId, seedHost, projectAnchors, twitter, github] =
meta.map(r => r.status == "fulfilled" ? r.value : null);
return {
@@ -89,6 +91,7 @@ export async function getRegistration(name: string, config: Config, resolver?: E
avatar,
seedId,
seedHost,
projectAnchors,
address,
twitter,
github,
@@ -126,6 +129,16 @@ export async function getSeedId(name: string, config: Config, resolver?: EnsReso
return resolver.getText('eth.radicle.seed.id');
}
export async function getProjectAnchorsAddress(name: string, config: Config, resolver?: EnsResolver): Promise<string | null> {
name = name.toLowerCase();
resolver = resolver ?? await config.provider.getResolver(name);
if (! resolver) {
return null;
}
return resolver.getText('eth.radicle.project.anchors');
}
export function registrar(config: Config): ethers.Contract {
return new ethers.Contract(config.registrar.address, config.abi.registrar, config.provider);
}
+1
View File
@@ -36,6 +36,7 @@ export async function setRecords(name: string, records: EnsRecord[], resolver: E
break;
case "seed.id":
case "seed.host":
case "project.anchors":
calls.push(
iface.encodeFunctionData("setText", [node, "eth.radicle." + r.name, r.value])
);
+26 -18
View File
@@ -104,8 +104,14 @@
<div class="fields">
<!-- Address -->
<div class="label">Address</div>
<div><Address noAvatar {config} address={profile.address} /></div>
<div><Address {config} address={profile.address} /></div>
<div></div>
<!-- Project anchors -->
{#if profile.projectAnchors}
<div class="label">Anchors</div>
<div><Address {config} address={profile.projectAnchors} /></div>
<div></div>
{/if}
<!-- Profile -->
<div class="label">Profile</div>
<div>
@@ -117,23 +123,25 @@
</div>
</div>
<div class="projects">
{#await Org.getOrgsByOwner(profile.address, config)}
<Loading center fadeIn />
{:then orgs}
{#each orgs as org}
{#await org.getProjects(config) then projects}
{#each projects as project}
<div class="project">
<Project {project} org={org.address} config={profile.config(config)} />
</div>
{/each}
{:catch err}
<Message error>
<strong>Error: </strong> failed to load projects: {err.message}.
</Message>
{/await}
{/each}
{/await}
{#if profile.projectAnchors}
{#await Org.get(profile.projectAnchors, config)}
<Loading center fadeIn />
{:then org}
{#if org}
{#await org.getProjects(config) then projects}
{#each projects as project}
<div class="project">
<Project {project} user={addressOrName} config={profile.config(config)} />
</div>
{/each}
{:catch err}
<Message error>
<strong>Error: </strong> failed to load projects: {err.message}.
</Message>
{/await}
{/if}
{/await}
{/if}
</div>
</main>
{/await}
+5
View File
@@ -84,6 +84,11 @@ export class Profile {
return this.profile?.ens?.seedId ?? undefined;
}
// Using undefined as return type if nothing to be returned since it works better with <a href> links
get projectAnchors(): string | undefined {
return this.profile?.ens?.projectAnchors ?? undefined;
}
// Get the name, and if not available, the address.
get nameOrAddress(): string {
return this.name ?? this.address;
+4 -2
View File
@@ -107,13 +107,15 @@ export async function getReadme(
}
export function path(
opts: { urn: string; org?: string; commit?: string; path?: string }
opts: { urn: string; org?: string; user?: string; commit?: string; path?: string }
): string {
const { urn, org, commit, path } = opts;
const { urn, org, user, commit, path } = opts;
const result = [];
if (org) {
result.push("orgs", org);
} else if (user) {
result.push("users", user);
}
result.push("projects", urn);
+4 -2
View File
@@ -7,7 +7,7 @@ import type { Config } from '@app/config';
import config from "@app/config.json";
import { assert } from '@app/error';
import type { EnsProfile } from "@app/base/registrations/registrar";
import { getAvatar, getSeedHost, getSeedId, getRegistration } from '@app/base/registrations/registrar';
import { getAvatar, getSeedHost, getSeedId, getProjectAnchorsAddress, getRegistration } from '@app/base/registrations/registrar';
import type { BasicProfile } from "@ceramicstudio/idx-constants";
import { ProfileType } from '@app/profile';
@@ -301,12 +301,13 @@ export async function resolveEnsProfile(addressOrName: string, profileType: Prof
if (profileType === ProfileType.Project) {
promises.push(getSeedHost(name, config, resolver));
promises.push(getSeedId(name, config, resolver));
promises.push(getProjectAnchorsAddress(name, config, resolver));
} else if (profileType === ProfileType.Minimal) {
promises.push(Promise.resolve(null));
}
const project = await Promise.allSettled(promises);
const [avatar, address, seedHost, seedId] =
const [avatar, address, seedHost, seedId, projectAnchors] =
project.map(r => r.status == "fulfilled" ? r.value : null);
return {
@@ -315,6 +316,7 @@ export async function resolveEnsProfile(addressOrName: string, profileType: Prof
address,
seedHost,
seedId,
projectAnchors,
url: null,
twitter: null,
github: null,