diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml new file mode 100644 index 000000000..98f41b2c0 --- /dev/null +++ b/.github/workflows/demo.yaml @@ -0,0 +1,64 @@ +name: scaffold-stark-demo workflow + +on: + pull_request: + types: [closed] + branches: [main] + paths: + - 'packages/nextjs/**' + +jobs: + version-bump-nextjs: + runs-on: ubuntu-22.04 + steps: + + - name: Checkout Source Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/scaffold-stark-2 + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: source_repo + + - name: Modify scaffoldConfig in Source Repository + run: | + cd source_repo + sed -i 's/targetNetworks: \[chains.devnet\]/targetNetworks: \[chains.sepolia\]/' packages/nextjs/scaffold.config.ts + cat packages/nextjs/scaffold.config.ts + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://registry.yarnpkg.com' + + - name: Deploy to vercel + if: success() + id: deploy + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + cd source_repo + yarn install + vercel link --yes --project $VERCEL_PROJECT_ID --token $VERCEL_TOKEN --scope $VERCEL_ORG_ID + vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true --prod --token $VERCEL_TOKEN --scope $VERCEL_ORG_ID + + - name: Notify Slack on Success + if: success() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub deployed to vercel result: ${{ job.status }}\nRepository Name: ${{ github.repository }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Notify Slack on Failure + if: failure() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub deployed to vercel result: ${{ job.status }}\nRepository Name: ${{ github.repository }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + diff --git a/.github/workflows/release-create-stark.yaml b/.github/workflows/release-create-stark.yaml new file mode 100644 index 000000000..47c698ccf --- /dev/null +++ b/.github/workflows/release-create-stark.yaml @@ -0,0 +1,114 @@ +name: Version Bump and Notify + +on: + pull_request: + types: [closed] + branches: [main] + +jobs: + version-bump: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout Source Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/scaffold-stark-2 + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: source_repo + + - name: Checkout Destination Repository + uses: actions/checkout@v4 + with: + repository: Quantum3-Labs/create-stark + token: ${{ secrets.ORG_GITHUB_TOKEN }} + path: destination_repo + + - name: Determine version bump type + id: version + run: | + cd source_repo + commit_message=$(git log -1 --pretty=%B) + if [[ "$commit_message" == *"[major]"* ]]; then + echo "type=major" >> "$GITHUB_ENV" + elif [[ "$commit_message" == *"[minor]"* ]]; then + echo "type=minor" >> "$GITHUB_ENV" + else + echo "type=patch" >> "$GITHUB_ENV" + fi + + - name: Bump version in Source Repository + id: bump-version-source + run: | + cd source_repo + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + new_version=$(npm version ${{ env.type }} -m "chore(release): %s [skip ci]") + echo "NEW_VERSION=${new_version}" >> "$GITHUB_ENV" + git push origin main --follow-tags + + - name: Copy Files to Destination Repository + run: | + rsync -av --delete --exclude='.git' source_repo/ destination_repo/templates/base + cd destination_repo + git add . + git commit -m "chore: sync files from scaffold-stark-2 [skip ci]" + + - name: Format .gitignore files + run: | + find destination_repo/templates/base -type f -name ".gitignore" | while read -r gitignore_file; do + mjs_file="${gitignore_file%/*}/.gitignore.template.mjs" + gitignore_content=$(cat "$gitignore_file") + cat > "$mjs_file" <<-EOF + const contents = () => + \`${gitignore_content}\` + + export default contents; + EOF + rm "$gitignore_file" + done + cd destination_repo + git add . + git commit -m "Processed $gitignore_file into $mjs_file" + + - name: Bump version in Destination Repository + id: bump-version-destination + run: | + cd destination_repo + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + new_version=$(npm version ${{ env.type }} -m "chore(release): %s [skip ci]") + git push origin main --follow-tags + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org/' + + - name: Publish release + if: success() + id: publish-release + run: | + cd destination_repo + npm install && npm run build && npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Notify Slack on Success + if: success() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub Action succeeded for version bump to ${{ env.NEW_VERSION }}." + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Notify Slack on Failure + if: failure() + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + slack-message: "GitHub Action failed for version bump." + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/test_contract.yml b/.github/workflows/test_contract.yml index b6a5b83ef..3424b5ed8 100644 --- a/.github/workflows/test_contract.yml +++ b/.github/workflows/test_contract.yml @@ -16,13 +16,13 @@ jobs: uses: actions/checkout@master - name: Install scarb - run: curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.5.4 + run: curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.6.5 - name: Install snfoundryup run: curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh - name: Install snforge - run: snfoundryup -v 0.25.0 + run: snfoundryup -v 0.27.0 - name: Run snforge tests run: snforge test diff --git a/.gitmodules b/.gitmodules index bbe18cc7f..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "packages/snfoundry/local-devnet"] - path = packages/snfoundry/local-devnet - url = https://github.com/0xSpaceShard/starknet-devnet-rs - branch = json-rpc-v0.5.1 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89a319a02..a6e589a20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,4 +37,4 @@ If your changes involve updates to how users interact with Scaffold-Stark or Spe ## Need Help? -Reach out via our community channels if you encounter issues or need clarification on contributing. +Reach out via our community channels if you encounter issues or need clarification on contributing [here](https://t.me/+wO3PtlRAreo4MDI9). diff --git a/README.md b/README.md index a3f3f32e3..2c89b234c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Make sure you have the compatible versions otherwise refer to [Scaffold-Stark Re Then download the challenge to your computer and install dependencies by running: ```sh -git clone https://github.com/Quantum3-Labs/speedrunstark.git --recurse-submodules {challengeName} +git clone https://github.com/Quantum3-Labs/speedrunstark.git {challengeName} cd {challengeName} git checkout {challengeName} yarn install diff --git a/package.json b/package.json index 993eeba83..e21e893ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ss-2", - "version": "0.0.2", + "version": "0.2.3", "author": "Q3 Labs", "license": "MIT", "private": true, diff --git a/packages/nextjs/.env.example b/packages/nextjs/.env.example index 7b8be8bee..10cffdbc0 100644 --- a/packages/nextjs/.env.example +++ b/packages/nextjs/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_PROVIDER_URL=https://starknet-sepolia.infura.io/v3/c45bd0ce3e584ba4a5e6a5928c9c0b0f \ No newline at end of file +NEXT_PUBLIC_PROVIDER_URL=https://starknet-sepolia.public.blastapi.io/rpc/v0_7 \ No newline at end of file diff --git a/packages/nextjs/app/debug/_components/contract/Array.tsx b/packages/nextjs/app/debug/_components/contract/Array.tsx new file mode 100644 index 000000000..d5803960f --- /dev/null +++ b/packages/nextjs/app/debug/_components/contract/Array.tsx @@ -0,0 +1,129 @@ +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; +import { getFunctionInputKey, getInitialTupleFormState } from "./utilsContract"; +import { + AbiEnum, + AbiParameter, + AbiStruct, +} from "~~/utils/scaffold-stark/contract"; +import { replacer } from "~~/utils/scaffold-stark/common"; +import { ContractInput } from "./ContractInput"; +import { Abi } from "abi-wan-kanabi"; +import { parseGenericType } from "~~/utils/scaffold-stark"; + +type ArrayProps = { + abi: Abi; + abiParameter: AbiParameter; + parentForm: Record | undefined; + setParentForm: (form: Record) => void; + parentStateObjectKey: string; + setFormErrorMessage: Dispatch>; +}; + +export const ArrayInput = ({ + abi, + parentForm, + setParentForm, + parentStateObjectKey, + abiParameter, + setFormErrorMessage, +}: ArrayProps) => { + // array in object representation + const [inputArr, setInputArr] = useState({}); + const [arrLength, setArrLength] = useState(-1); + + const elementType = useMemo(() => { + const parsed = parseGenericType(abiParameter.type); + return Array.isArray(parsed) ? parsed[0] : parsed; + }, [abiParameter.type]); + + // side effect to transform data before setState + useEffect(() => { + // non empty objects only + setParentForm({ + ...parentForm, + [parentStateObjectKey]: Object.values(inputArr).filter( + (item) => item !== null, + ), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(inputArr, replacer)]); + + return ( +
+
+ +
+

array (length: {arrLength + 1})

+
+
+ {/* do note here that the "index" are basically array keys */} + {Object.keys(inputArr).map((index) => { + return ( + + | ((arg: Record) => void), + ) => { + let nextInputObject: Record = nextInputRecipe; + + // set state recipe function, handle + if (typeof nextInputRecipe === "function") { + nextInputObject = nextInputRecipe(parentForm!); + } + + const currentInputArray = { ...inputArr }; + + // we do some nasty workaround + currentInputArray[index] = + nextInputObject?.[`input_${index}`] || null; + + setInputArr(currentInputArray); + }} + form={inputArr[index]} + stateObjectKey={`input_${index}`} + paramType={ + { + name: `${abiParameter.name}[${index}]`, + type: elementType, + } as AbiParameter + } + setFormErrorMessage={setFormErrorMessage} + /> + ); + })} +
+ + +
+
+
+
+ ); +}; diff --git a/packages/nextjs/app/debug/_components/contract/ContractInput.tsx b/packages/nextjs/app/debug/_components/contract/ContractInput.tsx index bd61256ec..15c4d02be 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractInput.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractInput.tsx @@ -8,21 +8,29 @@ import { isCairoArray, isCairoBigInt, isCairoInt, + isCairoType, isCairoU256, } from "~~/utils/scaffold-stark"; +import { Struct } from "./Struct"; +import { Abi } from "abi-wan-kanabi"; +import { ArrayInput } from "./Array"; type ContractInputProps = { + abi?: Abi; setForm: Dispatch>>; form: Record | undefined; stateObjectKey: string; paramType: AbiParameter; + setFormErrorMessage: Dispatch>; }; export const ContractInput = ({ + abi, setForm, form, stateObjectKey, paramType, + setFormErrorMessage, }: ContractInputProps) => { const inputProps = { name: stateObjectKey, @@ -39,18 +47,40 @@ export const ContractInput = ({ }; const renderInput = () => { - switch (paramType.type) { - default: - if ( - !isCairoArray(paramType.type) && - (isCairoInt(paramType.type) || - isCairoBigInt(paramType.type) || - isCairoU256(paramType.type)) - ) { - return ; - } else { - return ; - } + if (isCairoArray(paramType.type)) { + return ( + + ); + } else if ( + isCairoInt(paramType.type) || + isCairoBigInt(paramType.type) || + isCairoU256(paramType.type) + ) { + return ; + } else if (isCairoType(paramType.type)) { + return ; + } else { + return ( + member.name === paramType.type, + )} + /> + ); } }; diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx index ea256671e..53d4bb23a 100644 --- a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { Abi } from "abi-wan-kanabi"; import { Address } from "@starknet-react/chains"; import { @@ -30,14 +30,16 @@ export const ReadOnlyFunctionForm = ({ getInitialFormState(abiFunction), ); const [inputValue, setInputValue] = useState(undefined); + const [formErrorMessage, setFormErrorMessage] = useState(null); const lastForm = useRef(form); const { isFetching, data, refetch } = useContractRead({ address: contractAddress, functionName: abiFunction.name, abi: [...abi], - args: inputValue ? inputValue.flat() : [], + args: inputValue ? inputValue.flat(Infinity) : [], enabled: false, + parseArgs: false, blockIdentifier: "pending" as BlockNumber, }); @@ -46,23 +48,24 @@ export const ReadOnlyFunctionForm = ({ const key = getFunctionInputKey(abiFunction.name, input, inputIndex); return ( ); }); - const handleRead = async () => { - const newInputValue = getParsedContractFunctionArgs(form, true); - if (JSON.stringify(form) === JSON.stringify(lastForm.current)) { - await refetch(); - } else { + const handleRead = () => { + const newInputValue = getParsedContractFunctionArgs(form, false); + if (JSON.stringify(form) !== JSON.stringify(lastForm.current)) { setInputValue(newInputValue); lastForm.current = form; } + refetch(); }; return ( @@ -82,16 +85,24 @@ export const ReadOnlyFunctionForm = ({ )} - + + ); diff --git a/packages/nextjs/app/debug/_components/contract/Struct.tsx b/packages/nextjs/app/debug/_components/contract/Struct.tsx new file mode 100644 index 000000000..ec7f236ab --- /dev/null +++ b/packages/nextjs/app/debug/_components/contract/Struct.tsx @@ -0,0 +1,119 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { getFunctionInputKey, getInitialTupleFormState } from "./utilsContract"; +import { AbiEnum, AbiStruct } from "~~/utils/scaffold-stark/contract"; +import { replacer } from "~~/utils/scaffold-stark/common"; +import { ContractInput } from "./ContractInput"; +import { Abi } from "abi-wan-kanabi"; + +type StructProps = { + abi?: Abi; + parentForm: Record | undefined; + setParentForm: (form: Record) => void; + parentStateObjectKey: string; + abiMember?: AbiStruct | AbiEnum; + setFormErrorMessage: Dispatch>; +}; + +export const Struct = ({ + parentForm, + setParentForm, + parentStateObjectKey, + abiMember, + abi, + setFormErrorMessage, +}: StructProps) => { + const [form, setForm] = useState>(() => + getInitialTupleFormState( + abiMember ?? { type: "struct", name: "", members: [] }, + ), + ); + + // side effect to transform data before setState + useEffect(() => { + const values = Object.values(form); + const argsStruct: Record = {}; + if (!abiMember) return; + + if (abiMember.type === "struct") { + abiMember.members.forEach((member, index) => { + argsStruct[member.name || `input_${index}_`] = { + type: member.type, + value: values[index], + }; + }); + } else { + abiMember.variants.forEach((variant, index) => { + argsStruct[variant.name || `input_${index}_`] = { + type: variant.type, + value: values[index], + }; + }); + + // check for enum validity + if (values.filter((item) => (item || "").length > 0).length > 1) { + setFormErrorMessage("Enums can only have one defined value"); + } else { + setFormErrorMessage(null); + } + } + + setParentForm({ + ...parentForm, + [parentStateObjectKey]: + abiMember.type === "struct" ? argsStruct : { variant: argsStruct }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [abiMember, JSON.stringify(form, replacer)]); + + if (!abiMember) return null; + + return ( +
+
+ +
+

{abiMember.type}

+
+
+ {abiMember.type === "struct" + ? abiMember.members.map((member, index) => { + const key = getFunctionInputKey( + abiMember.name || "struct", + member, + index, + ); + return ( + + ); + }) + : abiMember.variants.map((variant, index) => { + const key = getFunctionInputKey( + abiMember.name || "tuple", + variant, + index, + ); + return ( + + ); + })} +
+
+
+ ); +}; diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx index 725ca6db2..6ac497690 100644 --- a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx +++ b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx @@ -10,6 +10,7 @@ import { } from "~~/app/debug/_components/contract"; import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; import { + useAccount, useContractWrite, useNetwork, useWaitForTransaction, @@ -37,10 +38,22 @@ export const WriteOnlyFunctionForm = ({ const [form, setForm] = useState>(() => getInitialFormState(abiFunction), ); + const [formErrorMessage, setFormErrorMessage] = useState(null); + const { status: walletStatus } = useAccount(); const { chain } = useNetwork(); const writeTxn = useTransactor(); const { targetNetwork } = useTargetNetwork(); - const writeDisabled = !chain || chain?.network !== targetNetwork.network; + const writeDisabled = + !chain || + chain?.network !== targetNetwork.network || + walletStatus === "disconnected"; + + // side effect to update error state when not connected + useEffect(() => { + setFormErrorMessage( + writeDisabled ? "Wallet not connected or in the wrong network" : null, + ); + }, [writeDisabled]); const { data: result, @@ -51,7 +64,9 @@ export const WriteOnlyFunctionForm = ({ { contractAddress, entrypoint: abiFunction.name, - calldata: getParsedContractFunctionArgs(form, false).flat(), + + // use infinity to completely flatten array from n dimensions to 1 dimension + calldata: getParsedContractFunctionArgs(form, false).flat(Infinity), }, ], }); @@ -90,6 +105,7 @@ export const WriteOnlyFunctionForm = ({ const key = getFunctionInputKey(abiFunction.name, input, inputIndex); return ( { setDisplayedTxResult(undefined); @@ -98,6 +114,7 @@ export const WriteOnlyFunctionForm = ({ form={form} stateObjectKey={key} paramType={input} + setFormErrorMessage={setFormErrorMessage} /> ); }); @@ -124,16 +141,14 @@ export const WriteOnlyFunctionForm = ({ )}