diff --git a/docs/components/Divider.js b/docs/components/Divider.js new file mode 100644 index 0000000..ad0d870 --- /dev/null +++ b/docs/components/Divider.js @@ -0,0 +1,7 @@ +import React from "react"; + +const Divider = () => { + return
; +}; + +export default Divider; diff --git a/docs/quickstart-blind-app.md b/docs/quickstart-blind-app.md index 007d6cb..5dbdc26 100644 --- a/docs/quickstart-blind-app.md +++ b/docs/quickstart-blind-app.md @@ -6,8 +6,9 @@ import QuickstartIntro from './\_quickstart-intro.mdx'; import VenvSetup from './\_nada-venv-setup.mdx'; import UnderstandingProgram from './\_understanding-first-nada-program.mdx'; import CompileRunTest from './\_quickstart-compile-run-test.mdx'; +import Divider from '../src/components/Divider.js'; -# Build a Blind App with the cra-nillion starter repo +# Build a Blind App :::info @@ -18,7 +19,24 @@ This is the 3rd step in the Blind App Quickstart. Before starting this guide, ::: -The [cra-nillion Starter Repo](https://github.com/NillionNetwork/cra-nillion) repo is a Create React App which has everything you need to start building your blind app. +### Setting up a new project + +There are two pathways to creating a nillion starter repo. + +**1. Template Approach** + +Use our out-of-the-box react app by cloning our `cra-nillion` repository. + +OR + +**2. New Project** + +Create a brand new React / Nextjs app from scratch. + + + + + The [cra-nillion Starter Repo](https://github.com/NillionNetwork/cra-nillion) repo is a Create React App which has everything you need to start building your blind app. ## Clone the CRA-Nillion JavaScript starter repo @@ -218,6 +236,674 @@ Open the Nillion [JavaScript Client Reference](https://nillion.pub/nillion-js-re ## Run the Blind Computation Demo Go back to the Blind App on http://localhost:8080/compute and run through the steps on the page to test out the full blind computation flow. + + + + + + + + + + ## Installation of a new NextJS App + Let's get started with the Next application. + - `npx create-next-app@latest nillion-app` + - Select `yes` when it asks to use the app router option. + - Select `no` when it asks to use the pages router option. + + ### Install repo dependencies + + Then add the below packages to your fresh React / Next.js app. + + + + ```shell + npm i @nillion/client-core@latest @nillion/client-vms@latest @nillion/client-react-hooks@latest + ``` + + + ```shell + yarn add @nillion/client-core@latest @nillion/client-vms@latest @nillion/client-react-hooks@latest + ``` + + + + ### Update your `next.config.mjs`. + + This is necessary to help adjust the HTTP Headers, allows the use of browser web-workers and provide access to the nilchain. + + ``` typescript + /** @type {import("next").NextConfig} */ + const nextConfig = { + webpack: ( + config, + { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } + ) => { + config.resolve.fallback = { + crypto: false, + stream: false, + buffer: false, + vm: false, + }; + + config.module.rules.push({ + test: /\.wasm$/, + type: "asset/resource", + }); + + return config; + }, + async headers() { + return [ + { + source: "/:path*", + headers: [ + { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }, + { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, + ], + }, + ]; + }, + async rewrites() { + return [ + { + source: "/nilchain", + destination: "http://127.0.0.1:48102/", + }, + ]; + }, + }; + export default nextConfig; + ``` + + ### Create the `NillionClient` + This step will help you initialize the NillionClientProvider over your application. + + Note: If network: `NamedNetwork.enum.Devnet` is provided, then we don't need to specify bootnodes, cluster or chain since these values are copied from the partial config. + + The configurations can be referenced [here.](https://github.com/NillionNetwork/client-ts/blob/main/packages/client-core/src/configs.ts) + + + + + ``` typescript + //page.tsx + “use client”; + + import Home from “../components/Home”; + import { NamedNetwork } from “@nillion/client-core”; + import { NillionClientProvider } from “@nillion/client-react-hooks”; + import { NillionClient } from “@nillion/client-vms”; + import { createSignerFromKey } from “@nillion/client-payments”; + const client = NillionClient.create({ + network: NamedNetwork.enum.Devnet, + overrides: async () => { + // first account when running `nillion-devnet` with default seed + const signer = await createSignerFromKey( + “9a975f567428d054f2bf3092812e6c42f901ce07d9711bc77ee2cd81101f42c5” + ); + return { + endpoint: “http://localhost:8080/nilchain”, + signer, + userSeed: “nillion-devnet”, + nodeSeed: Math.random().toString(), + bootnodes: [ + “/ip4/127.0.0.1/tcp/54936/ws/p2p/12D3KooWMvw1hEqm7EWSDEyqTb6pNetUVkepahKY6hixuAuMZfJS”, + ], + cluster: “9e68173f-9c23-4acc-ba81-4f079b639964”, + chain: “nillion-chain-devnet”, + }; + }, + }); + export default function App() { + return ( + + + + ); + } + + ``` + + + + ``` typescript + //index.tsx + + import * as React from "react"; + import { NamedNetwork } from "@nillion/client-core"; + import { createSignerFromKey } from "@nillion/client-payments"; + import { NillionClientProvider } from "@nillion/client-react-hooks"; + import { NillionClient } from "@nillion/client-vms"; + import type { AppProps } from "next/app"; + import "../styles/globals.css"; + + export const client = NillionClient.create({ + network: NamedNetwork.enum.Devnet, + overrides: async () => { + const signer = await createSignerFromKey( + process.env.NEXT_PUBLIC_NILLION_NILCHAIN_PRIVATE_KEY! + ); + return { + signer, + endpoint: process.env.NEXT_PUBLIC_NILLION_ENDPOINT, + cluster: process.env.NEXT_PUBLIC_NILLION_CLUSTER_ID, + bootnodes: [process.env.NEXT_PUBLIC_NILLION_BOOTNODE_WEBSOCKET], + chain: process.env.NEXT_PUBLIC_CHAIN, + userSeed: process.env.NEXT_PUBLIC_NILLION_USER_SEED, + nodeSeed: Math.random().toString(), + }; + }, + }); + + export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); + } + ``` + + + + + We have provided some ENV environments, copy this into a new `.env`. + + ``` + // DevNet + NEXT_PUBLIC_NILLION_CLUSTER_ID="9e68173f-9c23-4acc-ba81-4f079b639964" + NEXT_PUBLIC_NILLION_BOOTNODE_WEBSOCKET="/ip4/127.0.0.1/tcp/54936/ws/p2p/12D3KooWMvw1hEqm7EWSDEyqTb6pNetUVkepahKY6hixuAuMZfJS" + NEXT_PUBLIC_NILLION_ENDPOINT=http://localhost:3000/nilchain + NEXT_PUBLIC_API_BASE_PATH=/nilchain-proxy + NEXT_PUBLIC_CHAIN="nillion-chain-devnet" + NEXT_PUBLIC_NILLION_USER_SEED="nillion-devnet" + NEXT_PUBLIC_NILLION_NILCHAIN_PRIVATE_KEY="9a975f567428d054f2bf3092812e6c42f901ce07d9711bc77ee2cd81101f42c5" + ``` + + ### Update your app.tsx + + :::info + + For App Router applications - do this in your `components/Home.tsx` + + ::: + + ```ts reference showGithubLink + https://github.com/NillionNetwork/client-ts/blob/main/examples/nextjs/src/pages/index.tsx + ``` + + ### Initializing the `nillion-devnet` + In order to interact with the nillion developer network (devnet), we will use need to spin up the local development cluster with `nilup`. + The nilchain spawned with `nillion-devnet` does not support CORS. The recommended workaround is proxy requests to nilchain for local development. + + In a separate terminal, run `nillion-devnet` and allow connections to pass if prompted. The following below should be the output you see. + + ``` + nillion-app % nillion-devnet + ℹ️ cluster id is 9e68173f-9c23-4acc-ba81-4f079b639964 + ℹ️ using 256 bit prime + ℹ️ storing state in /var/folders/f4/cqlsh9k167vcx1swjlh_6pp80000gn/T/.tmpd3wwtD (123.08Gbs available) + 🏃 starting nilchain node in: /var/folders/f4/cqlsh9k167vcx1swjlh_6pp80000gn/T/.tmpd3wwtD/nillion-chain + ⛓ nilchain JSON RPC available at http://127.0.0.1:48102 + ⛓ nilchain REST API available at http://localhost:26650 + ⛓ nilchain gRPC available at localhost:26649 + 🏃 starting node 12D3KooWMvw1hEqm7EWSDEyqTb6pNetUVkepahKY6hixuAuMZfJS + ⏳ waiting until bootnode is up... + 🏃 starting node 12D3KooWAiwGZUwSUaT2bYVxGS8jmfMrfsanZYkHwH3uL7WJPsFq + 🏃 starting node 12D3KooWM3hsAswc7ZT6VpwQ1TCZU4GCYY55nLhcsxCcfjuixW57 + 👛 funding nilchain keys + 📝 nillion CLI configuration written to /Users/XXX/.config/nillion/nillion-cli.yaml + 🌄 environment file written to /Users/XXX/.config/nillion/nillion-devnet.env + ``` + + ### Interact with your app + 42 is a SecretInteger we have hard coded to store. Feel free to press the `store` button and then it should pass with a success. + + Amazing - you have interacted with a Nada based app on your front end! 🥳 + + + + ## Hook up your secret_addition.py Nada program to your first blind app + + Now we want to complete the full-stack Nada application experience and connect the secret_addition program we wrote earlier. + + This is what our application will look like: + + ![Blind App Example](/img/blind-app-example.png) + + + In your nextjs directory - create `programs` directory in public. + + ``` + cp nada_quickstart_programs/src/secret_addition.py nillion-app/public/programs + cp nada_quickstart_programs/target/secret_addition.nada.bin nillion-app/public/programs + ``` + + Then in `pages`, add a new directory called `compute` to have a separate page for the compute page. + + Also we need to add one utility file to our app so create a directory called `utils` and add a file called `transformNadaProgramToUint8Array.ts`. This will be explained in the next section. + + ``` typescript + export const transformNadaProgramToUint8Array = async ( + publicProgramPath: string // `./programs/${programName}.nada.bin` + ): Promise => { + try { + const response = await fetch(publicProgramPath); + if (!response.ok) { + throw new Error(`Failed to fetch program: ${response.statusText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + return new Uint8Array(arrayBuffer); + } catch (error) { + console.error('Error fetching and transforming program:', error); + throw error; + } + }; + ``` + + Your currently directory tree should be looking like: + + + + + ``` + ├── app + │   ├── layout.tsx + │   ├── page.tsx + │   ├── compute + │   │   └── page.tsx + ├── postcss.config.mjs + ├── public + │   ├── favicon.ico + │   ├── next.svg + │   ├── programs + │   │   ├── secret_addition.nada.bin + │   │   └── secret_addition.py + │   └── vercel.svg + ├── styles + │   └── globals.css + ├── tailwind.config.ts + ├── tsconfig.json + └── utils + └── transformNadaProgramToUint8Array.ts + ``` + + + ``` + ├── pages + │   ├── _app.tsx + │   ├── _document.tsx + │   ├── api + │   │   └── hello.ts + │   ├── compute + │   │   └── index.tsx + │   └── index.tsx + ├── postcss.config.mjs + ├── public + │   ├── favicon.ico + │   ├── next.svg + │   ├── programs + │   │   ├── secret_addition.nada.bin + │   │   └── secret_addition.py + │   └── vercel.svg + ├── styles + │   └── globals.css + ├── tailwind.config.ts + ├── tsconfig.json + └── utils + └── transformNadaProgramToUint8Array.ts + ``` + + + + + + ## Using the Hooks + + We will now be using the react-hooks from the `@nillion/client-react-hooks` to store programs, store values and use the respective program (i.e. secret_addition). + + Copy the following snippet into your `compute/page.tsx` / `compute/index.tsx` page. We will be going through each section. + + We are using several hooks: + - `useNillion`: Access to the Nillion ClientProvider we added in previosuly. + - `useStoreProgram`: Store our program with Nada + - `useStoreValue`: Store values with Nada + - `useRunProgram`: Run the Nada Program + - `useFetchProgramOutput`: Fetch the Program Output / computation. + + The `useStates` are for storing the data values that we want to parse through into the + - `selectedProgramCode`: This is to grab the Nada Code Snippet from secret_additon + - `secretValue1`: The secret value we want to input + - `secretValue2`:The secret value we want to input + - `programID`: The Nada program ID + - `secretValue1ID`: The ID after we stored the secret value 1 + - `secretValue2ID`: The ID after we stored the secret value 2 + - `computeID`: The response ID we receive after running the program + - `computeResult`: The result of the computation from the program + + The constants relate the Nada program we wrote: + - `PARTY_NAME`: Name of the party + - `PROGRAM_NAME`: Name of the program + + ***Functions*** + + - The first `useEffect` allows us to fetch the Nada Program Code to display in our rendering. + + - The `handleStoreProgram` function allows us to store the Program which takes in two arguments: + - `name`: ProgramName | string; + - `program`: Uint8Array; + - The programName is a string and in our constants that was mentioned previously + - The program binary is related to the .nada.bin file we copied in our `public` folder and is processed in the `transformNadaProgramToUint8Array.ts` file we added in `utils`. + + - The `handleStoreSecretInteger1` & `handleStoreSecretInteger2` function allows us to store the secret integer values with Nada. It takes in the `value` you want to store wih ttl for time and permissions for allowing the user to compute to the program. + + - The `handleUseProgram` function allows us to use the stored program and takes in several arguments: + - bindings: Association to the program with regards to the `programID`and who is the `inputParty` and `OutputParty` + - values: NadaValues of what you are parsing through + - storeIds: The storeIDs of previous storedValues. + + - The last UseEffect is for fetching the computeResult via the `useFetchProgramOutput` react hook. + + ***Rendering*** + + The rendering uses tailwind and divided into several sections. Feel free to adjust the styles and approach. + + 1. The Program Code + Storing it with Nada + 2. Store Secret Integer 1 + 3. Store Secret Integer 2 + 4. Compute the Result of above integers. + + ```typescript + import * as React from "react"; + import { + useRunProgram, + useStoreValue, + useStoreProgram, + useNillion, + useFetchProgramOutput, + } from "@nillion/client-react-hooks"; + import { useEffect, useState } from "react"; + import { + ProgramId, + PartyName, + Permissions, + PartyId, + StoreId, + ProgramBindings, + NadaValues, + NadaValue, + NamedValue, + } from "@nillion/client-core"; + import { transformNadaProgramToUint8Array } from "@/utils/transformNadaProgramToUint8Array"; + + export default function Compute() { + // Use of Nillion Hooks + const client = useNillion(); + const storeProgram = useStoreProgram(); + const storeValue = useStoreValue(); + const runProgram = useRunProgram(); + const fetchProgram = useFetchProgramOutput({ + id: computeID, + }); + + // UseStates + const [selectedProgramCode, setSelectedProgramCode] = useState(""); + const [secretValue1, setSecretValue1] = useState(0); + const [secretValue2, setSecretValue2] = useState(0); + const [programID, setProgramID] = useState(); + const [secretValue1ID, setSecretValue1ID] = useState(); + const [secretValue2ID, setSecretValue2ID] = useState(); + const [computeResult, setComputeResult] = useState(null); + const [computeID, setComputeID] = useState(null); + + // Other CONSTS + const PARTY_NAME = "Party1" as PartyName; + const PROGRAM_NAME = "secret_addition"; + + // Fetch Nada Program Code. + useEffect(() => { + const fetchProgramCode = async () => { + const response = await fetch(`./programs/secret_addition.py`); + const text = await response.text(); + setSelectedProgramCode(text); + }; + fetchProgramCode(); + }, [selectedProgramCode]); + + + // Action to store Program with Nada + const handleStoreProgram = async () => { + try { + const programBinary = await transformNadaProgramToUint8Array( + `./programs/${PROGRAM_NAME}.nada.bin` + ); + const result = await storeProgram.mutateAsync({ + name: PROGRAM_NAME, + program: programBinary, + }); + setProgramID(result!); + } catch (error) { + console.log("error", error); + } + }; + + // Action to handle storing secret integer 1 + const handleStoreSecretInteger1 = async () => { + try { + const permissions = Permissions.create().allowCompute( + client.vm.userId, + programID as ProgramId + ); + + const result = await storeValue.mutateAsync({ + values: { + mySecretInt: secretValue1, + }, + ttl: 3600, + permissions, + }); + setSecretValue1ID(result); + } catch (error) { + console.error("Error storing SecretInteger:", error); + } + }; + + // Action to handle storing secret integer 2 + const handleStoreSecretInteger2 = async () => { + try { + const permissions = Permissions.create().allowCompute( + client.vm.userId, + programID as ProgramId + ); + const result = await storeValue.mutateAsync({ + values: { + mySecretInt: secretValue2, + }, + ttl: 3600, + permissions, + }); + console.log("Stored SecretInteger2:", result); + setSecretValue2ID(result); + } catch (error) { + console.error("Error storing SecretInteger2:", error); + } + }; + + // Handle using the secret_addition Program + const handleUseProgram = async () => { + try { + // Bindings + const bindings = ProgramBindings.create(programID!); + bindings.addInputParty( + PARTY_NAME as PartyName, + client.vm.partyId as PartyId + ); + bindings.addOutputParty( + PARTY_NAME as PartyName, + client.vm.partyId as PartyId + ); + + const values = NadaValues.create() + .insert( + NamedValue.parse("my_int1"), + NadaValue.createSecretInteger(secretValue1) + ) + .insert( + NamedValue.parse("my_int2"), + NadaValue.createSecretInteger(secretValue2) + ); + + const res = await runProgram.mutateAsync({ + bindings: bindings, + values, + storeIds: [], + }); + + setComputeID(res); + } catch (error) { + console.error("Error executing program:", error); + throw error; + } + }; + + // Fetch the new compute result + useEffect(() => { + if (fetchProgram.data) { + setComputeResult(fetchProgram.data.my_output.toString()); + } + }, [fetchProgram.data]); + + return ( +
+ {/* Store Programs Section */} +
+

Program Code:

+
+
+                        {selectedProgramCode}
+                    
+
+ +
+ + {programID && ( +
+

Program ID: {programID}

+
+ )} + +
+ + {/* Store Secrets Section */} +
+

Store Secret:

+

Store your int_1

+ setSecretValue1(Number(e.target.value))} + className="w-full px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:border-blue-500" + /> + + + {secretValue1ID && ( +
+

+ Secret Value 1 ID: {secretValue1ID} +

+
+ )} + +

Store your int_2

+ setSecretValue2(Number(e.target.value))} + className="w-full px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:border-blue-500" + /> + + + {secretValue2ID && ( +
+

+ Secret Value 2 ID: {secretValue2ID} +

+
+ )} +
+ +
+ + {/* Compute Section */} +
+

Compute:

+ + {computeResult && ( +
+

+ Compute Result: {computeResult} +

+
+ )} +
+
+ ); + } + + ``` + +
+ + + +
+ +
+ +
## Next steps diff --git a/docs/quickstart-testnet.md b/docs/quickstart-testnet.md index 1f7bcc7..873a521 100644 --- a/docs/quickstart-testnet.md +++ b/docs/quickstart-testnet.md @@ -10,6 +10,8 @@ Your blind app is currently running locally against the nillion-devnet. Let's co Update your `.env` values to point at the Nillion Testnet +Note: for Next apps, change the prefix to `NEXT_PUBLIC_` + The `REACT_APP_NILLION_NILCHAIN_PRIVATE_KEY` private key value above should correspond to an address you've funded with Testnet NIL. @@ -34,15 +36,17 @@ Test the configuration locally against your blind app to make sure the full blin ## Commit your project to Github -Commit your repo to Github and tag your Github repo with `nillion-nada` so the rest of the Nillion community can find it. +Commit your repo to Github from your React/Next.js folder and tag your Github repo with `nillion-nada` so the rest of the Nillion community can find it. ## Host your blind app with Vercel -1. Follow the https://vercel.com/docs/getting-started-with-vercel/import guide to import your Github project to Vercel +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new) + +1. Use the above deploy button + import your repository. -2. Follow the https://vercel.com/docs/projects/environment-variables guide to add all Testnet environment variables +2. Follow the https://vercel.com/docs/projects/environment-variables guide to add all Testnet environment variables. A shortcut is to copy and paste from .env and into their textbox. -3. Set up the vercel.json file with headers and proxy rewrites +3. Set up the `vercel.json` file with headers and proxy rewrites ```json reference showGithubLink https://github.com/NillionNetwork/cra-nillion/blob/main/vercel.json diff --git a/src/components/Divider.js b/src/components/Divider.js new file mode 100644 index 0000000..ad0d870 --- /dev/null +++ b/src/components/Divider.js @@ -0,0 +1,7 @@ +import React from "react"; + +const Divider = () => { + return
; +}; + +export default Divider; diff --git a/static/img/blind-app-example.png b/static/img/blind-app-example.png new file mode 100644 index 0000000..512731e Binary files /dev/null and b/static/img/blind-app-example.png differ