diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 63ade28..d710f3d 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -27,17 +27,25 @@ jobs: node-version: ${{ matrix.node }} cache: yarn + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Install dependencies run: yarn install --immutable - - name: Run hardhat node, deploy contracts (& generate contracts typescript output) - run: yarn chain & yarn deploy + - name: Compile contracts + run: | + cd packages/hardhat + yarn hardhat clean + yarn compile - name: Run nextjs lint - run: yarn next:lint --max-warnings=0 + run: yarn next:lint - name: Check typings on nextjs run: yarn next:check-types - name: Run hardhat lint - run: yarn hardhat:lint --max-warnings=0 \ No newline at end of file + run: yarn hardhat:lint \ No newline at end of file diff --git a/README.md b/README.md index 0eec9dc..9fef62f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,46 @@ -# fil-frame -Quickstart your Filecoin dApp using this open source dev stack. +# FIL-Frame 🚀 -Tutorial video: https://youtu.be/dzg7ygwAp1Q +Welcome to FIL-Frame, a starter repository designed to help developers quickly get started with building decentralized applications (dApps) on the Filecoin network. This repository provides various integration options, including an example template using Lighthouse. -Axelarscan (testnet): https://testnet.axelarscan.io/gmp/search?sourceChain=filecoin-2 +## Table of Contents 📚 -Filecoin Calibration faucet: https://faucet.calibnet.chainsafe-fil.io/ +- Overview +- Getting Started + - Prerequisites + - Installation + - Configuration +- Usage + - Deploying smart contracts + - Running the frontend +- Storage Onramp Options + - Lighthouse + - Storacha +- Project Structure +- Contribution guidelines +- License -# Getting Started +## Overview 🌐 + +FIL-Frame is a monorepo that includes two main packages: + +`hardhat`: Manages the blockchain-related aspects, including smart contract development, deployment, and testing. + +`nextjs`: Handles the frontend and API aspects of the project using Next.js. + +This repository is designed to be a quickstart for developers new to the Filecoin ecosystem, providing various integration options to suit different needs. + +## Getting Started 🚀 + +### Prerequisites 📋 + +Before you begin, ensure you have the following installed: +- [Node.js](https://nodejs.org/en/download/package-manager) (v20.9.0) +- [Yarn](https://yarnpkg.com/getting-started/install) +- [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started#installation) + +### Installation 💻 + +### From source code 1. Clone the repository: diff --git a/package.json b/package.json index b32eb4e..78c2d93 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "fork": "yarn workspace @fil-frame/hardhat fork", "generate": "yarn workspace @fil-frame/hardhat run scripts/generateAccount.ts", "flatten": "yarn workspace @fil-frame/hardhat flatten", - "lint": "yarn workspace @fil-frame/hardhat eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", + "next:lint": "yarn workspace @fil-frame/hardhat eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", + "next:check-types": "yarn workspace @fil-frame/nextjs check-types", "lint-staged": "yarn workspace @fil-frame/hardhat eslint --config ./.eslintrc.json --ignore-path ./.eslintignore", "format": "yarn workspace @fil-frame/hardhat prettier --write ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "verify": "yarn workspace @fil-frame/hardhat etherscan-verify", "hardhat-verify": "yarn workspace @fil-frame/hardhat verify", + "hardhat:lint": "yarn workspace @fil-frame/hardhat lint", "deploy": "yarn workspace @fil-frame/hardhat deploy", "deploy:verify": "yarn workspace @fil-frame/hardhat deploy:verify", "chain": "yarn workspace @fil-frame/hardhat chain", diff --git a/packages/hardhat/.eslintignore b/packages/hardhat/.eslintignore new file mode 100644 index 0000000..faef36d --- /dev/null +++ b/packages/hardhat/.eslintignore @@ -0,0 +1,8 @@ +# folders +artifacts +cache +contracts +node_modules/ +typechain-types +# files +**/*.json \ No newline at end of file diff --git a/packages/hardhat/.eslintrc.json b/packages/hardhat/.eslintrc.json new file mode 100644 index 0000000..edb3c31 --- /dev/null +++ b/packages/hardhat/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "node": true + }, + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:prettier/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-explicit-any": "off", + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto" + } + ] + } +} \ No newline at end of file diff --git a/packages/hardhat/.prettierrc.json b/packages/hardhat/.prettierrc.json new file mode 100644 index 0000000..7cdc6f4 --- /dev/null +++ b/packages/hardhat/.prettierrc.json @@ -0,0 +1,19 @@ +{ + "arrowParens": "avoid", + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "all", + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 80, + "tabWidth": 4, + "useTabs": true, + "singleQuote": false, + "bracketSpacing": true, + "explicitTypes": "always" + } + } + ] +} \ No newline at end of file diff --git a/packages/nextjs/.env.example b/packages/nextjs/.env.example index c8d03d7..a3e56c9 100644 --- a/packages/nextjs/.env.example +++ b/packages/nextjs/.env.example @@ -11,3 +11,8 @@ # More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables NEXT_PUBLIC_ALCHEMY_API_KEY= NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= +<<<<<<< HEAD +======= +LIGHTHOUSE_API_KEY= +WEB3_STORAGE_API_TOKEN= +>>>>>>> 993d799 (Fix CI pipeline) diff --git a/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx b/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx index fb975a4..3e60921 100644 --- a/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx +++ b/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx @@ -19,10 +19,10 @@ export const AddressComponent = ({
-
+
Balance: - +
diff --git a/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx b/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx index f19588c..8a2b548 100644 --- a/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx +++ b/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx @@ -32,7 +32,7 @@ export const ContractTabs = ({ address, contractData }: PageProps) => { useEffect(() => { const checkIsContract = async () => { - const contractCode = await publicClient.getBytecode({ address: address }); + const contractCode = await publicClient.getBytecode({ address: address as `0x${string}` }); setIsContract(contractCode !== undefined && contractCode !== "0x"); }; @@ -85,8 +85,8 @@ export const ContractTabs = ({ address, contractData }: PageProps) => { {activeTab === "code" && contractData && ( )} - {activeTab === "storage" && } - {activeTab === "logs" && } + {activeTab === "storage" && } + {activeTab === "logs" && } ); }; diff --git a/packages/nextjs/app/dealClient/_components/WriteContractFunctionForm.tsx b/packages/nextjs/app/dealClient/_components/WriteContractFunctionForm.tsx new file mode 100644 index 0000000..7c00ece --- /dev/null +++ b/packages/nextjs/app/dealClient/_components/WriteContractFunctionForm.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useEffect } from "react"; +import { DealInputs, createDealObject, getDefaultDealInputs } from "../utils"; +import { Abi, AbiFunction } from "abitype"; +import { JsonView, allExpanded, darkStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; +import { Address, TransactionReceipt } from "viem"; +import { useAccount, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { + ContractInput, + TxReceipt, + getFunctionInputKey, + getInitialFormState, + getParsedContractFunctionArgs, + transformAbiFunction, +} from "~~/app/debug/_components/contract"; +import { InheritanceTooltip } from "~~/app/debug/_components/contract/InheritanceTooltip"; +import { IntegerInput } from "~~/components/fil-frame"; +import { useTransactor } from "~~/hooks/fil-frame"; +import { useTargetNetwork } from "~~/hooks/fil-frame/useTargetNetwork"; +import { DealInfoData } from "~~/hooks/lighthouse/useUpload"; + +type WriteContractFunctionProps = { + abi: Abi; + abiFunction: AbiFunction; + onChange: () => void; + contractAddress: Address; + inheritedFrom?: string; + dealParams?: DealInfoData; +}; + +export const WriteContractFunctionForm = ({ + abi, + abiFunction, + onChange, + contractAddress, + inheritedFrom, + dealParams, +}: WriteContractFunctionProps) => { + const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); + const [txValue, setTxValue] = useState(""); + const { chain } = useAccount(); + const writeTxn = useTransactor(); + const { targetNetwork } = useTargetNetwork(); + const writeDisabled = !chain || chain?.id !== targetNetwork.id; + + const { data: result, isPending, writeContractAsync } = useWriteContract(); + + const handleWrite = async () => { + if (writeContractAsync) { + try { + const makeWriteWithParams = () => + writeContractAsync({ + address: contractAddress, + functionName: abiFunction.name, + abi: abi, + args: getParsedContractFunctionArgs(form), + value: BigInt(txValue), + }); + await writeTxn(makeWriteWithParams); + onChange(); + } catch (e: any) { + console.error("⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e); + } + } + }; + + const [displayedTxResult, setDisplayedTxResult] = useState(); + const { data: txResult } = useWaitForTransactionReceipt({ + hash: result, + }); + useEffect(() => { + setDisplayedTxResult(txResult); + }, [txResult]); + + // TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm + const transformedFunction = transformAbiFunction(abiFunction); + const inputs = transformedFunction.inputs.map((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + return ( + { + setDisplayedTxResult(undefined); + setForm(updatedFormValue); + }} + form={form} + stateObjectKey={key} + paramType={input} + /> + ); + }); + const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable"; + const defaultDealInputs = useMemo(() => getDefaultDealInputs(dealParams), [dealParams]); + const [dealInputs, setDealInputs] = useState(defaultDealInputs); + + const setFormValue = () => { + const dealObject = createDealObject(dealInputs); + setForm(prevForm => ({ + ...prevForm, + ["makeDealProposal_deal_struct DealRequest_tuple"]: JSON.stringify(dealObject), + })); + }; + + useEffect(() => { + setFormValue(); + }, [dealParams]); + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setDealInputs(prevInputs => ({ + ...prevInputs, + [name]: type === "checkbox" ? checked : value, + })); + }; + return ( +
+
+

+ {abiFunction.name} + +

+ + + + + + {abiFunction.stateMutability === "payable" ? ( +
+
+ payable value + wei +
+ { + setDisplayedTxResult(undefined); + setTxValue(updatedTxValue); + }} + placeholder="value (wei)" + /> +
+ ) : null} +
+ {!zeroInputs && ( +
+ {displayedTxResult ? : null} +
+ )} +
+ +
+
+
+ {zeroInputs && txResult ? ( +
+ +
+ ) : null} +
+ ); +}; + +const DealForm = ({ + dealInputs, + handleInputChange, +}: { + dealInputs: DealInputs; + handleInputChange: (e: React.ChangeEvent) => void; +}) => { + const handleDurationChange = (e: React.ChangeEvent) => { + const months = Math.max(7, Math.min(36, Number(e.target.value))); + const endEpoch = dealInputs.start_epoch + months * 43200; + handleInputChange({ + ...e, + // @ts-ignore: number to string conversion + target: { ...e.target, name: "end_epoch", value: endEpoch }, + }); + }; + + return ( +
+ +
+ ); +}; diff --git a/packages/nextjs/components/assets/BlueBox.tsx b/packages/nextjs/components/assets/BlueBox.tsx new file mode 100644 index 0000000..1e067dd --- /dev/null +++ b/packages/nextjs/components/assets/BlueBox.tsx @@ -0,0 +1,34 @@ +export default function BlueBox(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/components/assets/Logo.tsx b/packages/nextjs/components/assets/Logo.tsx new file mode 100644 index 0000000..0344351 --- /dev/null +++ b/packages/nextjs/components/assets/Logo.tsx @@ -0,0 +1,34 @@ +export default function Logo(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/components/assets/LogoLandscape.tsx b/packages/nextjs/components/assets/LogoLandscape.tsx new file mode 100644 index 0000000..cd2a0b9 --- /dev/null +++ b/packages/nextjs/components/assets/LogoLandscape.tsx @@ -0,0 +1,39 @@ +export default function LogoLandscape(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/components/assets/LogoWithText.tsx b/packages/nextjs/components/assets/LogoWithText.tsx new file mode 100644 index 0000000..e520f9a --- /dev/null +++ b/packages/nextjs/components/assets/LogoWithText.tsx @@ -0,0 +1,34 @@ +export default function LogoWithText(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/components/assets/RedArrow.tsx b/packages/nextjs/components/assets/RedArrow.tsx new file mode 100644 index 0000000..8cb636d --- /dev/null +++ b/packages/nextjs/components/assets/RedArrow.tsx @@ -0,0 +1,39 @@ +export default function RedArrow(props: any) { + return ( + + + + + + + + + + ); +} \ No newline at end of file diff --git a/packages/nextjs/components/assets/RedPopBox.tsx b/packages/nextjs/components/assets/RedPopBox.tsx new file mode 100644 index 0000000..68903db --- /dev/null +++ b/packages/nextjs/components/assets/RedPopBox.tsx @@ -0,0 +1,34 @@ +export default function RedPopBox(props: any) { + return ( + + + + + + + + + + ); +} \ No newline at end of file diff --git a/packages/nextjs/components/assets/YellowCircle.tsx b/packages/nextjs/components/assets/YellowCircle.tsx new file mode 100644 index 0000000..a7b25bd --- /dev/null +++ b/packages/nextjs/components/assets/YellowCircle.tsx @@ -0,0 +1,34 @@ +export default function YellowCircle(props: any) { + return ( + + + + + + + + + + ); +} \ No newline at end of file diff --git a/packages/nextjs/components/svg/Logo.tsx b/packages/nextjs/components/svg/Logo.tsx new file mode 100644 index 0000000..0344351 --- /dev/null +++ b/packages/nextjs/components/svg/Logo.tsx @@ -0,0 +1,34 @@ +export default function Logo(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/components/svg/LogoLandscape.tsx b/packages/nextjs/components/svg/LogoLandscape.tsx new file mode 100644 index 0000000..5cc5f94 --- /dev/null +++ b/packages/nextjs/components/svg/LogoLandscape.tsx @@ -0,0 +1,31 @@ +export default function LogoLandscape(props: any) { + return ( + + + + + + + + + + ); +} diff --git a/packages/nextjs/hooks/lighthouse/index.ts b/packages/nextjs/hooks/lighthouse/index.ts new file mode 100644 index 0000000..a030532 --- /dev/null +++ b/packages/nextjs/hooks/lighthouse/index.ts @@ -0,0 +1,58 @@ +import { generate, recoverKey, recoverShards, saveShards } from "@lighthouse-web3/kavach"; +import lighthouse from "@lighthouse-web3/sdk"; + +type accessControlConditions = any; + +export const uploadFilesEncrypted = async ( + files: File[], + apiKey: string, + userAddress: string, + jwt: string, + conditions?: accessControlConditions[], + aggregator?: string, +) => { + let cid = await _uploadFilesEncrypted(files, apiKey, userAddress, jwt); + if (conditions?.length === 0 || !conditions || !aggregator) { + return cid; + } + cid = await applyAccessConditions(cid, userAddress, jwt, conditions, aggregator); + return cid; +}; + +export const uploadFiles = async (files: File[], apiKey: string) => { + const output = await lighthouse.upload(files, apiKey); + return output.data.Hash; +}; + +/* Deploy file along with encryption */ +export const _uploadFilesEncrypted = async (files: File[], apiKey: string, userAddress: string, jwt: string) => { + const output = await lighthouse.uploadEncrypted(files, apiKey, userAddress, jwt); + console.log("output", output.data[0].Hash); + + const { keyShards } = await generate(); + + await saveShards(userAddress, output.data[0].Hash, jwt, keyShards); + + return output.data[0].Hash; +}; + +export const decrypt = async (cid: string, userAddress: string, jwt: string) => { + let decrypted; + const { shards } = await recoverShards(userAddress, cid, jwt, 3); + try { + const { masterKey } = await recoverKey(shards); + decrypted = await lighthouse.decryptFile(cid, masterKey); + } catch {} + return decrypted; +}; + +export const applyAccessConditions = async ( + cid: string, + address: string, + jwt: string, + conditions: accessControlConditions[], + aggregator: string, +) => { + const response = await lighthouse.applyAccessCondition(address, cid, jwt, conditions, aggregator); + return response.data.cid; +};