diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..e8ca4a068 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Deploy primodium interface + +on: + workflow_dispatch: + # pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.0 + + - uses: pnpm/action-setup@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build code + run: | + cat > .env <<-EOF + ${{vars.PRIMODIUM_ENV_FILE}} + EOF + pnpm install + pnpm build:client-core + + - name: Copy files to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "packages/client/dist" + debug: true + target: /home/deployer/primodium-interface + strip_components: 3 # Remove the first 3 levels of directories from the source path to copy only the files inside dist + rm: true + + - name: Deploy primodium interface to server + uses: appleboy/ssh-action@v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + debug: true + script_stop: true + script: | + chmod -R o+rX /home/deployer/primodium-interface + mv /home/deployer/primodium-interface /tmp + sudo -u primodium_interface bash -c ' + rm -rf /var/www/primodium-interface/* + cp -r /tmp/primodium-interface/* /var/www/primodium-interface + ' + rm -rf /tmp/primodium-interface + + \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9756e97d6..49b9cdcea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,9 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + with: + version: "v0.3.0" + - name: Build client bundle env: diff --git a/README.md b/README.md index dbca1952f..5b75ee880 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ This monorepo contains the entire stack for running Primodium, including the Rea ### Installation +[![Test suites for client and contracts](https://github.com/HappyChainDevs/primodium/actions/workflows/test.yml/badge.svg)](https://github.com/HappyChainDevs/primodium/actions/workflows/test.yml) + #### Prerequisites There are a few CLI tools to install to be compatible with the entire monorepo. diff --git a/packages/client/package.json b/packages/client/package.json index 9e6c42afd..ea50791b7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -15,7 +15,7 @@ "lint": "eslint ./src --ext .js,.ts,.tsx", "lint:fix": "eslint ./src --ext .js,.ts,.tsx --fix", "preview": "vite preview", - "test": "vitest" + "test": "vitest --environment jsdom" }, "dependencies": { "@amplitude/analytics-browser": "^2.1.3", @@ -38,6 +38,7 @@ "comlink": "^4.4.1", "contracts": "workspace:*", "framer-motion": "^10.12.18", + "jsdom": "^24.1.0", "lodash": "^4.17.21", "phaser": "^3.80.1", "react": "^18.2.0", @@ -56,6 +57,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "@types/lodash": "^4.14.195", + "@types/node": "^22.15.17", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/react-virtualized-auto-sizer": "^1.0.3", diff --git a/packages/client/src/components/transfer/Authorize.tsx b/packages/client/src/components/transfer/Authorize.tsx index 1707b943d..6973b6363 100644 --- a/packages/client/src/components/transfer/Authorize.tsx +++ b/packages/client/src/components/transfer/Authorize.tsx @@ -1,58 +1,36 @@ -import { useEffect, useState } from "react"; -import { FaClipboard, FaExclamationCircle, FaEye, FaEyeSlash, FaInfoCircle, FaTimes, FaUnlink } from "react-icons/fa"; -import { Address, Hex } from "viem"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { requestSessionKey } from "@happy.tech/core"; +import { useState } from "react"; +import { FaInfoCircle, FaTimes } from "react-icons/fa"; -import { STORAGE_PREFIX } from "@primodiumxyz/core"; import { useAccountClient } from "@primodiumxyz/core/react"; import { defaultEntity } from "@primodiumxyz/reactive-tables"; import { Button } from "@/components/core/Button"; import { SecondaryCard } from "@/components/core/Card"; import { TransactionQueueMask } from "@/components/shared/TransactionQueueMask"; -import { useContractCalls } from "@/hooks/useContractCalls"; -import { copyToClipboard } from "@/util/clipboard"; -import { findEntriesWithPrefix } from "@/util/localStorage"; +import { HAPPY_STORAGE_PREFIX, isHappySessionKeyRegistered } from "@/util/localStorage"; -const sessionWalletTooltip = - "Bypass annoying confirmation popups by authorizing a session account. This allows you to securely perform certain actions without external confirmation."; +const sessionWalletTooltip = ( + <> + Bypass confirmation popups by authorizing a session key. Powered by{" "} + Happy Wallet, this lets you securely perform actions without repeated + approvals. + +); export function Authorize() { - const { sessionAccount } = useAccountClient(); - const { grantAccessWithSignature, revokeAccess } = useContractCalls(); - const [showDetails, setShowDetails] = useState(false); + const { + playerAccount: { address: playerAddress, worldContract }, + } = useAccountClient(); const [showHelp, setShowHelp] = useState(!localStorage.getItem("hideHelp")); - useEffect(() => { - setShowDetails(false); - }, [sessionAccount]); + const weAreHappySponsored = isHappySessionKeyRegistered(playerAddress); - // Function to handle private key validation and connection - const sessionAddress = sessionAccount?.address; + const handleHappySessionKeyRegister = async () => { + if (!isHappySessionKeyRegistered(playerAddress)) { + await requestSessionKey(worldContract.address); - const submitPrivateKey = async (privateKey: Hex) => { - // Validate the private key format here - // This is a basic example, adjust the validation according to your requirements - const isValid = /^0x[a-fA-F0-9]{64}$/.test(privateKey); - if (!isValid) return; - const account = privateKeyToAccount(privateKey as Hex); - - if (sessionAddress && sessionAddress === account.address) return; - else await grantAccessWithSignature(privateKey, { id: defaultEntity }); - }; - - const handleRandomPress = () => { - const storedKeys = findEntriesWithPrefix(); - const privateKey = storedKeys.length > 0 ? storedKeys[0].privateKey : generatePrivateKey(); - - const account = privateKeyToAccount(privateKey as Hex); - localStorage.setItem(STORAGE_PREFIX + account.address, privateKey); - - return privateKey; - }; - - const removeSessionKey = async (publicKey: Address) => { - await revokeAccess(publicKey); - localStorage.removeItem(STORAGE_PREFIX + publicKey); + localStorage.setItem(HAPPY_STORAGE_PREFIX + playerAddress, "true"); + } else return; }; const hideHelp = () => { @@ -73,75 +51,17 @@ export function Authorize() { )} - {sessionAddress ? ( + {weAreHappySponsored ? (
-

AUTHORIZING

-
- - -
+

+ 🤠 SESSION KEY REGISTERED 🤠 +

- {showDetails && ( - -
-
-
- Session Address: - - -
-

{sessionAddress}

-
-
-
- )}
) : ( - )}
diff --git a/packages/client/src/screens/Enter.tsx b/packages/client/src/screens/Enter.tsx index 59db0889d..0ef3ed79b 100644 --- a/packages/client/src/screens/Enter.tsx +++ b/packages/client/src/screens/Enter.tsx @@ -1,32 +1,32 @@ +import { requestSessionKey } from "@happy.tech/core"; import { useEffect, useState } from "react"; import { FaExclamationTriangle, FaInfoCircle } from "react-icons/fa"; import { useLocation, useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { STORAGE_PREFIX } from "@primodiumxyz/core"; import { useAccountClient, useCore } from "@primodiumxyz/core/react"; import { defaultEntity } from "@primodiumxyz/reactive-tables"; import { Tooltip } from "@/components/core/Tooltip"; import { TransactionQueueMask } from "@/components/shared/TransactionQueueMask"; import { useContractCalls } from "@/hooks/useContractCalls"; -import { findEntriesWithPrefix } from "@/util/localStorage"; +import { HAPPY_STORAGE_PREFIX, isHappySessionKeyRegistered } from "@/util/localStorage"; import { Landing } from "./Landing"; export const Enter: React.FC = () => { const { tables } = useCore(); const { - playerAccount: { entity: playerEntity }, + playerAccount: { address: playerAddress, worldContract, entity: playerEntity }, sessionAccount, } = useAccountClient(); - const { grantAccessWithSignature, spawn } = useContractCalls(); + const { spawn } = useContractCalls(); const navigate = useNavigate(); const location = useLocation(); const [showingToast, setShowingToast] = useState(false); const [state, setState] = useState<"loading" | "delegate" | "play">("loading"); + const confirmSkip = async () => { toast.dismiss(); if (showingToast) await new Promise((resolve) => setTimeout(resolve, 500)); @@ -37,6 +37,9 @@ export const Enter: React.FC = () => {
Are you sure you want to skip? You will need to confirm every action with your external wallet. +
+
+ You can still enable a session key within the game settings.
@@ -72,8 +75,9 @@ export const Enter: React.FC = () => { }, ); }; + useEffect(() => { - if (!sessionAccount) { + if (!isHappySessionKeyRegistered(playerAddress)) { setState("delegate"); } else { setState("play"); @@ -93,13 +97,14 @@ export const Enter: React.FC = () => { navigate("/game" + location.search); }; - const handleDelegate = async () => { - const storedKeys = findEntriesWithPrefix(); - const privateKey = storedKeys.length > 0 ? storedKeys[0].privateKey : generatePrivateKey(); - const account = privateKeyToAccount(privateKey); - localStorage.setItem(STORAGE_PREFIX + account.address, privateKey); + const handleHappySessionKeyRegister = async () => { + if (!isHappySessionKeyRegistered(playerAddress)) { + // once this resolves, write a status into local storage that this has been done + await requestSessionKey(worldContract.address); - await grantAccessWithSignature(privateKey, { id: defaultEntity }); + localStorage.setItem(HAPPY_STORAGE_PREFIX + playerAddress, "true"); + setState("play"); + } else return; }; return ( @@ -108,7 +113,7 @@ export const Enter: React.FC = () => { {state === "delegate" && (