Skip to content

Commit

Permalink
Add Solana modular governance support (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChewingGlass authored Oct 12, 2023
1 parent 9974d3c commit 14c18bf
Show file tree
Hide file tree
Showing 70 changed files with 8,475 additions and 2,276 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/create-proposals.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Create Proposals

on:
push:
branches:
- main

env:
NODE_VERSION: 16

jobs:
create:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}

- uses: actions/cache@v2
name: Cache Typescript node_modules
id: cache-typescript-node-modules
with:
path: |
./node_modules/
key: node-modules-${{ runner.os }}-v0000-${{ env.NODE_VERSION }}-${{ hashFiles('./yarn.lock') }}
- run: yarn install
shell: bash

- run: echo "$DEPLOY_KEYPAIR" > ./deploy-keypair.json && chmod 600 ./deploy-keypair.json
shell: bash
env:
DEPLOY_KEYPAIR: ${{ secrets.DEPLOYER_KEYPAIR }}

- name: Run Bulk Create Helium Proposals
run: |
./node_modules/.bin/ts-node ./bin/helium-vote.ts bulk-create-proposal \
-u "https://solana-rpc.web.helium.io?session-key=Pluto" \
--file ./helium-proposals.json \
--wallet ./deploy-keypair.json \
--orgName Helium \
--multisig ${{ secrets.MULTISIG }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ yarn-error.log*

# vercel
.vercel
.yalc
8 changes: 0 additions & 8 deletions .vscode/settings.json

This file was deleted.

14 changes: 14 additions & 0 deletions bin/helium-vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env ts-node

const { hideBin } = require("yargs/helpers");

const args = hideBin(process.argv);
const script = args[0];

require(__dirname + `/../scripts/${script}`)
.run(args.filter((arg) => arg !== script))
.catch((err) => {
console.error(err);
process.exit(1);
})
.then(() => process.exit());
142 changes: 142 additions & 0 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { FunctionComponent } from "react";
import Loading, { LoadingDots } from "./Loading";
import Tooltip from "./Tooltip";

interface ButtonProps {
className?: string;
isLoading?: boolean;
onClick?: () => void;
disabled?: boolean;
small?: boolean;
tooltipMessage?: string;
style?: any;
type?: "button" | "submit";
}

const Button: FunctionComponent<React.PropsWithChildren<ButtonProps>> = ({
children,
className,
disabled,
isLoading,
small,
tooltipMessage = "",
style,
type = "button",
...props
}) => {
return (
<button
className={`${className} default-transition font-bold px-4 bg-hv-blue-700 rounded-lg ${
small ? "py-1" : "py-2.5"
} text-sm focus:outline-none ${
disabled
? "opacity-60 cursor-not-allowed"
: "hover:bg-hv-blue-400 text-bkg-2 hover:bg-fgd-1"
}`}
{...props}
style={style}
type={type}
disabled={disabled}
>
<Tooltip content={tooltipMessage}>
<div>{isLoading ? <Loading /> : children}</div>
</Tooltip>
</button>
);
};

export default Button;

export const SecondaryButton: FunctionComponent<React.PropsWithChildren<ButtonProps>> = ({
children,
onClick,
disabled = false,
className,
isLoading,
small = false,
tooltipMessage = "",
type = "button",
...props
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
type={type}
className={`${className} border border-hv-blue-700 ${
disabled ? "opacity-60" : "hover:bg-blue-400"
} font-bold default-transition rounded-lg px-4 ${
small ? "py-1" : "py-2.5"
} text-primary-light text-sm hover:border-fgd-1 hover:text-fgd-1 focus:outline-none disabled:border-fgd-4 disabled:text-fgd-3 disabled:cursor-not-allowed`}
{...props}
>
<Tooltip content={tooltipMessage}>
<div>{isLoading ? <Loading /> : children}</div>
</Tooltip>
</button>
);
};

export const LinkButton: FunctionComponent<React.PropsWithChildren<ButtonProps>> = ({
children,
onClick,
disabled = false,
className,
type = "button",
...props
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
type={type}
className={`${className} border-0 default-transition text-sm disabled:cursor-not-allowed disabled:opacity-60 hover:opacity-60 focus:outline-none`}
{...props}
>
{children}
</button>
);
};

interface NewButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
secondary?: boolean;
radio?: boolean;
selected?: boolean;
className?: string;
}

export const NewButton: FunctionComponent<NewButtonProps> = ({
className = "",
loading = false,
secondary = false,
children,
...props
}) => {
let classNames = `heading-cta default-transition rounded-full focus-visible:outline-none disabled:cursor-not-allowed `;

if (loading) {
classNames +=
" h-[64px] min-w-[208px] border border-fgd-3 disabled:border-fgd-3";
} else if (secondary) {
classNames +=
"py-3 px-2 h-[64px] min-w-[208px] text-fgd-1 border border-fgd-3 focus:border-fgd-1 hover:bg-fgd-1 hover:text-bkg-1 active:bg-fgd-2 active:text-bkg-1 active:border-none disabled:bg-fgd-4 disabled:text-bkg-1 disabled:border-none ";
} else {
// this is a primary button
// TODO: make sure this using the typogrpahic class for CTAs
classNames +=
"py-4 px-2 h-[64px] min-w-[208px] text-bkg-1 bg-fgd-1 hover:bg-fgd-2 active:bg-fgd-3 active:border-none focus:border-2 focus:border-[#00E4FF] disabled:bg-fgd-4";
}

classNames += ` ${className}`;

return (
<button
className={classNames}
disabled={props.disabled || loading}
{...props}
>
{!loading ? children : <LoadingDots />}
</button>
);
};
47 changes: 47 additions & 0 deletions components/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FunctionComponent } from "react";

interface ButtonGroupProps {
activeValue: string;
className?: string;
onChange: (x) => void;
unit?: string;
values: Array<string>;
names?: Array<string>;
}

const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
activeValue,
className,
unit,
values,
onChange,
names,
}) => {
return (
<div className="bg-bkg-3 rounded-md">
<div className="flex relative">
{values.map((v, i) => (
<button
type="button"
className={`${className} mx-1 cursor-pointer default-transition font-normal px-2 py-1.5 relative rounded-lg text-center text-xs w-1/2
${
v === activeValue
? `text-primary-light border border-hv-blue-700`
: `border border-hv-gray-400 text-fgd-2 hover:text-primary-light`
}
`}
key={`${v}${i}`}
onClick={() => onChange(v)}
style={{
width: `${100 / values.length}%`,
}}
>
{names ? (unit ? names[i] + unit : names[i]) : unit ? v + unit : v}
</button>
))}
</div>
</div>
);
};

export default ButtonGroup;
38 changes: 38 additions & 0 deletions components/ClaimAllRewardsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useMemo } from "react";
import { SecondaryButton } from "./Button";
import { useWallet } from "@solana/wallet-adapter-react";
import { useHeliumVsrState } from "@helium/voter-stake-registry-hooks";

export const ClaimAllRewardsButton: React.FC<{
className?: string;
onClick: () => void;
isLoading?: boolean;
}> = ({ className = "", onClick, isLoading = false }) => {
const { connected } = useWallet();
const { loading, positions } = useHeliumVsrState();

const positionsWithRewards = useMemo(
() => positions?.filter((p) => p.hasRewards),
[positions]
);

const tooltipContent = !connected
? "Connect your wallet to claim"
: !positionsWithRewards?.length
? "You don't have any positions with claimable rewards."
: "";

return (
<SecondaryButton
tooltipMessage={tooltipContent}
className={className}
disabled={!connected || loading || !positionsWithRewards?.length}
isLoading={isLoading}
onClick={onClick}
>
<div className="flex items-center">
<span>Claim All Rewards</span>
</div>
</SecondaryButton>
);
};
10 changes: 3 additions & 7 deletions components/ContentSection.js → components/ContentSection.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import classNames from "classnames";

const ContentSection = ({
children,
className,
flatBottom,
flatTop,
first,
}) => {
const ContentSection: React.FC<
React.PropsWithChildren<{ className?: string }>
> = ({ children, className }) => {
return (
<div className={classNames("max-w-5xl mx-auto px-4 sm:px-10", className)}>
{children}
Expand Down
15 changes: 8 additions & 7 deletions components/CopyableText.js → components/CopyableText.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useEffect, useState } from "react";
import classNames from "classnames";

const CopyableText = ({
textToCopy,
className,
iconClasses,
children,
customIcon,
}) => {
const CopyableText: React.FC<
React.PropsWithChildren<{
textToCopy: string;
className?: string;
iconClasses?: string;
customIcon?: React.ReactNode;
}>
> = ({ textToCopy, className, iconClasses, children, customIcon }) => {
const [successStatus, setSuccessStatus] = useState(false);

useEffect(() => {
Expand Down
13 changes: 9 additions & 4 deletions components/CountdownTimer.js → components/CountdownTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ import { addMinutes } from "date-fns";
import { useState } from "react";
import Countdown from "react-countdown";

const CountdownTimer = ({ blocksRemaining }) => {
const CountdownTimer: React.FC<{ endTs?: number; blocksRemaining?: number }> = ({ endTs, blocksRemaining }) => {
const now = new Date(Date.now());
const deadlineDate = addMinutes(now, parseInt(blocksRemaining));
let deadlineDate;
if (endTs) {
deadlineDate = new Date(endTs * 1000);
} else {
deadlineDate = addMinutes(now, blocksRemaining);
}

const [countdownCompleted, setCountdownCompleted] = useState(false);

if (countdownCompleted) {
return "Voting closed";
return <div>Voting closed</div>;
}

return (
<Countdown
key={deadlineDate}
key={deadlineDate.toString()}
date={deadlineDate}
renderer={({ days, hours, minutes, seconds, completed }) => {
if (completed) {
Expand Down
Loading

0 comments on commit 14c18bf

Please sign in to comment.