From 27ba7642a0494ffcfd0e4d0d445ce92c9cc93d08 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Fri, 27 Sep 2024 17:02:10 -0400 Subject: [PATCH 1/3] add new flow to handle same user on different devices --- src/modules/api.ts | 21 ++++++++- src/tasks/prompt-for-missing-user-state.ts | 55 ++++++++++------------ src/utils/helpers.ts | 33 ++++++++++++- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/modules/api.ts b/src/modules/api.ts index 06937ce..3c2f147 100644 --- a/src/modules/api.ts +++ b/src/modules/api.ts @@ -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', diff --git a/src/tasks/prompt-for-missing-user-state.ts b/src/tasks/prompt-for-missing-user-state.ts index 2f937bc..4f49d10 100644 --- a/src/tasks/prompt-for-missing-user-state.ts +++ b/src/tasks/prompt-for-missing-user-state.ts @@ -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 } from "../utils/helpers"; // default values for unspecified args const defaultOptions: Partial = { @@ -15,36 +14,43 @@ const defaultOptions: Partial = { export async function promptForMissingUserState( userState: UserState ): Promise { - const cliAnswers = Object.fromEntries( - Object.entries(userState).filter(([key, value]) => value !== null) - ); - const questions = []; + const userDevice = getDevice(); + let userAddress = userState.address; if (!userState.address) { - questions.push({ + const answer = await inquirer.prompt({ type: "input", name: "address", message: "Your wallet address (or ENS):", validate: isValidAddressOrENS, }); + + userAddress = answer.address; } + + // Fetch the user data from the server - also handles ens resolution + let user = await getUser(userAddress as string); - if (!userState.installLocation) { - questions.push({ + const existingInstallLocation = user?.installLocations?.find((loc: {location: string, device: string}) => loc.device === userDevice); + // New user + 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); @@ -52,16 +58,3 @@ export async function promptForMissingUserState( return newState; } - -export async function fetchUser(userResponse: string): Promise { - 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; -}; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 8a849e2..afad31c 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -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)); @@ -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); - }; \ No newline at end of file +}; + +export const getDevice = (): string => { + const hostname = os.hostname(); + const platform = os.platform(); + const arch = os.arch(); + return `${hostname}(${platform}:${arch})`; +} \ No newline at end of file From 5fc43e8f70f40c491384d86fbd2cc5d368010ef5 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Fri, 27 Sep 2024 17:02:17 -0400 Subject: [PATCH 2/3] update readme --- README.md | 57 ++++++++++++++++--------------------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e19ed63..3fa81cf 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +## 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 \ No newline at end of file From d22f38cef29091fa2d46bc10e161c187d0a381f6 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Mon, 30 Sep 2024 11:52:15 -0400 Subject: [PATCH 3/3] get new user and check for install location --- src/modules/api.ts | 2 +- src/tasks/prompt-for-missing-user-state.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/modules/api.ts b/src/modules/api.ts index 3c2f147..9817514 100644 --- a/src/modules/api.ts +++ b/src/modules/api.ts @@ -36,7 +36,7 @@ export const getUser = async (identifier: string) => { /** * Create User */ -export const upsertUser = async (userData: { address: string, ens?: string, deviceInstallLocation: { [device: string]: string } }) => { +export const upsertUser = async (userData: { address?: string, ens?: string, deviceInstallLocation: { [device: string]: string } }) => { try { const response = await fetch(`${API_URL}/user`, { method: 'POST', diff --git a/src/tasks/prompt-for-missing-user-state.ts b/src/tasks/prompt-for-missing-user-state.ts index 4f49d10..c50fbc2 100644 --- a/src/tasks/prompt-for-missing-user-state.ts +++ b/src/tasks/prompt-for-missing-user-state.ts @@ -4,7 +4,7 @@ import { } from "../types"; import inquirer from "inquirer"; import { saveUserState } from "../utils/stateManager"; -import { isValidAddressOrENS, getDevice, checkValidPathOrCreate } from "../utils/helpers"; +import { isValidAddressOrENS, getDevice, checkValidPathOrCreate, isValidAddress } from "../utils/helpers"; // default values for unspecified args const defaultOptions: Partial = { @@ -15,24 +15,34 @@ export async function promptForMissingUserState( userState: UserState ): Promise { const userDevice = getDevice(); - let userAddress = userState.address; + let identifier = userState.address; if (!userState.address) { const answer = await inquirer.prompt({ type: "input", - name: "address", + name: "identifier", message: "Your wallet address (or ENS):", validate: isValidAddressOrENS, }); - userAddress = answer.address; + identifier = answer.identifier; } // Fetch the user data from the server - also handles ens resolution - let user = await getUser(userAddress as string); - + let user = await getUser(identifier as string); + const newUser = !user?.address; const existingInstallLocation = user?.installLocations?.find((loc: {location: string, device: string}) => loc.device === userDevice); + // 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",