Skip to content

Commit

Permalink
Add changes to changeset (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
escottalexander authored Sep 30, 2024
2 parents 555115a + 90f9d22 commit 71be733
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 76 deletions.
57 changes: 16 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,25 @@
Test your skills and find some new ones by completing challenges.

There are three different types of nodes on the tree:
- Challenges: A repository that poses a problem that you must solve with Solidity. You deploy your contract and submit your contract address so we can test it to ensure your solution works, allowing you to progress.
- Quizzes: Links to source material that will help you to master a topic that will be encountered in later challenges.
- Capstone Projects: These are large scale projects that stretch your knowledge about the ecosystem. A description of the project is provided but it is up to you to fulfill the description.
- [x] Challenges: A repository that poses a problem that you must solve with Solidity. You deploy your contract and submit your contract address so we can test it to ensure your solution works, allowing you to progress.
- [ ] Quizzes: Links to source material that will help you to master a topic that will be encountered in later challenges.
- [ ] Capstone Projects: These are large scale projects that stretch your knowledge about the ecosystem. A description of the project is provided but it is up to you to fulfill the description.

## Quick Start
To run this CLI application
Run the following command to use the NPM package
```bash
npx eth-tech-tree
```
The CLI visualizes several categories which contain challenges. Navigate with your arrow keys and hit enter to view options for a challenge. Follow the instructions in your CLI to complete challenges fill out your Ethereum dev tech skills.

## Development
Clone and `cd` into the repo then run this CLI application with the following commands
- `yarn install`
- `yarn build`
- `yarn cli`

If you are actively developing you may find it helpful to run the build and cli commands together each time since you have to build for your changes to be present.
- `yarn build && yarn cli`

## CLI (conceptualized)
```vbnet
Governance
├─ Token Voting ♟️ - LVL 1
│ └─ DAO governance proposals and Voting ♟️ - LVL 2
│ ├─ Moloch Rage quit ♟️ - LVL 2
❯ │ │ └─ OZ Governor ♟️ - LVL 2
│ └─ Offchain Voting ♟️ - LVL 3
├─ The DAO (for context) 📖 - LVL 1
(Move up and down to reveal more choices)
```

## MVP
- ETT CLI enables users to see the full scope of the tech tree
- Challenges are locked/unlocked based on a users progress
- It shows them their proficiency in each branch of the tree based on challenges completed.
- They can download a challenge locally
- They can deploy and verify their contract with very few steps
- They can submit the contract address for the challenge and get real-time feedback
- There are 5 - 10 challenges available. We will limit to a few branches at the start
- CLI will show leaderboard

## Future Ambitions
- Issue onchain attestation for completion of the challenge
- Integrate with BuidlGuidl app to show their completed challenges
- Front end that shows their progress and a leaderboard - phase 1
- Front end that offers full functionality outside of the ETT CLI - phase 2
- Add the rest of challenges + add new ones + add new branches
- Add the other challenge types ("references" which require quizzes, "personal-challenges" which are very big tasks with little instruction - hard for us to test)

## Ideas
- Add a way for users to prove they have mastered certain concepts so that they can unlock harder challenges earlier
- Granular testing so that a user is awarded points based on additional tests (also may need to allow users to go back and resubmit challenges)
- Capture the flag type NFT that a user can "steal" when they are the new leader. Might consider 1st, 2nd, 3rd place NFTs. Announcing a steal on social media would be great for encouraging user engagement
## TODO
- [ ] Show users how many challenges they have completed in a category
- [ ] Show users where they rank on a leaderboard
- [ ] Onchain NFT mint or attestations showing a user has completed certain challenges
- [ ] Enable Gas Efficiency CTF element
21 changes: 20 additions & 1 deletion src/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,29 @@ export const fetchChallenges = async () => {
}
};

/**
* Get User
*/
export const getUser = async (identifier: string) => {
try {
const response = await fetch(`${API_URL}/user/${identifier}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
return data.user;
} catch (error) {
console.error('Error:', error);
return {};
}
};

/**
* Create User
*/
export const createUser = async (userData: { address?: string, ens?: string }) => {
export const upsertUser = async (userData: { address?: string, ens?: string, deviceInstallLocation: { [device: string]: string } }) => {
try {
const response = await fetch(`${API_URL}/user`, {
method: 'POST',
Expand Down
67 changes: 35 additions & 32 deletions src/tasks/prompt-for-missing-user-state.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import fs from "fs";
import { createUser } from "../modules/api";
import { getUser, upsertUser } from "../modules/api";
import {
UserState
} from "../types";
import inquirer from "inquirer";
import { saveUserState } from "../utils/stateManager";
import { isValidAddress, isValidAddressOrENS } from "../utils/helpers";
import { isValidAddressOrENS, getDevice, checkValidPathOrCreate, isValidAddress } from "../utils/helpers";

// default values for unspecified args
const defaultOptions: Partial<UserState> = {
Expand All @@ -15,53 +14,57 @@ const defaultOptions: Partial<UserState> = {
export async function promptForMissingUserState(
userState: UserState
): Promise<UserState> {
const cliAnswers = Object.fromEntries(
Object.entries(userState).filter(([key, value]) => value !== null)
);
const questions = [];
const userDevice = getDevice();
let identifier = userState.address;

if (!userState.address) {
questions.push({
const answer = await inquirer.prompt({
type: "input",
name: "address",
name: "identifier",
message: "Your wallet address (or ENS):",
validate: isValidAddressOrENS,
});

identifier = answer.identifier;
}

// Fetch the user data from the server - also handles ens resolution
let user = await getUser(identifier as string);
const newUser = !user?.address;
const existingInstallLocation = user?.installLocations?.find((loc: {location: string, device: string}) => loc.device === userDevice);

if (!userState.installLocation) {
questions.push({
// New user
if (newUser) {
if (isValidAddress(identifier as string)) {
user.address = identifier as string;
} else {
user.ens = identifier as string;
}
}

// Prompt for install location if it doesn't exist on device
if (!existingInstallLocation) {
const answer = await inquirer.prompt({
type: "input",
name: "installLocation",
message: "Where would you like to download the challenges?",
default: defaultOptions.installLocation,
validate: (value: string) => fs.lstatSync(value).isDirectory()
,
validate: checkValidPathOrCreate,
});
}

const answers = await inquirer.prompt(questions, cliAnswers);

// Fetch the user data from the server (create a new user if it doesn't exist) - also handles ens resolution
const user = await fetchUser(answers.address);
const newState = { ...answers, ...user };
// Create (or update) the user with their preferred install location for this device
user.location = answer.installLocation;
user.device = userDevice;
user = await upsertUser(user);
}

const { address, ens, installLocations } = user;
const thisDeviceLocation = installLocations.find((loc: {location: string, device: string}) => loc.device === userDevice);
const newState = { address, ens, installLocation: thisDeviceLocation.location };
if (JSON.stringify(userState) !== JSON.stringify(newState)) {
// Save the new state locally
await saveUserState(newState);
}

return newState;
}

export async function fetchUser(userResponse: string): Promise<UserState> {
const body: { address?: string, ens?: string } = {};
if (isValidAddress(userResponse)) {
body["address"] = userResponse;
} else {
body["ens"] = userResponse;
}
// TODO: handle no returned data (no connection or error)
const user = await createUser(body);

return user;
};
33 changes: 31 additions & 2 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import inquirer from "inquirer";
import os from "os";
import fs from "fs";

export function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand All @@ -12,10 +14,37 @@ export async function pressEnterToContinue(customMessage?: string) {
});
}

export const checkValidPathOrCreate = async (path: string) => {
try {
const exists = fs.lstatSync(path).isDirectory();
if (!exists) {
console.log('That path is not a directory');
return false;
}
return true;
} catch (error) {
// Try to create the directory
try {
fs.mkdirSync(path);
return true;
} catch (error) {
console.error('Error creating directory:', error);
return false;
}
}
};

export const isValidAddress = (value: string): boolean => {
return /^0x[a-fA-F0-9]{40}$/.test(value)
};
};

export const isValidAddressOrENS = (value: string): boolean => {
return /^(0x[a-fA-F0-9]{40}|.+\.eth)$/.test(value);
};
};

export const getDevice = (): string => {
const hostname = os.hostname();
const platform = os.platform();
const arch = os.arch();
return `${hostname}(${platform}:${arch})`;
}

0 comments on commit 71be733

Please sign in to comment.