Remove deprecated org parts

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2022-09-13 10:31:28 +02:00
committed by Alexis Sellier
parent 538c6d90c1
commit 271fcde43f
22 changed files with 15 additions and 1456 deletions
-4
View File
@@ -6,8 +6,6 @@
import Home from "@app/base/home/Index.svelte";
import Vesting from "@app/base/vesting/Index.svelte";
import Registrations from "@app/base/registrations/Routes.svelte";
import Orgs from "@app/base/orgs/Routes.svelte";
import Users from "@app/base/users/Routes.svelte";
import Seeds from "@app/base/seeds/Routes.svelte";
import Faucet from "@app/base/faucet/Routes.svelte";
import Projects from "@app/base/projects/Routes.svelte";
@@ -92,10 +90,8 @@
<Vesting {config} session={$session} />
</Route>
<Registrations {config} session={$session} />
<Orgs />
<Seeds {config} session={$session} />
<Faucet {config} />
<Users />
<Resolver {config} />
<Route path="/:addressOrName" let:params>
<Profile addressOrName={params.addressOrName} {config} />
-113
View File
@@ -1,113 +0,0 @@
<script lang="ts">
import { Link } from "svelte-routing";
import Avatar from "@app/Avatar.svelte";
import type { Profile } from "@app/profile";
import type { Config } from "@app/config";
import { formatName, formatAddress } from "@app/utils";
import type { Seed } from "@app/base/seeds/Seed";
export let profile:
| Profile
| {
address: string;
avatar?: string;
name?: string;
}
| null = null;
export let seed: Seed | null = null;
export let config: Config;
export let path: string;
export let members: string[] = [];
</script>
<style>
.card {
width: 12rem;
height: 10.5rem;
margin-right: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
padding: 1rem;
display: inline-block;
text-align: center;
border-radius: var(--border-radius);
}
.card::last-child {
margin-right: 0;
}
.card:hover {
background: var(--color-foreground-background-lighter);
}
.card-avatar {
margin: 0 auto;
text-align: center;
width: 4rem;
height: 4rem;
}
.card-label {
color: var(--color-foreground-90);
display: inline-block;
font-weight: var(--font-weight-medium);
margin-top: 0.75rem;
border-radius: var(--border-radius);
padding: 0rem;
text-overflow: ellipsis;
overflow-x: hidden;
}
.card-members {
font-size: var(--font-size-small);
color: var(--color-foreground-faded);
}
.card.seed {
width: 14rem;
}
.card.seed .card-label {
max-width: 12rem;
}
.card.seed .seed-emoji {
font-size: 3rem;
}
@media (max-width: 720px) {
.card {
margin-right: 0;
}
}
</style>
<Link to={path}>
<div class="card" class:seed={!!seed}>
<div class="card-avatar">
{#if profile}
<Avatar
source={profile.avatar ?? profile.address}
title={profile.address} />
{:else if seed}
<span class="seed-emoji">
{seed.emoji}
</span>
{/if}
</div>
<div class="card-label">
{#if profile}
{#if profile.name}
{formatName(profile.name, config)}
{:else}
{formatAddress(profile.address)}
{/if}
{:else if seed}
{seed.host}
{/if}
</div>
<div class="card-members">
{#if profile}
{#if members.length > 0}
{members.length} member(s)
{:else}
{formatAddress(profile.address)}
{/if}
{/if}
</div>
</div>
</Link>
-71
View File
@@ -1,71 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import { Profile, ProfileType } from "@app/profile";
import Card from "@app/Card.svelte";
import type { Org } from "@app/base/orgs/Org";
import type { Config } from "@app/config";
import type { Seed } from "@app/base/seeds/Seed";
export let config: Config;
export let orgs: Org[] = [];
export let profiles: Profile[] = [];
export let seeds: Seed[] = [];
const orgMembers: Record<string, string[]> = {};
onMount(async () => {
const promises = orgs.map(org => {
org.getMembers(config).then(members => {
orgMembers[org.address] = members;
});
});
Promise.all(promises);
});
</script>
<style>
.list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
@media (max-width: 720px) {
.list {
justify-content: center;
}
}
</style>
<div class="list">
{#each orgs as org}
{#await Profile.get(org.name ?? org.address, ProfileType.Minimal, config)}
<Card
profile={{ address: org.address }}
{config}
path={`/${org.address}`} />
{:then profile}
{#if orgMembers[profile.address]?.length}
<Card
{profile}
{config}
path={`/${profile.nameOrAddress}`}
members={orgMembers[profile.address]} />
{:else}
<Card {profile} {config} path={`/${profile.nameOrAddress}`} />
{/if}
{/await}
{/each}
{#each profiles as profile}
<Card {profile} {config} path={`/${profile.nameOrAddress}`} />
{/each}
{#each seeds as seed}
<Card {seed} {config} path={`/seeds/${seed.host}`} />
{/each}
{#if !orgs.length && !profiles.length && !seeds.length}
<slot />
{/if}
</div>
-92
View File
@@ -1,92 +0,0 @@
<script lang="ts">
import { marked } from "marked";
import { createEventDispatcher } from "svelte";
import { markdownExtensions as extensions } from "@app/utils";
export let options: {
label: string;
value: string;
description?: string[];
}[];
export let name: string;
export let selected = "";
export let disabled = false;
marked.use({ extensions });
const dispatch = createEventDispatcher();
</script>
<style>
main {
display: flex;
justify-content: center;
}
.options {
text-align: left;
}
.options label {
display: block;
margin-bottom: 1rem;
line-height: 1.5rem;
width: 100%;
}
.options label:last-child {
margin-bottom: 0.5rem;
}
.options label,
.options input {
cursor: pointer;
}
.options input {
margin-right: 0.75rem;
display: inline-block;
}
.options .description {
color: var(--color-secondary);
font-size: var(--font-size-small);
margin-bottom: 2rem;
}
.options .description:last-child {
margin-bottom: 0;
}
.description :global(a) {
text-decoration: underline;
}
.description :global(em) {
font-style: italic;
}
.description :global(strong) {
font-weight: var(--font-weight-medium);
}
.desktop {
display: block !important;
}
@media (max-width: 720px) {
.desktop {
display: none !important;
}
}
</style>
<main>
<div class="options">
{#each options as option}
<label for={option.value}>
<input
type="radio"
{disabled}
checked={selected === option.value}
{name}
id={option.value}
value={option.value}
on:click={() => dispatch("changed", option.value)} />
{option.label}
</label>
{#if option.description}
<div class="description desktop">
{@html marked(option.description.join("\n"))}
</div>
{/if}
{/each}
</div>
</main>
+3 -8
View File
@@ -6,7 +6,7 @@
import Icon from "@app/Icon.svelte";
import SetName from "@app/ens/SetName.svelte";
import SeedAddress from "@app/SeedAddress.svelte";
import TransferOwnership from "@app/base/orgs/TransferOwnership.svelte";
import TransferOwnership from "@app/components/TransferOwnership.svelte";
import Link from "@app/Link.svelte";
import { getBalance, Profile, ProfileType } from "@app/profile";
import Loading from "@app/Loading.svelte";
@@ -16,7 +16,7 @@
import Message from "@app/Message.svelte";
import Error from "@app/Error.svelte";
import { User } from "@app/base/users/User";
import Projects from "@app/base/orgs/View/Projects.svelte";
import Projects from "@app/base/seeds/View/Projects.svelte";
import { MissingReverseRecord, NotFoundError } from "@app/error";
import NotFound from "@app/NotFound.svelte";
import RadicleUrn from "@app/RadicleUrn.svelte";
@@ -448,12 +448,7 @@
{/if}
{#if profile.seed?.valid}
<Async fetch={profile.seed.getProjects(10, profile.id)} let:result>
<Projects
{profile}
seed={profile.seed}
{account}
projects={result}
{config} />
<Projects {profile} seed={profile.seed} projects={result} />
</Async>
{/if}
</main>
-34
View File
@@ -1,34 +0,0 @@
import { ethers } from "ethers";
import type { Config } from "./config";
import { decodeRadicleId, formatProjectHash, querySubgraph } from "@app/utils";
const GetAllAnchors = `
query GetAllAnchors($project: Bytes!, $org: ID!) {
anchors(orderBy: timestamp, orderDirection: desc, where: { objectId: $project, org: $org }) {
multihash
timestamp
}
}
`;
interface AnchorObject {
timestamp: number;
multihash: string;
}
export async function getProjectAnchors(
urn: string,
anchorsStorage: string,
config: Config,
): Promise<string[]> {
const unpadded = decodeRadicleId(urn);
const id = ethers.utils.hexZeroPad(unpadded, 32);
const allAnchors = await querySubgraph(config.orgs.subgraph, GetAllAnchors, {
project: id,
org: anchorsStorage,
});
return allAnchors.anchors.map((anchor: AnchorObject) =>
formatProjectHash(ethers.utils.arrayify(anchor.multihash)),
);
}
-225
View File
@@ -1,225 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { navigate } from "svelte-routing";
import Modal from "@app/Modal.svelte";
import Error from "@app/Error.svelte";
import type { Err } from "@app/error";
import { Org } from "@app/base/orgs/Org";
import type { Config } from "@app/config";
import Loading from "@app/Loading.svelte";
import Options from "@app/Options.svelte";
import Address from "@app/Address.svelte";
import Button from "@app/Button.svelte";
export let config: Config;
export let owner: string;
enum State {
Idle,
Signing,
Pending,
Success,
}
enum Governance {
Existing = "existing",
Quorum = "quorum",
}
const orgTypes = [
{
label: "Multi-signature",
description: [
"Creates an org with a multi-signature contract as its owner, and the specified account as the first member.",
"A [Gnosis Safe](https://gnosis-safe.io) will be deployed for your org.",
"Transactions such as anchoring have to be approved by a quorum of signers.",
],
value: Governance.Quorum,
},
{
label: "Existing owner",
description: [
`Creates an org with the specified account as the sole owner.`,
`Org transactions such as anchoring are signed and executed from that account.`,
`This option allows for using an existing contract or EOA as the owner of the org.`,
],
value: Governance.Existing,
},
];
let state = State.Idle;
let error: Err | null = null;
let org: Org | null = null;
let governance = Governance.Quorum;
const dispatch = createEventDispatcher();
const createOrg = async () => {
state = State.Signing;
try {
const tx =
governance === Governance.Quorum
? await Org.createMultiSig([owner], 1, config)
: await Org.create(owner, config);
state = State.Pending;
const receipt = await tx.wait();
org = Org.fromReceipt(receipt);
state = State.Success;
} catch (e: any) {
console.error(e);
state = State.Idle;
error = e;
}
};
const onGovernanceChanged = (event: { detail: string }) => {
switch (event.detail) {
case "existing":
governance = Governance.Existing;
break;
case "quorum":
governance = Governance.Quorum;
break;
}
};
</script>
<style>
.fields {
display: grid;
grid-template-columns: 1fr 7fr;
grid-gap: 0.5rem 2rem;
}
.fields > span {
justify-self: start;
align-self: center;
height: 2rem;
line-height: 2rem;
}
.configuration {
max-width: 35rem;
text-align: left;
}
.governance {
background-color: var(--color-secondary-background);
border-radius: var(--border-radius);
padding: 1rem 1rem 0rem 1rem;
margin-bottom: 2rem;
}
label[for="address"] {
margin-left: 1.5rem;
font-size: var(--font-size-tiny);
color: var(--color-secondary);
}
input[name="address"] {
margin: 0.5rem 0 0 0;
width: 100%;
}
.notice {
font-size: var(--font-size-small);
border-radius: var(--border-radius);
color: var(--color-yellow);
background-color: var(--color-yellow-background);
padding: 1rem;
margin-bottom: 1rem;
}
</style>
{#if error}
<Error {error} floating on:close />
{:else if org}
<!-- Org created -->
<Modal floating on:close>
<span slot="title">🎉</span>
<span slot="subtitle">
<span class="txt-bold">Your org was successfully created.</span>
</span>
<span slot="body">
<div class="fields">
<span class="label">Address</span>
<span><Address address={org.address} {config} /></span>
<span class="label">Owner</span>
<span><Address resolve address={org.owner} {config} /></span>
</div>
</span>
<span slot="actions">
<Button
variant="foreground"
on:click={() => navigate(`/${org?.address}`)}>
Done
</Button>
</span>
</Modal>
{:else}
<!-- Org creation flow -->
<Modal floating on:close center>
<span slot="title">
<div>🎪</div>
<span>Create an Org</span>
</span>
<span slot="subtitle">
{#if state === State.Idle}
<div class="highlight">Select how you'd like to create your org</div>
{:else if state === State.Signing}
<div class="highlight">
Please confirm the transaction in your wallet.
</div>
{:else if state === State.Pending}
<div class="highlight">Waiting for transaction to be processed...</div>
{/if}
</span>
<span slot="body">
{#if state === State.Idle}
<div class="configuration">
<div class="notice">
<span class="txt-bold">Notice:</span>
Orgs V1 are being deprecated. It is recommended not to create new orgs
at this point.
</div>
<div class="governance">
<Options
name="governance"
disabled={state !== State.Idle}
selected={governance}
options={orgTypes}
on:changed={onGovernanceChanged} />
</div>
<label class="input" for="address">Ethereum account address</label>
<input
name="address"
class="small"
type="text"
maxlength="42"
bind:value={owner} />
</div>
{:else}
<Loading center small />
{/if}
</span>
<span slot="actions">
{#if state === State.Idle}
<Button
on:click={createOrg}
variant="primary"
waiting={[State.Signing, State.Pending].includes(state)}
disabled={state !== State.Idle}>
Create
</Button>
<Button on:click={() => dispatch("close")} variant="text">Close</Button>
{/if}
</span>
</Modal>
{/if}
-122
View File
@@ -1,122 +0,0 @@
<script lang="ts">
import type { SvelteComponent } from "svelte";
import { session } from "@app/session";
import Create from "@app/base/orgs/Create.svelte";
import { Org } from "@app/base/orgs/Org";
import type { Config } from "@app/config";
import Loading from "@app/Loading.svelte";
import Message from "@app/Message.svelte";
import Cards from "@app/Cards.svelte";
import { setOpenGraphMetaTag } from "@app/utils";
import Button from "@app/Button.svelte";
export let config: Config;
setOpenGraphMetaTag([
{ prop: "og:title", content: "Radicle Orgs" },
{ prop: "og:description", content: "Orgs of the Radicle Network" },
{ prop: "og:url", content: window.location.href },
]);
const onCreate = () => (modal = Create);
let modal: typeof SvelteComponent | null = null;
$: account = $session && $session.address;
</script>
<style>
main {
width: 100%;
padding: 3rem;
display: block;
align-items: space-between;
justify-content: space-between;
}
main header {
color: var(--color-secondary);
font-size: var(--font-size-medium);
display: flex;
align-items: center;
padding: 1rem 0;
gap: 1.5rem;
}
.my-orgs {
margin-bottom: 2rem;
}
.orgs-empty {
padding: 2rem 0 1rem 0;
}
.loading {
padding: 2rem 0;
}
</style>
<svelte:head>
<title>Radicle: Orgs</title>
</svelte:head>
<main>
{#if account}
<div class="my-orgs">
<header>
<span>
My <span class="txt-bold">Orgs</span>
</span>
<Button variant="secondary" on:click={onCreate} disabled={!account}>
Create
</Button>
</header>
{#await Org.getOrgsByMember(account, config)}
<div class="loading">
<Loading center />
</div>
{:then orgs}
<Cards {config} {orgs}>
<div class="orgs-empty txt-missing">
Orgs you are a member of show up here.
</div>
</Cards>
{:catch}
<div>
<Message error>
<span class="txt-bold">Error:</span>
failed to load orgs.
</Message>
</div>
{/await}
</div>
{/if}
<header>
<span>
<span class="txt-bold">Orgs</span>
of the Radicle network
</span>
</header>
{#await Org.getAll(config)}
<div class="loading">
<Loading center />
</div>
{:then orgs}
<Cards {config} {orgs}>
<div class="orgs-empty">There are no orgs.</div>
</Cards>
{:catch}
<div>
<Message error>
<span class="txt-bold">Error:</span>
failed to load orgs.
</Message>
</div>
{/await}
</main>
<svelte:component
this={modal}
owner={account}
{config}
on:close={() => (modal = null)} />
-211
View File
@@ -1,37 +1,12 @@
import * as ethers from "ethers";
import type { TransactionResponse } from "@ethersproject/providers";
import type { ContractReceipt } from "@ethersproject/contracts";
import { OperationType } from "@gnosis.pm/safe-core-sdk-types";
import { assert } from "@app/error";
import * as utils from "@app/utils";
import * as cache from "@app/cache";
import type { SafeMultisigTransactionListResponse } from "@gnosis.pm/safe-service-client";
import type SafeServiceClient from "@gnosis.pm/safe-service-client";
import type { Safe } from "@app/utils";
import type { Config } from "@app/config";
import type { PendingAnchor, Anchor } from "@app/project";
const GetProjects = `
query GetProjects($org: ID!) {
projects(where: { org: $org }) {
anchor {
objectId
multihash
timestamp
}
}
}
`;
const GetOrgs = `
query GetOrgs {
orgs {
id
owner
}
}
`;
const GetSafesByOwners = `
query GetSafesByOwners($owners: [String!]!) {
@@ -198,119 +173,6 @@ export class Org {
return members.includes(address.toLowerCase());
}
async getProjects(config: Config): Promise<Anchor[]> {
const result = await utils.querySubgraph(
config.orgs.subgraph,
GetProjects,
{ org: this.address },
);
const projects: Anchor[] = [];
for (const p of result.projects) {
try {
const proj: Anchor = {
confirmed: true,
id: utils.formatRadicleId(ethers.utils.arrayify(p.anchor.objectId)),
anchor: {
stateHash: utils.formatProjectHash(
ethers.utils.arrayify(p.anchor.multihash),
),
},
};
projects.push(proj);
} catch (e) {
console.error(e);
}
}
return projects;
}
async getPendingProjects(config: Config): Promise<PendingAnchor[]> {
if (!config.safe.client) return [];
try {
const orgAddr = ethers.utils.getAddress(this.address);
const response = await getPendingProjects(
ethers.utils.getAddress(this.owner),
config.safe.client,
);
const projects: PendingAnchor[] = [];
for (const tx of response.results || []) {
if (tx.data && tx.to === orgAddr) {
const anchor = parseAnchorTx(tx.data, config);
const confirmations = tx.confirmations?.map(t => t.owner) || [];
if (anchor) {
projects.push({
id: anchor.id,
anchor: { stateHash: anchor.stateHash },
confirmations,
safeTxHash: tx.safeTxHash,
confirmed: false,
});
}
}
}
return projects;
} catch {
return [];
}
}
static async getAnchor(
orgAddr: string,
urn: string,
config: Config,
): Promise<string | null> {
const org = new ethers.Contract(orgAddr, config.abi.org, config.provider);
const unpadded = utils.decodeRadicleId(urn);
const id = ethers.utils.zeroPad(unpadded, 32);
try {
const [, hash] = await org.anchors(id);
const anchor = utils.formatProjectHash(ethers.utils.arrayify(hash));
return anchor;
} catch (e) {
console.error(e);
return null;
}
}
static async getMulti(ids: string[], config: Config): Promise<Array<Org>> {
const results = await Promise.all(ids.map(addr => Org.get(addr, config)));
const orgs = results.filter((org): org is Org => org !== null);
return orgs;
}
static async getAll(config: Config): Promise<Array<Org>> {
const result = await utils.querySubgraph(config.orgs.subgraph, GetOrgs, {});
const orgs: Org[] = [];
for (const o of result.orgs) {
try {
orgs.push(new Org(o.id, o.owner));
} catch (e) {
console.error(e);
}
}
return orgs;
}
static fromReceipt(receipt: ContractReceipt): Org | null {
const event = receipt.events?.find(e => e.event === "OrgCreated");
if (event && event.args) {
const address = event.args[0];
const owner = event.args[1];
return new Org(address, owner);
}
return null;
}
static async get(addressOrName: string, config: Config): Promise<Org | null> {
const org = await getOrgContract(addressOrName, config);
@@ -354,62 +216,6 @@ export class Org {
return orgs.map(o => new Org(o.id, o.owner));
}
static async createMultiSig(
owners: [string],
threshold: number,
config: Config,
): Promise<TransactionResponse> {
assert(config.signer);
const orgFactory = new ethers.Contract(
config.orgFactory.address,
config.abi.orgFactory,
config.signer,
);
return orgFactory["createOrg(address[],uint256)"](owners, threshold, {
gasLimit: config.gasLimits.createOrg,
});
}
static async create(
owner: string,
config: Config,
): Promise<TransactionResponse> {
assert(config.signer);
const orgFactory = new ethers.Contract(
config.orgFactory.address,
config.abi.orgFactory,
config.signer,
);
return orgFactory["createOrg(address)"](owner, {
gasLimit: config.gasLimits.createOrg,
});
}
}
export function parseAnchorTx(
data: string,
config: Config,
): { id: string; stateHash: string } | null {
const iface = new ethers.utils.Interface(config.abi.org);
const parsedTx = iface.parseTransaction({ data });
if (parsedTx.name === "anchor") {
const encodedProjectUrn = parsedTx.args[0];
const encodedCommitHash = parsedTx.args[2];
const id = utils.formatRadicleId(
ethers.utils.arrayify(`${encodedProjectUrn}`),
);
const byteArray = ethers.utils.arrayify(encodedCommitHash);
const stateHash = utils.formatProjectHash(byteArray);
return { id, stateHash };
}
return null;
}
export const getOrgContract = cache.cached(
@@ -425,20 +231,3 @@ export const resolveOrgOwner = cache.cached(
},
org => org.address,
);
export const getPendingProjects = cache.cached(
async (
owner: string,
client: SafeServiceClient,
): Promise<SafeMultisigTransactionListResponse> => {
try {
return await client.getPendingTransactions(
ethers.utils.getAddress(owner),
);
} catch (e) {
return { count: 0, results: [] };
}
},
owner => owner,
{ max: 1000, ttl: 5 * 60 * 1000 }, // Cache results for 5 minutes.
);
-15
View File
@@ -1,15 +0,0 @@
<script lang="ts">
import { Route } from "svelte-routing";
import Redirect from "@app/Redirect.svelte";
import NotFound from "@app/NotFound.svelte";
</script>
<Route path="/orgs">
<NotFound
title="404"
subtitle="Radicle Orgs are in the process of being re-designed." />
</Route>
<Route path="/orgs/:addressOrName" let:params>
<Redirect to="/{params.addressOrName}" />
</Route>
-267
View File
@@ -1,267 +0,0 @@
<script lang="ts">
import { ethers } from "ethers";
import type { Safe } from "@app/utils";
import type { PendingAnchor } from "@app/project";
import type { Config } from "@app/config";
import * as utils from "@app/utils";
import Modal from "@app/Modal.svelte";
import Avatar from "@app/Avatar.svelte";
import { createEventDispatcher } from "svelte";
import Badge from "@app/Badge.svelte";
import Button from "@app/Button.svelte";
export let safe: Safe;
export let anchor: PendingAnchor;
export let account: string;
export let config: Config;
enum State {
Idle,
Confirm,
Signing,
Submitting,
Success,
Execute,
Failed,
}
enum Action {
Sign,
Execute,
}
const dispatch = createEventDispatcher();
let state = State.Idle;
let error: string | null = null;
let action: null | Action = null;
const close = () => {
action = null;
state = State.Idle;
};
const pending = safe.threshold - anchor.confirmations.length;
const executeTransaction = async (safeTxHash: string) => {
try {
action = Action.Execute;
state = State.Signing;
const txResult = await utils.executeSignedSafeTransaction(
safe.address,
safeTxHash,
config,
);
state = State.Submitting;
await txResult.transactionResponse?.wait();
state = State.Success;
} catch (err: any) {
console.error(err);
error = err.message;
state = State.Failed;
}
};
const confirmAnchor = async (safeTxHash: string) => {
try {
action = Action.Sign;
state = State.Signing;
const signature = await utils.signSafeTransaction(
safe.address,
safeTxHash,
config,
);
state = State.Submitting;
await config.safe.client?.confirmTransaction(safeTxHash, signature.data);
state = State.Success;
} catch (err: any) {
console.error(err);
error = err.message;
state = State.Failed;
}
};
$: isSigned = anchor.confirmations.includes(ethers.utils.getAddress(account));
</script>
<style>
.confirmations {
margin-right: 0.5rem;
}
.table {
display: grid;
grid-template-columns: 5rem 4fr;
grid-gap: 1rem;
text-align: left;
}
.table > *:nth-child(odd) {
/* Labels */
color: var(--color-secondary);
}
.table > *:nth-child(even) {
/* Values */
display: flex;
align-items: center;
justify-content: left;
}
.avatars {
display: flex;
margin-right: 0.75rem;
}
</style>
<span class="confirmations">
{#if pending > 0}
<span class="txt-bold">{pending}</span>
signature(s) pending
{/if}
</span>
<div class="avatars">
{#each anchor.confirmations as signee}
<Avatar inline source={signee} title={signee} />
{/each}
</div>
<!-- Check whether the threshold has been matched or passed -->
{#if pending <= 0}
<Button
on:click={() => {
action = Action.Execute;
state = State.Confirm;
}}
variant="foreground"
size="small">
<Avatar inline source={account} title={account} /> Execute
</Button>
<!-- Check whether or not we've signed this proposal -->
{:else if isSigned}
<Badge variant="caution">✓ signed</Badge>
{:else}
<Button
on:click={() => {
action = Action.Sign;
state = State.Confirm;
}}
variant="foreground"
size="small">
Confirm
</Button>
{/if}
<!-- We've initiated an action -->
{#if state !== State.Idle && action === Action.Sign}
<Modal floating>
<span slot="title">
<div>⚓</div>
<div>Anchor project</div>
</span>
<span slot="subtitle">
{#if state === State.Confirm}
<span>Initiate the transaction...</span>
{:else if state === State.Signing}
<span>Sign the transaction in your wallet...</span>
{:else if state === State.Submitting}
<span>Transaction is being confirmed...</span>
{:else if state === State.Success}
<span>Transaction confirmed.</span>
{:else if state === State.Failed}
<span>Transaction failed</span>
{/if}
</span>
<span slot="body">
{#if state === State.Confirm}
<div class="table">
<div>Project</div>
<span class="txt-monospace">{anchor.id}</span>
<div>Hash</div>
<span class="txt-monospace">{anchor.anchor.stateHash}</span>
</div>
{:else if state === State.Failed}
<div>{error}</div>
{/if}
</span>
<span slot="actions">
{#if state === State.Confirm}
<Button
variant="primary"
on:click={() => confirmAnchor(anchor.safeTxHash)}>
Confirm
</Button>
<Button variant="text" on:click={close}>Cancel</Button>
{:else if state === State.Success || state === State.Failed}
<Button
variant="foreground"
on:click={() => {
close();
dispatch("success");
}}>
Done
</Button>
{/if}
</span>
</Modal>
{:else if state !== State.Idle && action === Action.Execute}
<Modal floating>
<span slot="title">
<div>⚡</div>
<div>Execute Safe Transaction</div>
</span>
<span slot="subtitle">
{#if state === State.Confirm}
<span>Initiate the transaction...</span>
{:else if state === State.Signing}
<span>Sign the transaction in your wallet...</span>
{:else if state === State.Submitting}
<span>Transaction is being confirmed...</span>
{:else if state === State.Success}
<span>Transaction confirmed.</span>
{:else if state === State.Failed}
<span>Transaction failed</span>
{/if}
</span>
<span slot="body">
{#if state === State.Confirm}
<div class="table">
<div>TxHash</div>
<span class="txt-monospace">
{utils.formatHash(anchor.safeTxHash)}
</span>
<div>Quorum</div>
<span class="txt-monospace">
{anchor.confirmations.length} of {safe.threshold}
</span>
</div>
{:else if state === State.Failed}
<div>{error}</div>
{/if}
</span>
<span slot="actions">
{#if state === State.Confirm}
<Button
variant="primary"
on:click={() => executeTransaction(anchor.safeTxHash)}>
Confirm
</Button>
<Button variant="text" on:click={close}>Cancel</Button>
{:else if state === State.Success || state === State.Failed}
<Button
variant="foreground"
on:click={() => {
close();
dispatch("success");
}}>
Done
</Button>
{/if}
</span>
</Modal>
{/if}
-93
View File
@@ -1,93 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let anchors: Array<string> = [];
export let head: string | null;
export let commit: string;
export let noText = false;
export let noBg = false;
const text = !noText;
</script>
<style>
.anchor-widget {
display: flex;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius-small);
color: var(--color-tertiary);
background-color: var(--color-tertiary-background);
cursor: pointer;
}
.anchor-widget.not-allowed {
cursor: not-allowed;
}
.anchor-widget.not-anchored {
color: var(--color-foreground-faded);
background-color: var(--color-foreground-background);
}
.anchor-widget.no-bg {
background: none !important;
padding: 0 !important;
}
.anchor-label {
font-family: var(--font-family-monospace);
margin-right: 0.5rem;
}
.anchor-label:last-child {
margin-right: 0;
}
.anchor-latest {
cursor: default;
}
</style>
{#if anchors && head}
<!-- commit is head and latest anchor -->
{#if commit === anchors[0] && commit === head}
<span class="anchor-widget anchor-latest" class:no-bg={noBg}>
<span class="anchor-label" title={anchors[0]}>
{#if text}latest&nbsp;{/if}🔐
</span>
</span>
<!-- commit is not head but latest anchor -->
{:else if commit === anchors[0] && commit !== head}
<span
class="anchor-widget"
class:no-bg={noBg}
on:click={() => dispatch("click", head)}>
<span class="anchor-label" title={anchors[0]}>
{#if text}latest&nbsp;{/if}🔐
</span>
</span>
<!-- commit is not head a stale anchor -->
{:else if anchors.includes(commit)}
<span
class="anchor-widget"
class:no-bg={noBg}
on:click={() => dispatch("click", anchors[0])}>
<span class="anchor-label" title={commit}>
{#if text}stale&nbsp;{/if}🔒
</span>
</span>
<!-- commit is not anchored, could be head or any other commit -->
{:else}
<span
class="anchor-widget not-anchored"
class:no-bg={noBg}
on:click={() => dispatch("click", anchors[0])}>
<span class="anchor-label">
{#if text}not anchored&nbsp;{/if}🔓
</span>
</span>
{/if}
{:else}
<!-- commit is not head and neither an anchor, and there are no anchors available -->
<span class="anchor-widget not-anchored not-allowed" class:no-bg={noBg}>
<span class="anchor-label">
{#if text}not anchored&nbsp;{/if}🔓
</span>
</span>
{/if}
+1 -16
View File
@@ -2,7 +2,6 @@
import type { Writable } from "svelte/store";
import { navigate } from "svelte-routing";
import { Browser, ProjectContent, Project } from "@app/project";
import AnchorBadge from "@app/base/profiles/AnchorBadge.svelte";
import BranchSelector from "@app/base/projects/BranchSelector.svelte";
import CloneButton from "@app/base/projects/CloneButton.svelte";
import PeerSelector from "@app/base/projects/PeerSelector.svelte";
@@ -12,9 +11,8 @@
export let tree: Tree;
export let commit: string;
export let browserStore: Writable<Browser>;
export let noAnchor = false;
const { urn, peers, branches, seed, anchors } = project;
const { urn, peers, branches, seed } = project;
$: browser = $browserStore;
$: revision = browser.revision || commit;
@@ -54,9 +52,6 @@
border-radius: var(--border-radius-small);
min-width: max-content;
}
.anchor {
display: inline-flex;
}
.clickable {
cursor: pointer;
}
@@ -104,16 +99,6 @@
{revision}
on:branchChanged={event => updateRevision(event.detail)} />
{#if !noAnchor}
<div class="anchor widget">
<AnchorBadge
{commit}
{anchors}
head={project.head}
on:click={event => updateRevision(event.detail)} />
</div>
{/if}
{#if seed.git.host}
<CloneButton seedHost={seed.git.host} {urn} />
{/if}
+1 -1
View File
@@ -74,7 +74,7 @@
{#await project.getRoot(revision)}
<Loading center />
{:then { tree, commit }}
<Header {tree} {commit} {browserStore} {project} noAnchor />
<Header {tree} {commit} {browserStore} {project} />
{#if content === proj.ProjectContent.Tree}
<Browser {project} {commit} {tree} {browserStore} />
+2 -44
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import type * as proj from "@app/project";
import AnchorBadge from "@app/base/profiles/AnchorBadge.svelte";
import Diagram from "@app/Diagram.svelte";
import { groupCommitsByWeek } from "@app/commit";
import type { Host } from "@app/api";
@@ -10,7 +9,6 @@
export let project: proj.ProjectInfo;
export let seed: { api: Host };
export let faded = false;
export let anchor: proj.Anchor | null = null;
export let compact = false;
const loadCommits = async () => {
@@ -69,9 +67,6 @@
article:hover {
border-color: var(--color-secondary);
}
article:hover .anchor {
display: block;
}
article:hover .activity {
display: none !important;
}
@@ -94,22 +89,10 @@
display: flex;
align-items: center;
}
article .anchor-info {
display: flex;
align-items: center;
}
article .actions {
margin-right: 1rem;
}
article .commit,
article .actions {
article .commit {
font-family: var(--font-family-monospace);
}
article.project-faded .anchor {
color: var(--color-foreground-faded);
}
article .id,
article .anchor {
article .id {
display: flex;
justify-content: space-between;
}
@@ -120,15 +103,9 @@
font-family: var(--font-family-monospace);
font-size: var(--font-size-tiny);
}
article .anchor-badge {
display: none;
}
article:hover .id .urn {
visibility: visible;
}
article:hover .anchor-badge {
display: block;
}
@media (max-width: 720px) {
article {
min-width: 0;
@@ -164,25 +141,6 @@
<div class="id">
<span class="urn desktop">{project.urn}</span>
</div>
<div class="anchor">
<span class="anchor-info">
<span class="actions">
<slot name="actions" />
</span>
<span class="anchor-badge">
<slot name="anchor">
{#if anchor && project.head}
<AnchorBadge
commit={project.head}
head={project.head}
noText
noBg
anchors={[anchor.anchor.stateHash]} />
{/if}
</slot>
</span>
</span>
</div>
{#await loadCommits() then points}
<div class="desktop activity">
<Diagram
+2 -2
View File
@@ -6,7 +6,7 @@
import SeedAddress from "@app/SeedAddress.svelte";
import NotFound from "@app/NotFound.svelte";
import Clipboard from "@app/Clipboard.svelte";
import Projects from "@app/base/orgs/View/Projects.svelte";
import Projects from "@app/base/seeds/View/Projects.svelte";
import type { Session } from "@app/session";
import Address from "@app/Address.svelte";
import SiweConnect from "@app/SiweConnect.svelte";
@@ -158,7 +158,7 @@
</div>
<!-- Seed Projects -->
<Async fetch={Project.getProjects(seedHost, { perPage: 10 })} let:result>
<Projects {seed} {config} projects={result} />
<Projects {seed} projects={result} />
</Async>
</main>
{:catch}
@@ -1,38 +1,19 @@
<script lang="ts">
import { navigate } from "svelte-routing";
import { onMount } from "svelte";
import type { Config } from "@app/config";
import * as proj from "@app/project";
import Widget from "@app/base/projects/Widget.svelte";
import type { Profile } from "@app/profile";
import type { ProjectInfo, Anchor, PendingAnchor } from "@app/project";
import type { ProjectInfo } from "@app/project";
import type { Seed } from "@app/base/seeds/Seed";
import AnchorActions from "@app/base/profiles/AnchorActions.svelte";
import List from "@app/List.svelte";
export let seed: Seed;
export let profile: Profile | null = null;
export let account: string | null = null;
export let projects: proj.ProjectInfo[];
export let config: Config;
let anchors: Record<string, Anchor> = {};
let pendingAnchors: Record<string, PendingAnchor> = {};
// A pointer to the current page of projects added to the listing
let page = 0;
const loadAnchors = async () => {
if (profile) {
const [pending, confirmed] = await Promise.all([
profile.pendingAnchors(config),
profile.confirmedAnchors(config),
]);
anchors = confirmed;
pendingAnchors = pending;
}
};
const fetchMoreProjects = async (): Promise<proj.ProjectInfo[]> => {
const projects = await proj.Project.getProjects(seed.api, {
perPage: 10,
@@ -56,8 +37,6 @@
}),
);
};
onMount(loadAnchors);
</script>
<style>
@@ -67,35 +46,15 @@
.projects .project {
margin-bottom: 0.5rem;
}
.actions {
display: flex;
align-items: center;
}
</style>
<div class="projects">
<List items={projects} query={fetchMoreProjects}>
<svelte:fragment slot="list" let:items>
{#each items as project}
{@const anchor = anchors[project.urn]}
{@const pendingAnchor = pendingAnchors[project.urn]}
{#if project.head}
<div class="project">
<Widget {project} {seed} {anchor} on:click={() => onClick(project)}>
<span class="actions" slot="actions">
{#if profile?.org?.safe && account && anchor}
{#if pendingAnchor}
<!-- Pending anchor -->
<AnchorActions
{account}
{config}
anchor={pendingAnchor}
safe={profile.org.safe}
on:success={() => loadAnchors()} />
{/if}
{/if}
</span>
</Widget>
<Widget {project} {seed} on:click={() => onClick(project)} />
</div>
{/if}
{/each}
-8
View File
@@ -1,8 +0,0 @@
<script lang="ts">
import { Route } from "svelte-routing";
import Redirect from "@app/Redirect.svelte";
</script>
<Route path="/users/:addressOrName" let:params>
<Redirect to="/{params.addressOrName}" />
</Route>
@@ -9,7 +9,7 @@
import Address from "@app/Address.svelte";
import Button from "@app/Button.svelte";
import type { Org } from "./Org";
import type { Org } from "@app/base/orgs/Org";
const dispatch = createEventDispatcher();
-59
View File
@@ -15,8 +15,6 @@ import { cached } from "@app/cache";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import { Org } from "@app/base/orgs/Org";
import { NotFoundError, MissingReverseRecord } from "@app/error";
import { getProjectAnchors } from "@app/anchors";
import type { Anchor, PendingAnchor } from "@app/project";
export interface IProfile {
address: string;
@@ -153,63 +151,6 @@ export class Profile {
}
}
// Get confirmed anchors.
async confirmedAnchors(config: Config): Promise<Record<string, Anchor>> {
const org = await this.getAnchorsOrg(config);
if (org) {
const result = await org.getProjects(config);
const anchors: Record<string, Anchor> = {};
for (const anchor of result) {
anchors[anchor.id] = anchor;
}
return anchors;
} else {
return {};
}
}
// Get pending anchors.
async pendingAnchors(config: Config): Promise<Record<string, PendingAnchor>> {
const org = await this.getAnchorsOrg(config);
if (org) {
const result = await org.getPendingProjects(config);
const anchors: Record<string, PendingAnchor> = {};
for (const anchor of result) {
anchors[anchor.id] = anchor;
}
return anchors;
} else {
return {};
}
}
async confirmedProjectAnchors(
urn: string,
config: Config,
): Promise<string[]> {
const storage = this.anchorsAccount || this.org?.address;
if (storage) {
return await getProjectAnchors(urn, storage, config);
}
return [];
}
// Get the anchors account as an org, or the org, if available.
private async getAnchorsOrg(config: Config): Promise<Org | null> {
if (this.anchorsAccount) {
return await Org.get(this.anchorsAccount, config);
} else if (this.org) {
return this.org;
} else {
return null;
}
}
// Keeping this function private since the desired entrypoint is .get()
// All addresses returned from this function should be lowercase.
private static async lookupProfile(
+1 -25
View File
@@ -22,24 +22,6 @@ export type Delegate =
id: PeerId;
};
export interface Anchor {
confirmed: true;
id: string;
anchor: {
stateHash: string;
};
}
export interface PendingAnchor {
confirmed: false;
id: string;
safeTxHash: string; // Safe transaction hash.
confirmations: string[]; // Owner addresses who have confirmed.
anchor: {
stateHash: string;
};
}
// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
Tree,
@@ -277,7 +259,6 @@ export class Project implements ProjectInfo {
peers: Peer[];
branches: Branches;
profile: Profile | null;
anchors: string[];
// At the moment we still have seed nodes which won't return neither patches or issues
patches?: number;
issues?: number;
@@ -289,7 +270,6 @@ export class Project implements ProjectInfo {
peers: Peer[],
branches: Branches,
profile: Profile | null,
anchors: string[],
) {
this.urn = urn;
this.head = info.head;
@@ -304,7 +284,6 @@ export class Project implements ProjectInfo {
this.patches = info.patches;
this.issues = info.issues;
this.profile = profile;
this.anchors = anchors;
}
async getRoot(
@@ -480,9 +459,6 @@ export class Project implements ProjectInfo {
const info = await Project.getInfo(id, seed.api);
const urn = isRadicleId(id) ? id : info.urn;
const anchors = profile
? await profile.confirmedProjectAnchors(urn, config)
: [];
// Older versions of http-api don't include the URN.
if (!info.urn) info.urn = urn;
@@ -503,7 +479,7 @@ export class Project implements ProjectInfo {
}
}
return new Project(urn, info, seed, peers, remote.heads, profile, anchors);
return new Project(urn, info, seed, peers, remote.heads, profile);
}
static async getMulti(
+2 -2
View File
@@ -1,8 +1,8 @@
///<reference types="vitest" />
import path from "path";
import { UserConfig } from "vite";
import type { UserConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { ViteDevServer } from "vite";
import type { ViteDevServer } from "vite";
import IstanbulPlugin from "vite-plugin-istanbul";
import history from "connect-history-api-fallback";
import type { Request, Response } from "express-serve-static-core";