diff --git a/intro/4-ipfs-code-execution/aqua/package-lock.json b/intro/4-ipfs-code-execution/aqua/package-lock.json index a8d92e9..f397e1e 100644 --- a/intro/4-ipfs-code-execution/aqua/package-lock.json +++ b/intro/4-ipfs-code-execution/aqua/package-lock.json @@ -1,5 +1,5 @@ { - "name": "ipfs-execution", + "name": "@fluencelabs/ipfs-execution", "version": "0.1.0", "lockfileVersion": 1, "requires": true, @@ -1275,7 +1275,7 @@ } }, "node-fetch": { - "version": "npm:@achingbrain/node-fetch@2.6.7", + "version": "npm:node-fetch@2.6.7", "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==" }, @@ -1414,7 +1414,7 @@ } }, "node-fetch": { - "version": "npm:@achingbrain/node-fetch@2.6.7", + "version": "npm:node-fetch@2.6.7", "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==" }, @@ -1566,7 +1566,7 @@ } }, "node-fetch": { - "version": "npm:@achingbrain/node-fetch@2.6.7", + "version": "npm:node-fetch@2.6.7", "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==" }, diff --git a/intro/4-ipfs-code-execution/web/package-lock.json b/intro/4-ipfs-code-execution/web/package-lock.json index ca685cc..8c13abe 100644 --- a/intro/4-ipfs-code-execution/web/package-lock.json +++ b/intro/4-ipfs-code-execution/web/package-lock.json @@ -12679,6 +12679,11 @@ "pify": "^4.0.1" } }, + "hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -21786,6 +21791,14 @@ "ms": "^2.1.1" } }, + "recoil": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.3.1.tgz", + "integrity": "sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA==", + "requires": { + "hamt_plus": "1.0.2" + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", diff --git a/intro/4-ipfs-code-execution/web/package.json b/intro/4-ipfs-code-execution/web/package.json index 540f4bf..af36080 100644 --- a/intro/4-ipfs-code-execution/web/package.json +++ b/intro/4-ipfs-code-execution/web/package.json @@ -4,10 +4,9 @@ "private": true, "dependencies": { "@fluencelabs/aqua-ipfs": "0.1.8", - "@fluencelabs/ipfs-execution": "file:../aqua", "@fluencelabs/fluence": "0.9.53", "@fluencelabs/fluence-network-environment": "1.0.10", - "ipfs-http-client": "^50.1.2", + "@fluencelabs/ipfs-execution": "file:../aqua", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", @@ -15,15 +14,17 @@ "@types/node": "^12.20.16", "@types/react": "^17.0.14", "@types/react-dom": "^17.0.9", + "ipfs-http-client": "^50.1.2", + "multiaddr": "^10.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3", + "recoil": "^0.3.1", "typescript": "^4.3.5", - "web-vitals": "^1.1.2", - "multiaddr": "^10.0.0" + "web-vitals": "^1.1.2" }, "scripts": { - "prestart": "(cd ../aqua; npm run build)", + "_prestart": "(cd ../aqua; npm run build)", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", diff --git a/intro/4-ipfs-code-execution/web/src/App.tsx b/intro/4-ipfs-code-execution/web/src/App.tsx index d1481a4..d8c248f 100644 --- a/intro/4-ipfs-code-execution/web/src/App.tsx +++ b/intro/4-ipfs-code-execution/web/src/App.tsx @@ -1,143 +1,25 @@ -import React, { useState } from "react"; +import React from "react"; import logo from "./logo.svg"; import "./App.scss"; -import { createClient, FluenceClient } from "@fluencelabs/fluence"; -import { get_external_api_multiaddr } from "@fluencelabs/aqua-ipfs"; -import { stage } from "@fluencelabs/fluence-network-environment"; -import { - deploy_service, - put_file_size, - remove_service, - provideFile, -} from "@fluencelabs/ipfs-execution"; -import { Multiaddr, protocols } from "multiaddr"; -const { create, globSource, urlSource, CID } = require("ipfs-http-client"); - -const relayNodes = [stage[0], stage[1], stage[2]]; - -const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); -}; - -function fromOption(opt: T | T[] | null): T | null { - if (Array.isArray(opt)) { - if (opt.length === 0) { - return null; - } - - opt = opt[0]; - } - if (opt === null) { - return null; - } - - return opt; -} - -function decapsulateP2P(rpcAddr: string): string { - return new Multiaddr(rpcAddr) - .decapsulateCode(protocols.names.p2p.code) - .toString(); -} +import { gotRpcAddrState, isConnectedState, isDeployedState } from "./state"; +import { useRecoilValue } from "recoil"; +import { ConnectedInfo } from "./Components/ConnectedInfo"; +import { ConnectionForm } from "./Components/ConnectionForm"; +import { IpfsForm } from "./Components/IpfsForm"; +import { IpfsDeploymentInfo } from "./Components/IpfsDeploymentInfo"; +import { SizeCalcForm } from "./Components/SizeCalcForm"; function App() { - const [client, setClient] = useState(null); - const [serviceId, setServiceId] = useState(null); - - const [wasm, setWasm] = useState( - "Qmf8fH2cDZXGKS9uDGBcHxv5uQ51ChrigdZKe3QxS2C1AF" - ); - const [rpcAddr, setRpcAddr] = useState(""); - const [fileCID, setFileCID] = useState(""); - const [fileSize, setFileSize] = useState(""); - const [fileSizeCID, setFileSizeCID] = useState(""); - - const isConnected = client !== null; - const gotRpcAddr = rpcAddr !== null; - const deployed = serviceId !== null; - - const connect = async (relayPeerId: string) => { - try { - let client = await createClient(relayPeerId); - setClient(client); - await getRpcAddr(client); - } catch (err) { - console.log("Client initialization failed", err); - } - }; - - const getRpcAddr = async (client: FluenceClient) => { - if (client === null) { - console.log("getRpcAddr client is null"); - return; - } - - let result = await get_external_api_multiaddr(client, client.relayPeerId!); - console.log("getRpcAddr result", result); - let rpcAddr = result.multiaddr; - setRpcAddr(decapsulateP2P(rpcAddr)); - }; - - const deployService = async () => { - console.log("wasm %s rpcAddr %s", wasm, rpcAddr); - if (client === null || wasm === null || rpcAddr === null) { - return; - } - var service_id = await deploy_service( - client, - client.relayPeerId!, - wasm, - rpcAddr, - (msg, value) => console.log(msg, value), - { ttl: 10000 } - ); - service_id = fromOption(service_id); - setServiceId(service_id); - }; - - const getFileSize = async () => { - if (client === null || serviceId === null || rpcAddr === null) { - return; - } - - var putResult = await put_file_size( - client, - client.relayPeerId!, - fileCID, - rpcAddr, - serviceId, - (size) => setFileSize(size.toString()), - (label, error) => setFileSize("Error: " + label + ": " + error), - { ttl: 10000 } - ); - putResult = fromOption(putResult); - if (putResult === null) { - return; - } - if (putResult.success) { - setFileSizeCID(putResult.hash); - } else { - setFileSizeCID("Error: " + putResult.error); - } - }; - - const removeService = async () => { - if (client === null || serviceId === null) { - return; - } - - await remove_service(client, client.relayPeerId!, serviceId, { - ttl: 10000, - }); - setServiceId(null); - }; + const isConnected = useRecoilValue(isConnectedState); + const gotRpcAddr = useRecoilValue(gotRpcAddrState); + const isDeployed = useRecoilValue(isDeployedState); console.log( "isConnected gotRpcAddr deployed\n", isConnected, gotRpcAddr, - deployed + isDeployed ); return ( @@ -147,33 +29,13 @@ function App() {
- {!isConnected && } - {isConnected && } - {isConnected && gotRpcAddr && !deployed && ( - - )} - {deployed && ( + {!isConnected && } + {isConnected && } + {isConnected && gotRpcAddr && !isDeployed && } + {isDeployed && ( <> - - + + )}
@@ -181,207 +43,4 @@ function App() { ); } -const Deployed = (props: { - setRpcAddr: React.Dispatch>; - rpcAddr: string; - setFileCID: React.Dispatch>; - fileCID: string; - fileSize: string; - fileSizeCID: string; - getFileSize: () => Promise; -}) => { - return ( - <> -
-

Get file size

-

- Upload any file to IPFS node -

- {props.rpcAddr} -

- paste CID here and get the size of that file via ProcessFiles service - you have just deployed -

-
- - props.setFileCID(e.target.value)} - value={props.fileCID} - /> -
-
- -
-
-
- - -
-
- - -
- - ); -}; - -const IpfsForm = (props: { - rpcAddr: string; - setRpcAddr: React.Dispatch>; - wasm: string; - setWasm: React.Dispatch>; - deployService: () => Promise; -}) => { - return ( - <> -

Ipfs

-

- process_files.wasm will be downloaded via IPFS to the Fluence node, and - then a service will be dynamically created from it! After that, you will - be able to use that service to get sizes of IPFS files! -
- To do so, please specify IPFS RPC address to download process_files.wasm - from -
- And specify CID of WebAssembly module. -

- - - - -
- -
- - ); -}; - -const IfsDeployed = (props: { - wasm: string; - serviceId: string | null; - removeService: () => {}; -}) => { - return ( - <> -

Deployed

- - - - - - - - - - - -
process_files.wasm CID:{props.wasm} - -
ProcessFiles service ID:{props.serviceId}
- - ); -}; - -const ConnectionForm = (props: { - connect: (multiaddr: string) => Promise; -}) => { - return ( - <> -

Pick a relay

-
    - {relayNodes.map((x) => ( -
  • - {x.peerId} - -
  • - ))} -
- - ); -}; - -const Connected = (props: { client: FluenceClient }) => { - return ( - <> -

Connected

- - - - - - - - - - - -
Peer id:{props.client.selfPeerId} - -
Relay peer id:{props.client.relayPeerId} - -
- - ); -}; - -const TextInput = (props: { - text: string; - value: string; - setValue: React.Dispatch>; -}) => { - return ( -
- - props.setValue(e.target.value)} - value={props.value} - required={true} - /> -
- ); -}; - export default App; diff --git a/intro/4-ipfs-code-execution/web/src/Components/ConnectedInfo.tsx b/intro/4-ipfs-code-execution/web/src/Components/ConnectedInfo.tsx new file mode 100644 index 0000000..bb3983e --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/ConnectedInfo.tsx @@ -0,0 +1,44 @@ +import { useRecoilValue } from "recoil"; +import { clientState } from "../state"; +import { copyToClipboard } from "../util"; + +export const ConnectedInfo = () => { + const client = useRecoilValue(clientState); + if (client === null) { + return <>; + } + + return ( + <> +

Connected

+ + + + + + + + + + + + + +
Peer id:{client.selfPeerId} + +
Relay peer id:{client.relayPeerId} + +
+ + ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/Components/ConnectionForm.tsx b/intro/4-ipfs-code-execution/web/src/Components/ConnectionForm.tsx new file mode 100644 index 0000000..3b5769b --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/ConnectionForm.tsx @@ -0,0 +1,21 @@ +import { relayNodes, useClientConnect } from "../state"; + +export const ConnectionForm = () => { + const connect = useClientConnect(); + + return ( + <> +

Pick a relay

+
    + {relayNodes.map((x) => ( +
  • + {x.peerId} + +
  • + ))} +
+ + ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/Components/IpfsDeploymentInfo.tsx b/intro/4-ipfs-code-execution/web/src/Components/IpfsDeploymentInfo.tsx new file mode 100644 index 0000000..f073cf2 --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/IpfsDeploymentInfo.tsx @@ -0,0 +1,36 @@ +import { useRecoilValue } from "recoil"; +import { serviceIdState, useRemoveServuce, wasmState } from "../state"; +import { copyToClipboard } from "../util"; + +export const IpfsDeploymentInfo = () => { + const wasm = useRecoilValue(wasmState); + const serviceId = useRecoilValue(serviceIdState); + const removeService = useRemoveServuce; + + return ( + <> +

Deployed

+ + + + + + + + + + + +
process_files.wasm CID:{wasm} + +
ProcessFiles service ID:{serviceId}
+ + ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/Components/IpfsForm.tsx b/intro/4-ipfs-code-execution/web/src/Components/IpfsForm.tsx new file mode 100644 index 0000000..82af2fe --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/IpfsForm.tsx @@ -0,0 +1,42 @@ +import { useRecoilState, useResetRecoilState } from "recoil"; +import { rpcAddrState, useDeployService, wasmState } from "../state"; +import { TextInput } from "./TextInput"; + +export const IpfsForm = () => { + const [rpcAddr, setRpcAddr] = useRecoilState(rpcAddrState); + const [wasm, setWasm] = useRecoilState(wasmState); + const deployService = useDeployService(); + + return ( + <> +

Ipfs

+

+ process_files.wasm will be downloaded via IPFS to the Fluence node, and + then a service will be dynamically created from it! After that, you will + be able to use that service to get sizes of IPFS files! +
+ To do so, please specify IPFS RPC address to download process_files.wasm + from +
+ And specify CID of WebAssembly module. +

+ + + + +
+ +
+ + ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/Components/SizeCalcForm.tsx b/intro/4-ipfs-code-execution/web/src/Components/SizeCalcForm.tsx new file mode 100644 index 0000000..9ab587b --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/SizeCalcForm.tsx @@ -0,0 +1,56 @@ +import { useRecoilState, useRecoilValue } from "recoil"; +import { + fileCIDState, + fileSizeCIDState, + fileSizeState, + rpcAddrState, + useGetFileSize, +} from "../state"; + +export const SizeCalcForm = () => { + const rpcAddr = useRecoilValue(rpcAddrState); + const [fileCID, setFileCID] = useRecoilState(fileCIDState); + const fileSize = useRecoilValue(fileSizeState); + const fileSizeCID = useRecoilValue(fileSizeCIDState); + const getFileSize = useGetFileSize(); + + return ( + <> +
+

Get file size

+

+ Upload any file to IPFS node +

+ {rpcAddr} +

+ paste CID here and get the size of that file via ProcessFiles service + you have just deployed +

+
+ + setFileCID(e.target.value)} + value={fileCID} + /> +
+
+ +
+
+
+ + +
+
+ + +
+ + ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/Components/TextInput.tsx b/intro/4-ipfs-code-execution/web/src/Components/TextInput.tsx new file mode 100644 index 0000000..1ba57fd --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/Components/TextInput.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +export const TextInput = (props: { + text: string; + value: string | null; + setValue: (val: string) => void; +}) => { + return ( +
+ + props.setValue(e.target.value)} + value={props.value || ""} + required={true} + /> +
+ ); +}; diff --git a/intro/4-ipfs-code-execution/web/src/index.tsx b/intro/4-ipfs-code-execution/web/src/index.tsx index 62adcf3..666ddbc 100644 --- a/intro/4-ipfs-code-execution/web/src/index.tsx +++ b/intro/4-ipfs-code-execution/web/src/index.tsx @@ -1,12 +1,14 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import App from "./App"; +import { RecoilRoot } from "recoil"; ReactDOM.render( - + + + , - document.getElementById('root') + document.getElementById("root") ); - diff --git a/intro/4-ipfs-code-execution/web/src/state.ts b/intro/4-ipfs-code-execution/web/src/state.ts new file mode 100644 index 0000000..2a18a2d --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/state.ts @@ -0,0 +1,187 @@ +import { get_external_api_multiaddr } from "@fluencelabs/aqua-ipfs"; +import { + deploy_service, + put_file_size, + remove_service, +} from "@fluencelabs/ipfs-execution"; +import { createClient, FluenceClient } from "@fluencelabs/fluence"; +import { stage } from "@fluencelabs/fluence-network-environment"; +import { + atom, + selector, + useRecoilState, + useRecoilValue, + useSetRecoilState, +} from "recoil"; +import { decapsulateP2P, fromOption } from "./util"; + +export const relayNodes = [stage[0], stage[1], stage[2]]; + +export const clientState = atom({ + key: "clientState", + default: null, + dangerouslyAllowMutability: true, +}); + +export const serviceIdState = atom({ + key: "serviceIdState", + default: null, +}); + +export const wasmState = atom({ + key: "serviceState", + default: "Qmf8fH2cDZXGKS9uDGBcHxv5uQ51ChrigdZKe3QxS2C1AF", +}); + +export const rpcAddrState = atom({ + key: "rpcAddrState", + default: null, +}); + +export const fileCIDState = atom({ + key: "fileCIDState", + default: "", +}); + +export const fileSizeState = atom({ + key: "fileSizeState", + default: "", +}); + +export const fileSizeCIDState = atom({ + key: "fileSizeCIDState", + default: "", +}); + +export const isConnectedState = selector({ + key: "isConnectedState", + get: ({ get }) => { + const client = get(clientState); + + return client !== null && client.isConnected; + }, + dangerouslyAllowMutability: true, +}); + +export const gotRpcAddrState = selector({ + key: "getRpcAddrState", + get: ({ get }) => { + const rpcAddr = get(rpcAddrState); + + return rpcAddr !== null; + }, +}); + +export const isDeployedState = selector({ + key: "isDeployedState", + get: ({ get }) => { + const serviceId = get(serviceIdState); + + return serviceId !== null; + }, +}); + +const getRpcAddr = async (client: FluenceClient) => { + if (client === null) { + console.log("getRpcAddr client is null"); + return; + } + + let result = await get_external_api_multiaddr(client, client.relayPeerId!); + console.log("getRpcAddr result", result); + let rpcAddr = result.multiaddr; + return decapsulateP2P(rpcAddr); +}; + +export const useClientConnect = () => { + const setClient = useSetRecoilState(clientState); + const setRpcAddr = useSetRecoilState(rpcAddrState); + + const connect = async (relayPeerId: string) => { + try { + const client = await createClient(relayPeerId); + const addr = await getRpcAddr(client); + setRpcAddr(addr!); + setClient(client); + } catch (err) { + console.log("Client initialization failed", err); + } + }; + + return connect; +}; + +export const useDeployService = () => { + const wasm = useRecoilValue(wasmState); + const rpcAddr = useRecoilValue(rpcAddrState); + const client = useRecoilValue(clientState); + const setServiceId = useSetRecoilState(serviceIdState); + + return async () => { + console.log("wasm %s rpcAddr %s", wasm, rpcAddr); + if (client === null || wasm === null || rpcAddr === null) { + return; + } + + var service_id = await deploy_service( + client, + client.relayPeerId!, + wasm, + rpcAddr, + (msg, value) => console.log(msg, value), + { ttl: 10000 } + ); + service_id = fromOption(service_id); + setServiceId(service_id); + }; +}; + +export const useGetFileSize = () => { + const rpcAddr = useRecoilValue(rpcAddrState); + const client = useRecoilValue(clientState); + const serviceId = useRecoilValue(serviceIdState); + const setFileSize = useSetRecoilState(fileSizeState); + const [fileCID, setFileSizeCID] = useRecoilState(fileCIDState); + + return async () => { + if (client === null || serviceId === null || rpcAddr === null) { + return; + } + + var putResult = await put_file_size( + client, + client.relayPeerId!, + fileCID, + rpcAddr, + serviceId, + (size) => setFileSize(size.toString()), + (label, error) => setFileSize("Error: " + label + ": " + error), + { ttl: 10000 } + ); + putResult = fromOption(putResult); + if (putResult === null) { + return; + } + if (putResult.success) { + setFileSizeCID(putResult.hash); + } else { + setFileSizeCID("Error: " + putResult.error); + } + }; +}; + +export const useRemoveServuce = () => { + const client = useRecoilValue(clientState); + const [serviceId, setServiceId] = useRecoilState(serviceIdState); + + return async () => { + if (client === null || serviceId === null) { + return; + } + + await remove_service(client, client.relayPeerId!, serviceId, { + ttl: 10000, + }); + setServiceId(null); + }; +}; diff --git a/intro/4-ipfs-code-execution/web/src/util.ts b/intro/4-ipfs-code-execution/web/src/util.ts new file mode 100644 index 0000000..6f3a42f --- /dev/null +++ b/intro/4-ipfs-code-execution/web/src/util.ts @@ -0,0 +1,26 @@ +import { Multiaddr, protocols } from "multiaddr"; + +export const decapsulateP2P = (rpcAddr: string) => { + return new Multiaddr(rpcAddr) + .decapsulateCode(protocols.names.p2p.code) + .toString(); +}; + +export const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; + +export const fromOption = (opt: T | T[] | null): T | null => { + if (Array.isArray(opt)) { + if (opt.length === 0) { + return null; + } + + opt = opt[0]; + } + if (opt === null) { + return null; + } + + return opt; +};