Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Show anchors under orgs
Alexis Sellier committed 4 years ago
commit 9bb96879b3e27f2261aaf75732b2eb6c952f8289
parent 59fc3ba356d2a6d7c88b99319bb607618a23966f
9 files changed +164 -2
modified package-lock.json
@@ -7,6 +7,7 @@
      "dependencies": {
        "@snowpack/plugin-typescript": "^1.2.1",
        "ethers": "^5.0.31",
+
        "multibase": "^4.0.4",
        "svelte": "^3.32.3",
        "svelte-routing": "^1.6.0"
      },
@@ -686,6 +687,11 @@
        "@ethersproject/strings": "^5.1.0"
      }
    },
+
    "node_modules/@multiformats/base-x": {
+
      "version": "4.0.1",
+
      "resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz",
+
      "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="
+
    },
    "node_modules/@npmcli/git": {
      "version": "2.0.9",
      "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.0.9.tgz",
@@ -2292,6 +2298,18 @@
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
      "dev": true
    },
+
    "node_modules/multibase": {
+
      "version": "4.0.4",
+
      "resolved": "https://registry.npmjs.org/multibase/-/multibase-4.0.4.tgz",
+
      "integrity": "sha512-8/JmrdSGzlw6KTgAJCOqUBSGd1V6186i/X8dDCGy/lbCKrQ+1QB6f3HE+wPr7Tpdj4U3gutaj9jG2rNX6UpiJg==",
+
      "dependencies": {
+
        "@multiformats/base-x": "^4.0.1"
+
      },
+
      "engines": {
+
        "node": ">=12.0.0",
+
        "npm": ">=6.0.0"
+
      }
+
    },
    "node_modules/no-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -3840,6 +3858,11 @@
        "@ethersproject/strings": "^5.1.0"
      }
    },
+
    "@multiformats/base-x": {
+
      "version": "4.0.1",
+
      "resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz",
+
      "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="
+
    },
    "@npmcli/git": {
      "version": "2.0.9",
      "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.0.9.tgz",
@@ -5131,6 +5154,14 @@
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
      "dev": true
    },
+
    "multibase": {
+
      "version": "4.0.4",
+
      "resolved": "https://registry.npmjs.org/multibase/-/multibase-4.0.4.tgz",
+
      "integrity": "sha512-8/JmrdSGzlw6KTgAJCOqUBSGd1V6186i/X8dDCGy/lbCKrQ+1QB6f3HE+wPr7Tpdj4U3gutaj9jG2rNX6UpiJg==",
+
      "requires": {
+
        "@multiformats/base-x": "^4.0.1"
+
      }
+
    },
    "no-case": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
modified package.json
@@ -16,6 +16,7 @@
  "dependencies": {
    "@snowpack/plugin-typescript": "^1.2.1",
    "ethers": "^5.0.31",
+
    "multibase": "^4.0.4",
    "svelte": "^3.32.3",
    "svelte-routing": "^1.6.0"
  }
modified src/base/orgs/Org.ts
@@ -2,8 +2,19 @@ import * as ethers from 'ethers';
import type { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import type { ContractReceipt } from '@ethersproject/contracts';
import { assert } from '@app/error';
-

+
import * as utils from '@app/utils';
import type { Config } from '@app/config';
+
import type { Project } from '@app/base/projects/Project';
+

+
const GetProjects = `
+
  query GetProjects($org: ID!) {
+
    projects(where: { org: $org }) {
+
      id
+
      stateHash
+
      stateHashFormat
+
    }
+
  }
+
`;

const orgFactoryAbi = [
  "function createOrg(address) returns (address)",
@@ -24,7 +35,7 @@ export class Org {
  constructor(address: string, safe: string) {
    assert(ethers.utils.isAddress(address), "address must be valid");

-
    this.address = address;
+
    this.address = address.toLowerCase(); // Don't store address checksum.
    this.safe = safe;
  }

@@ -55,6 +66,25 @@ export class Org {
    return org.setOwner(address);
  }

+
  async getProjects(config: Config): Promise<Array<Project>> {
+
    const result = await utils.querySubgraph(GetProjects, { org: this.address }, config);
+
    let projects: Project[] = [];
+

+
    for (let p of result.projects) {
+
      try {
+
        p.id = utils.formatRadicleId(ethers.utils.arrayify(p.id));
+
        p.stateHash = utils.formatProjectHash(
+
          ethers.utils.arrayify(p.stateHash),
+
          p.stateHashFormat
+
        );
+
        projects.push(p);
+
      } catch (e) {
+
        console.error(e);
+
      }
+
    }
+
    return projects;
+
  }
+

  static fromReceipt(receipt: ContractReceipt): Org | null {
    let event = receipt.events?.find(e => e.event === 'OrgCreated');

modified src/base/orgs/View.svelte
@@ -13,6 +13,7 @@
  import Error from '@app/Error.svelte';
  import Icon from '@app/Icon.svelte';
  import SetName from '@app/ens/SetName.svelte';
+
  import Project from '@app/base/projects/Widget.svelte';
  import * as utils from '@app/utils';

  import { Org } from './Org';
@@ -90,6 +91,9 @@
    display: flex; /* Ensures correct vertical positioning of icons */
    margin-right: 1rem;
  }
+
  .projects {
+
    margin-top: 2rem;
+
  }
</style>

{#await Org.get(address, config)}
@@ -158,6 +162,20 @@
          {/if}
        </div>
      </div>
+

+
      <div class="projects">
+
        {#await org.getProjects(config)}
+
          <Loading center />
+
        {:then projects}
+
          {#each projects as project}
+
            <Project {project} />
+
          {/each}
+
        {:catch err}
+
          <div class="error">
+
            Error loading projects: {err}.
+
          </div>
+
        {/await}
+
      </div>
    </main>
  {:else}
    <Modal subtle>
added src/base/projects/Project.ts
@@ -0,0 +1,5 @@
+
export interface Project {
+
  id: string
+
  stateHash: string
+
  stateHashFormat: string
+
}
added src/base/projects/Widget.svelte
@@ -0,0 +1,26 @@
+
<script type="typescript">
+
  import type { Project } from './Project';
+

+
  export let project: Project;
+
</script>
+

+
<style>
+
  article {
+
    padding: 1rem;
+
    border: 1px solid var(--color-secondary);
+
  }
+
  article .id {
+
    font-weight: 600;
+
    margin-bottom: 0.5rem;
+
  }
+
  article .anchor {
+
    color: var(--color-secondary);
+
    font-size: 0.75rem;
+
    font-family: var(--font-family-monospace);
+
  }
+
</style>
+

+
<article>
+
  <div class="id">{project.id}</div>
+
  <div class="anchor">commit {project.stateHash}</div>
+
</article>
modified src/config.json
@@ -21,6 +21,9 @@
    },
    "orgFactory": {
      "address": "0xe1814B14430CF14f0950A29FF26217115B815676"
+
    },
+
    "orgs": {
+
      "subgraph": "https://api.thegraph.com/subgraphs/name/radicle-dev/radicle-orgs-ropsten"
    }
  }
}
modified src/config.ts
@@ -20,6 +20,7 @@ export class Config {
  registrar: { address: string, domain: string };
  radToken: { address: string };
  orgFactory: { address: string };
+
  orgs: { subgraph: string };
  gasLimits: { createOrg: number };
  provider: ethers.providers.JsonRpcProvider;
  signer: ethers.Signer & TypedDataSigner | null;
@@ -41,6 +42,7 @@ export class Config {
    this.registrar = cfg.registrar;
    this.radToken = cfg.radToken;
    this.orgFactory = cfg.orgFactory;
+
    this.orgs = cfg.orgs;
    this.provider = provider;
    this.signer = signer;
    this.gasLimits = gasLimits;
modified src/utils.ts
@@ -1,6 +1,8 @@
import { ethers } from "ethers";
import type { BigNumber } from "ethers";
+
import multibase from 'multibase';
import type { Config } from '@app/config';
+
import { assert } from '@app/error';

export function formatBalance(n: BigNumber) {
  return ethers.utils.commify(parseFloat(ethers.utils.formatUnits(n)).toFixed(2));
@@ -59,3 +61,47 @@ export function explorerLink(addr: string, config: Config): string {
  }
  return `https://etherscan.io/address/${addr}`;
}
+

+
// Query a subgraph.
+
export async function querySubgraph(
+
  query: string,
+
  variables: Record<string, any>,
+
  config: Config
+
): Promise<any> {
+
  const response = await fetch(config.orgs.subgraph, {
+
    method: 'POST',
+
    headers: {
+
      'Content-Type': 'application/json',
+
    },
+
    body: JSON.stringify({
+
      query,
+
      variables,
+
    })
+
  });
+
  const json = await response.json()
+

+
  return json.data;
+
}
+

+
// Create a Radicle ID from a root hash.
+
export function formatRadicleId(hash: Uint8Array): string {
+
  // Remove any zero-padding from the byte array. SHA1 is 20 bytes long.
+
  const sha1Bytes = 20;
+
  const suffix = hash.slice(hash.length - sha1Bytes);
+

+
  // Create a multihash by adding prefix 17 for SHA-1 and 20 for the hash length.
+
  const multihash = new Uint8Array([17, 20, ...suffix]);
+
  const payload = multibase.encode("base32z", multihash);
+

+
  return `rad:git:${new TextDecoder().decode(payload)}`;
+
}
+

+
// Create a project hash from a hash and format.
+
export function formatProjectHash(hash: Uint8Array, format: number): string {
+
  assert(format === 0x0, "Only SHA1 commit hashes are supported");
+

+
  // Remove any zero-padding from the byte array. SHA1 is 20 bytes long.
+
  const sha1Bytes = 20;
+
  const suffix = hash.slice(hash.length - sha1Bytes);
+
  return ethers.utils.hexlify(suffix).replace(/^0x/, '');
+
}