diff --git a/mint-dapp/.env b/mint-dapp/.env index b0950c5..3de6ef7 100644 --- a/mint-dapp/.env +++ b/mint-dapp/.env @@ -1,3 +1,7 @@ REACT_APP_CONTRACT_NAME="CIS2-Multi" REACT_APP_MODULE_REF="312f99d6406868e647359ea816e450eac0ecc4281c2665a24936e6793535c9f6" -REACT_APP_CONTRACT_SCHEMA="FFFF02010000000A000000434953322D4D756C7469000A0000000900000062616C616E63654F6606100114000200000008000000746F6B656E5F69641D0007000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202040000006D696E7404140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7312021D000F1400020000000300000075726C1601040000006861736816011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F0000006F6E526563656976696E67434953320315040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020A0000006F70657261746F724F66061001140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C07000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10010115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F000000736574496D706C656D656E746F72730414000200000002000000696416000C000000696D706C656D656E746F727310020C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720208000000737570706F727473061001160010011503000000090000004E6F537570706F72740207000000537570706F72740209000000537570706F72744279010100000010000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020D000000746F6B656E4D657461646174610610011D0010011400020000000300000075726C160104000000686173681502000000040000004E6F6E650204000000536F6D65010100000013200000000215040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202080000007472616E7366657204100114000500000008000000746F6B656E5F69641D0006000000616D6F756E741B250000000400000066726F6D1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C02000000746F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401020000000C160104000000646174611D0115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020E0000007570646174654F70657261746F720410011400020000000600000075706461746515020000000600000052656D6F7665020300000041646402080000006F70657261746F721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720204000000766965770114000200000005000000737461746510020F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C1400020000000800000062616C616E63657310020F1D001B25000000090000006F70657261746F727310021502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7310021D00" \ No newline at end of file +REACT_APP_CONTRACT_SCHEMA="FFFF02010000000A000000434953322D4D756C7469000A0000000900000062616C616E63654F6606100114000200000008000000746F6B656E5F69641D0007000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202040000006D696E7404140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7312021D000F1400020000000300000075726C1601040000006861736816011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F0000006F6E526563656976696E67434953320315040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020A0000006F70657261746F724F66061001140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C07000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10010115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F000000736574496D706C656D656E746F72730414000200000002000000696416000C000000696D706C656D656E746F727310020C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720208000000737570706F727473061001160010011503000000090000004E6F537570706F72740207000000537570706F72740209000000537570706F72744279010100000010000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020D000000746F6B656E4D657461646174610610011D0010011400020000000300000075726C160104000000686173681502000000040000004E6F6E650204000000536F6D65010100000013200000000215040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202080000007472616E7366657204100114000500000008000000746F6B656E5F69641D0006000000616D6F756E741B250000000400000066726F6D1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C02000000746F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401020000000C160104000000646174611D0115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020E0000007570646174654F70657261746F720410011400020000000600000075706461746515020000000600000052656D6F7665020300000041646402080000006F70657261746F721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720204000000766965770114000200000005000000737461746510020F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C1400020000000800000062616C616E63657310020F1D001B25000000090000006F70657261746F727310021502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7310021D00" +## Check https://docs.pinata.cloud/pinata-api/authentication to get Pinata JWT +REACT_APP_PINATA_JWT="" +## This is the default IPFS gateway. See more at https://docs.ipfs.tech/concepts/ipfs-gateway +REACT_APP_GATEWAY_URL="https://ipfs.io/ipfs" \ No newline at end of file diff --git a/mint-dapp/Tutorial.md b/mint-dapp/Tutorial.md index dc8371b..ffa396d 100644 --- a/mint-dapp/Tutorial.md +++ b/mint-dapp/Tutorial.md @@ -204,9 +204,67 @@ import { UpdateContractPayload, } from "@concordium/web-sdk"; import { Button, Link, Stack, TextField, Typography } from "@mui/material"; -import { FormEvent, useState } from "react"; +import { ChangeEvent, FormEvent, useState } from "react"; import { Buffer } from "buffer/"; +const mint = async (formValues: { + index: bigint; + subindex: bigint; + metadataUrl: string; + tokenId: string; + quantity: number; +}) => { + const provider = await detectConcordiumProvider(); + const account = await provider.connect(); + + if (!account) { + return Promise.reject(new Error("Could not connect")); + } + + const address = { index: formValues.index, subindex: formValues.subindex }; + const paramJson = { + owner: { + Account: [account], + }, + tokens: [ + [ + formValues.tokenId, + [ + { + url: formValues.metadataUrl, + hash: "", + }, + formValues.quantity.toString(), + ], + ], + ], + }; + + const schemaBuffer = Buffer.from( + process.env.REACT_APP_CONTRACT_SCHEMA!, + "hex" + ); + const serializedParams = serializeUpdateContractParameters( + process.env.REACT_APP_CONTRACT_NAME!, + "mint", + paramJson, + schemaBuffer + ); + return provider.sendTransaction( + account!, + AccountTransactionType.Update, + { + address, + message: serializedParams, + receiveName: `${process.env.REACT_APP_CONTRACT_NAME!}.mint`, + amount: new CcdAmount(BigInt(0)), + maxContractExecutionEnergy: BigInt(9999), + } as UpdateContractPayload, + paramJson, + schemaBuffer.toString("base64") + ); +}; + export default function Mint() { let [state, setState] = useState({ checking: false, @@ -214,19 +272,34 @@ export default function Mint() { hash: "", }); + const [formData, setFormData] = useState({ + contractIndex: "", + contractSubIndex: "0", + metadataUrl: "", + tokenId: "01", + quantity: "1", + }); + + const handleChange = (event: ChangeEvent) => { + setFormData({ + ...formData, + [event.target.name]: event.target.value, + }); + }; + const submit = async (event: FormEvent) => { event.preventDefault(); setState({ ...state, error: "", checking: true, hash: "" }); - const formData = new FormData(event.currentTarget); var formValues = { - index: BigInt(formData.get("contractIndex")?.toString() || "-1"), - subindex: BigInt(formData.get("contractSubindex")?.toString() || "-1"), - metadataUrl: formData.get("metadataUrl")?.toString() || "", - tokenId: formData.get("tokenId")?.toString() || "", - quantity: parseInt(formData.get("quantity")?.toString() || "-1"), + index: BigInt(formData.contractIndex || "-1"), + subindex: BigInt(formData.contractSubIndex || "-1"), + metadataUrl: formData.metadataUrl || "", + tokenId: formData.tokenId || "", + quantity: parseInt(formData.quantity || "-1"), }; + //form validations if (!(formValues.index >= 0)) { setState({ ...state, error: "Invalid Contract Index" }); return; @@ -252,61 +325,13 @@ export default function Mint() { return; } - const provider = await detectConcordiumProvider(); - const account = await provider.connect(); - - if (!account) { - alert("Please connect"); - } - - const address = { index: formValues.index, subindex: formValues.subindex }; - const paramJson = { - owner: { - Account: [account], - }, - tokens: [ - [ - formValues.tokenId, - [ - { - url: formValues.metadataUrl, - hash: "", - }, - formValues.quantity.toString(), - ], - ], - ], - }; - - try { - const schemaBuffer = Buffer.from( - process.env.REACT_APP_CONTRACT_SCHEMA!, - "base64" - ); - const serializedParams = serializeUpdateContractParameters( - process.env.REACT_APP_CONTRACT_NAME!, - "mint", - paramJson, - schemaBuffer + mint(formValues) + .then((txnHash) => + setState({ checking: false, error: "", hash: txnHash }) + ) + .catch((err) => + setState({ checking: false, error: err.message, hash: "" }) ); - const txnHash = await provider.sendTransaction( - account!, - AccountTransactionType.Update, - { - address, - message: serializedParams, - receiveName: `${process.env.REACT_APP_CONTRACT_NAME!}.mint`, - amount: new CcdAmount(BigInt(0)), - maxContractExecutionEnergy: BigInt(9999), - } as UpdateContractPayload, - paramJson, - process.env.REACT_APP_CONTRACT_SCHEMA! - ); - - setState({ checking: false, error: "", hash: txnHash }); - } catch (error: any) { - setState({ checking: false, error: error.message || error, hash: "" }); - } }; return ( @@ -323,6 +348,8 @@ export default function Mint() { variant="standard" type={"number"} disabled={state.checking} + value={formData.contractIndex} + onChange={handleChange} /> {state.error && ( @@ -379,13 +412,16 @@ export default function Mint() { size="large" disabled={state.checking} > - Mint + {" "} + Mint{" "} ); } ``` + - Update [`App.tsx`](./src/App.tsx) with the newly added component + ```tsx import "./App.css"; import Header from "./Header"; @@ -410,4 +446,332 @@ export default function App() { ); } -``` \ No newline at end of file +``` + +### Adding Pinata Support +- First add the required project dependencies +```bash +yarn add axios react-material-file-upload +``` +- Lets add the Pinata related configuration to the [.env file](./.env) +```bash +## Check https://docs.pinata.cloud/pinata-api/authentication to get Pinata JWT +REACT_APP_PINATA_JWT="" +## This is the default IPFS gateway. See more at https://docs.ipfs.tech/concepts/ipfs-gateway +REACT_APP_GATEWAY_URL="https://ipfs.io/ipfs" +``` + +- Now add a Pinata Component which would later replace the Metadata Url field in the Mint Form. Add the code to the [Metadata Url Input](./src/MetadataUrlInput.tsx) file + +```tsx +import { TextField, TextFieldProps, Typography } from "@mui/material"; +import { Stack } from "@mui/system"; +import { useState } from "react"; +import { default as axios } from "axios"; +import FileUpload from "react-material-file-upload"; + +const uploadFile = async (file: File, fileName: string): Promise => { + const data = new FormData(); + data.append("file", file); + data.append("pinataMetadata", JSON.stringify({ name: fileName })); + + const response = await axios({ + method: "post", + url: `https://api.pinata.cloud/pinning/pinFileToIPFS`, + headers: { + Authorization: `Bearer ${process.env.REACT_APP_PINATA_JWT!}`, + }, + data: data, + }); + + return `${process.env.REACT_APP_GATEWAY_URL!}/${response.data.IpfsHash}`; +}; + +export default function MetadataUrlInput( + props: { onChange?: (name?: string, value?: string) => void } & Omit< + TextFieldProps, + "onChange" + > +) { + const [state, setState] = useState({ + value: props.value as string | undefined, + disabled: props.disabled, + error: "", + }); + + const onInputChanged = (value?: string) => { + props.onChange && props.onChange(props.name, value); + setState({ ...state, error: "", disabled: false, value }); + }; + + const onFileChanged = (files: File[]) => { + setState({ ...state, disabled: true }); + uploadFile(files[0], files[0].name) + .then((url) => { + props.onChange && props.onChange(props.name, url); + setState({ ...state, error: "", disabled: false, value: url }); + }) + .catch((err) => + setState({ error: err.message, disabled: false, value: undefined }) + ); + }; + return ( + + + {!state.value && ( + + )} + {state.value && ( + onInputChanged(e.target.value)} + /> + )} + + {state.error && {state.error}} + + ); +} +``` + +- Update the [Mint component](./src/Mint.tsx) to use the newly created component + +```tsx +import { detectConcordiumProvider } from "@concordium/browser-wallet-api-helpers"; +import { + AccountTransactionType, + CcdAmount, + serializeUpdateContractParameters, + UpdateContractPayload, +} from "@concordium/web-sdk"; +import { Button, Link, Stack, TextField, Typography } from "@mui/material"; +import { ChangeEvent, FormEvent, useState } from "react"; +import { Buffer } from "buffer/"; + +import MetadataUrlInput from "./MetadataUrlInput"; + +const mint = async (formValues: { + index: bigint; + subindex: bigint; + metadataUrl: string; + tokenId: string; + quantity: number; +}) => { + const provider = await detectConcordiumProvider(); + const account = await provider.connect(); + + if (!account) { + return Promise.reject(new Error("Could not connect")); + } + + const address = { index: formValues.index, subindex: formValues.subindex }; + const paramJson = { + owner: { + Account: [account], + }, + tokens: [ + [ + formValues.tokenId, + [ + { + url: formValues.metadataUrl, + hash: "", + }, + formValues.quantity.toString(), + ], + ], + ], + }; + + const schemaBuffer = Buffer.from( + process.env.REACT_APP_CONTRACT_SCHEMA!, + "hex" + ); + const serializedParams = serializeUpdateContractParameters( + process.env.REACT_APP_CONTRACT_NAME!, + "mint", + paramJson, + schemaBuffer + ); + return provider.sendTransaction( + account!, + AccountTransactionType.Update, + { + address, + message: serializedParams, + receiveName: `${process.env.REACT_APP_CONTRACT_NAME!}.mint`, + amount: new CcdAmount(BigInt(0)), + maxContractExecutionEnergy: BigInt(9999), + } as UpdateContractPayload, + paramJson, + schemaBuffer.toString("base64") + ); +}; + +export default function Mint() { + let [state, setState] = useState({ + checking: false, + error: "", + hash: "", + }); + + const [formData, setFormData] = useState({ + contractIndex: "", + contractSubIndex: "0", + metadataUrl: "", + tokenId: "01", + quantity: "1", + }); + + const handleChange = (name?: string, value?: string) => { + name && + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleChangeEvent = (event: ChangeEvent) => { + handleChange(event.target.name, event.target.value); + }; + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setState({ ...state, error: "", checking: true, hash: "" }); + + var formValues = { + index: BigInt(formData.contractIndex || "-1"), + subindex: BigInt(formData.contractSubIndex || "-1"), + metadataUrl: formData.metadataUrl || "", + tokenId: formData.tokenId || "", + quantity: parseInt(formData.quantity || "-1"), + }; + + //form validations + if (!(formValues.index >= 0)) { + setState({ ...state, error: "Invalid Contract Index" }); + return; + } + + if (!(formValues.subindex >= 0)) { + setState({ ...state, error: "Invalid Contract Subindex" }); + return; + } + + if (!(formValues.quantity >= 0)) { + setState({ ...state, error: "Invalid Quantity" }); + return; + } + + if (!formValues.metadataUrl) { + setState({ ...state, error: "Invalid Metadata Url" }); + return; + } + + if (!formValues.tokenId) { + setState({ ...state, error: "Invalid Token Id" }); + return; + } + + mint(formValues) + .then((txnHash) => + setState({ checking: false, error: "", hash: txnHash }) + ) + .catch((err) => + setState({ checking: false, error: err.message, hash: "" }) + ); + }; + + return ( + + + + + + + {state.error && ( + + {state.error} + + )} + {state.checking && Checking..} + {state.hash && ( + + View Transaction
+ {state.hash} + + )} + +
+ ); +} +``` diff --git a/mint-dapp/package.json b/mint-dapp/package.json index 27a30bc..374b6c2 100644 --- a/mint-dapp/package.json +++ b/mint-dapp/package.json @@ -16,8 +16,10 @@ "@types/node": "^16.7.13", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "axios": "^1.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-material-file-upload": "^0.0.4", "react-scripts": "5.0.1", "typescript": "^4.4.2", "web-vitals": "^2.1.0" diff --git a/mint-dapp/src/MetadataUrlInput.tsx b/mint-dapp/src/MetadataUrlInput.tsx new file mode 100644 index 0000000..0ce808e --- /dev/null +++ b/mint-dapp/src/MetadataUrlInput.tsx @@ -0,0 +1,75 @@ +import { TextField, TextFieldProps, Typography } from "@mui/material"; +import { Stack } from "@mui/system"; +import { useState } from "react"; +import { default as axios } from "axios"; +import FileUpload from "react-material-file-upload"; + +const uploadFile = async (file: File, fileName: string): Promise => { + const data = new FormData(); + data.append("file", file); + data.append("pinataMetadata", JSON.stringify({ name: fileName })); + + const response = await axios({ + method: "post", + url: `https://api.pinata.cloud/pinning/pinFileToIPFS`, + headers: { + Authorization: `Bearer ${process.env.REACT_APP_PINATA_JWT!}`, + }, + data: data, + }); + + return `${process.env.REACT_APP_GATEWAY_URL!}/${response.data.IpfsHash}`; +}; + +export default function MetadataUrlInput( + props: { onChange?: (name?: string, value?: string) => void } & Omit< + TextFieldProps, + "onChange" + > +) { + const [state, setState] = useState({ + value: props.value as string | undefined, + disabled: props.disabled, + error: "", + }); + + const onInputChanged = (value?: string) => { + props.onChange && props.onChange(props.name, value); + setState({ ...state, error: "", disabled: false, value }); + }; + + const onFileChanged = (files: File[]) => { + setState({ ...state, disabled: true }); + uploadFile(files[0], files[0].name) + .then((url) => { + props.onChange && props.onChange(props.name, url); + setState({ ...state, error: "", disabled: false, value: url }); + }) + .catch((err) => + setState({ error: err.message, disabled: false, value: undefined }) + ); + }; + return ( + + + {!state.value && ( + + )} + {state.value && onInputChanged(e.target.value)} + />} + + {state.error && {state.error}} + + ); +} diff --git a/mint-dapp/src/Mint.tsx b/mint-dapp/src/Mint.tsx index 3a4747a..13ec5fb 100644 --- a/mint-dapp/src/Mint.tsx +++ b/mint-dapp/src/Mint.tsx @@ -6,9 +6,69 @@ import { UpdateContractPayload, } from "@concordium/web-sdk"; import { Button, Link, Stack, TextField, Typography } from "@mui/material"; -import { FormEvent, useState } from "react"; +import { ChangeEvent, FormEvent, useState } from "react"; import { Buffer } from "buffer/"; +import MetadataUrlInput from "./MetadataUrlInput"; + +const mint = async (formValues: { + index: bigint; + subindex: bigint; + metadataUrl: string; + tokenId: string; + quantity: number; +}) => { + const provider = await detectConcordiumProvider(); + const account = await provider.connect(); + + if (!account) { + return Promise.reject(new Error("Could not connect")); + } + + const address = { index: formValues.index, subindex: formValues.subindex }; + const paramJson = { + owner: { + Account: [account], + }, + tokens: [ + [ + formValues.tokenId, + [ + { + url: formValues.metadataUrl, + hash: "", + }, + formValues.quantity.toString(), + ], + ], + ], + }; + + const schemaBuffer = Buffer.from( + process.env.REACT_APP_CONTRACT_SCHEMA!, + "hex" + ); + const serializedParams = serializeUpdateContractParameters( + process.env.REACT_APP_CONTRACT_NAME!, + "mint", + paramJson, + schemaBuffer + ); + return provider.sendTransaction( + account!, + AccountTransactionType.Update, + { + address, + message: serializedParams, + receiveName: `${process.env.REACT_APP_CONTRACT_NAME!}.mint`, + amount: new CcdAmount(BigInt(0)), + maxContractExecutionEnergy: BigInt(9999), + } as UpdateContractPayload, + paramJson, + schemaBuffer.toString("base64") + ); +}; + export default function Mint() { let [state, setState] = useState({ checking: false, @@ -16,19 +76,39 @@ export default function Mint() { hash: "", }); + const [formData, setFormData] = useState({ + contractIndex: "", + contractSubIndex: "0", + metadataUrl: "", + tokenId: "01", + quantity: "1", + }); + + const handleChange = (name?: string, value?: string) => { + name && + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleChangeEvent = (event: ChangeEvent) => { + handleChange(event.target.name, event.target.value); + }; + const submit = async (event: FormEvent) => { event.preventDefault(); setState({ ...state, error: "", checking: true, hash: "" }); - const formData = new FormData(event.currentTarget); var formValues = { - index: BigInt(formData.get("contractIndex")?.toString() || "-1"), - subindex: BigInt(formData.get("contractSubindex")?.toString() || "-1"), - metadataUrl: formData.get("metadataUrl")?.toString() || "", - tokenId: formData.get("tokenId")?.toString() || "", - quantity: parseInt(formData.get("quantity")?.toString() || "-1"), + index: BigInt(formData.contractIndex || "-1"), + subindex: BigInt(formData.contractSubIndex || "-1"), + metadataUrl: formData.metadataUrl || "", + tokenId: formData.tokenId || "", + quantity: parseInt(formData.quantity || "-1"), }; + //form validations if (!(formValues.index >= 0)) { setState({ ...state, error: "Invalid Contract Index" }); return; @@ -54,61 +134,13 @@ export default function Mint() { return; } - const provider = await detectConcordiumProvider(); - const account = await provider.connect(); - - if (!account) { - alert("Please connect"); - } - - const address = { index: formValues.index, subindex: formValues.subindex }; - const paramJson = { - owner: { - Account: [account], - }, - tokens: [ - [ - formValues.tokenId, - [ - { - url: formValues.metadataUrl, - hash: "", - }, - formValues.quantity.toString(), - ], - ], - ], - }; - - try { - const schemaBuffer = Buffer.from( - process.env.REACT_APP_CONTRACT_SCHEMA!, - "hex" - ); - const serializedParams = serializeUpdateContractParameters( - process.env.REACT_APP_CONTRACT_NAME!, - "mint", - paramJson, - schemaBuffer - ); - const txnHash = await provider.sendTransaction( - account!, - AccountTransactionType.Update, - { - address, - message: serializedParams, - receiveName: `${process.env.REACT_APP_CONTRACT_NAME!}.mint`, - amount: new CcdAmount(BigInt(0)), - maxContractExecutionEnergy: BigInt(9999), - } as UpdateContractPayload, - paramJson, - schemaBuffer.toString("base64") + mint(formValues) + .then((txnHash) => + setState({ checking: false, error: "", hash: txnHash }) + ) + .catch((err) => + setState({ checking: false, error: err.message, hash: "" }) ); - - setState({ checking: false, error: "", hash: txnHash }); - } catch (error: any) { - setState({ checking: false, error: error.message || error, hash: "" }); - } }; return ( @@ -125,6 +157,8 @@ export default function Mint() { variant="standard" type={"number"} disabled={state.checking} + value={formData.contractIndex} + onChange={handleChangeEvent} /> - {state.error && ( diff --git a/mint-dapp/yarn.lock b/mint-dapp/yarn.lock index 4fa822e..5ce56f8 100644 --- a/mint-dapp/yarn.lock +++ b/mint-dapp/yarn.lock @@ -2951,6 +2951,11 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^10.4.13: version "10.4.13" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8" @@ -2973,6 +2978,15 @@ axe-core@^4.4.3: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.1.tgz#79cccdee3e3ab61a8f42c458d4123a6768e6fbce" integrity sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w== +axios@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" + integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -4762,6 +4776,13 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" + integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== + dependencies: + tslib "^2.0.3" + filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -4844,7 +4865,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -4884,6 +4905,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -7764,6 +7794,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -7880,6 +7915,15 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-dropzone@^11.4.2: + version "11.7.1" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" + integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.4.0" + prop-types "^15.8.1" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" @@ -7900,6 +7944,13 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-material-file-upload@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/react-material-file-upload/-/react-material-file-upload-0.0.4.tgz#c829723076d1da52ddc360827f170acc8c321406" + integrity sha512-gXRPpOc3hZdrNiVR4SptGCGrSjOVQkkxnSsOU4++937qcmp4W3N8n/s0LPIc8Pd3T4McK0XaWegGAUPEO5riaA== + dependencies: + react-dropzone "^11.4.2" + react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"