From f97ec7062aa9537bb59e6030bb9139f2f845edba Mon Sep 17 00:00:00 2001 From: XuananLe Date: Wed, 11 Sep 2024 08:45:27 +0700 Subject: [PATCH 01/15] Update solana-mobile-dapps-with-expo.md --- .../mobile/solana-mobile-dapps-with-expo.md | 212 ++++++++++-------- 1 file changed, 122 insertions(+), 90 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index d6707b4b8..069e8c716 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -37,7 +37,7 @@ lesson will be spent in the lab. ### React Native Expo -Expo is an open-source collection of tools and libraries that wrap around React +Expo is an open-source platform for making universal native apps for Android, iOS, and the web that wrap around React Native, much like Next.js is a framework built on top of React. Expo consists of three main parts: @@ -64,7 +64,7 @@ with the Solana mobile SDK. Coming from the > fully compatible with Expo. Lastly, and most importantly, Expo does an amazing job providing -[easy-to-use libraries](https://docs.expo.dev/versions/latest/) that give you +[comprehensive libraries](https://docs.expo.dev/versions/latest/) that give you access to the device's onboard peripherals, such as camera, battery, and speakers. The libraries are intuitive and the documentation is phenomenal. @@ -73,12 +73,12 @@ speakers. The libraries are intuitive and the documentation is phenomenal. To get started with Expo, you first need the prerequisite setup described in the [Introduction to Solana Mobile lesson](/content/courses/mobile/intro-to-solana-mobile). After that, you'll want to sign up for an -[Expo Application Services (EAS) account](https://expo.dev/). +[Expo Application Services (EAS) account](https://expo.dev/eas). Once you have an EAS account, you can install the EAS CLI and log in: ```bash -npm install --global eas-cli +npm install -g eas-cli eas login ``` @@ -120,13 +120,10 @@ the following inside this file: } ``` -With the EAS config file created, you can build using the -`npx eas build --local` command plus relevant flags for any additional -requirements. For example, the following will build the project locally with a -development profile specifically for Android: +With the EAS configuration file in place, you can build your project using the ```eas build``` command along with relevant flags to meet any additional requirements. This command submits a job to the EAS Build service, where your APK is built using Expo's cloud infrastructure. If you want to build locally, you can add the ```--local``` flag. For example, the following command builds the project locally with a development profile specifically for Android: ```bash -npx eas build --profile development --platform android --local +eas build --profile development --platform android --message "Developing on Android!" --local ``` You then need to install the output APK to your device or emulator. If you're @@ -168,9 +165,9 @@ JS/TS. import { Pedometer } from "expo-sensors"; ``` -Depending on the package, there may be additional setup required. Be sure to -read the [docs](https://docs.expo.dev/versions/latest/) when working with a new -package. +Depending on the package, there may be additional setup required. +For example, if you're using the ```expo-camera``` package, you not only need to install the package but also configure the appropriate permissions in your ```app.json``` or ```AndroidManifest.xml``` file for Android and request runtime permissions for accessing the camera. +Be sure to read the [docs](https://docs.expo.dev/versions/latest/) when working with a new package. ### Integrate ecosystem libraries into your Expo app @@ -204,8 +201,8 @@ For a Solana + Expo app, you'll need the following: as `Transaction` and `Uint8Array`. - `@solana/web3.js`: Solana Web Library for interacting with the Solana network through the [JSON RPC API](https://docs.solana.com/api/http). -- `react-native-get-random-values`: Secure random number generator polyfill - for `web3.js` underlying Crypto library on React Native. +- `expo-crypto`: Secure random number generator polyfill. + for `web3.js` underlying Crypto library on React Native. (This only works for Expo SDK Version 49+ and Expo Router, so make sure you update) - `buffer`: Buffer polyfill needed for `web3.js` on React Native. #### Metaplex Polyfills @@ -213,36 +210,38 @@ For a Solana + Expo app, you'll need the following: If you want to use the Metaplex SDK, you'll need to add the Metaplex library plus a few additional polyfills: -- `@metaplex-foundation/js@0.19.4` - Metaplex Library +- `@metaplex-foundation/umi` `@metaplex-foundation/umi-bundle-defaults` `@metaplex-foundation/mpl-core` - Metaplex Library - Several more polyfills - `assert` - - `util` - `crypto-browserify` - - `stream-browserify` - `readable-stream` - - `browserify-zlib` - - `path-browserify` + - `zlib` - `react-native-url-polyfill` - All of the libraries that the above polyfills are meant to replace are utilized -by the Metaplex library in the background. It's unlikely you'll be importing any +by the Metaplex libraries in the background. It's unlikely you'll be importing any of them into your code directly. Because of this, you'll need to register the polyfills using a `metro.config.js` file. This will ensure that Metaplex uses the polyfills instead of the usual Node.js libraries that aren't supported in React Native. Below is an example `metro.config.js` file: ```js +// Import the default Expo Metro config const { getDefaultConfig } = require("@expo/metro-config"); + +// Get the default Expo Metro configuration const defaultConfig = getDefaultConfig(__dirname); +// Customize the configuration to include your extra node modules defaultConfig.resolver.extraNodeModules = { crypto: require.resolve("crypto-browserify"), stream: require.resolve("readable-stream"), url: require.resolve("react-native-url-polyfill"), zlib: require.resolve("browserify-zlib"), path: require.resolve("path-browserify"), + crypto : require.resolve('expo-crypto') }; +// Export the modified configuration module.exports = defaultConfig; ``` @@ -293,7 +292,7 @@ it to run. We use 5GB of ram on our side. To simplify the Expo process, you'll want an Expo Application Services (EAS) account. This will help you build and run the application. -First sign up for an [EAS account](https://expo.dev/). +First sign up for an [EAS account](https://expo.dev/eas). Then, install the EAS CLI and log in: @@ -307,13 +306,13 @@ eas login Let’s create our app with the following: ```bash -npx create-expo-app -t expo-template-blank-typescript solana-expo +npx create-expo-app --template blank-typescript solana-expo cd solana-expo +npx expo install expo-dev-client # A library that allows creating a development build and includes useful development tools. It is optional but recommended. ``` This uses `create-expo-app` to generate a new scaffold for us based on the -`expo-template-blank-typescript` template. This is just an empty Typescript -React Native app. +`blank-typescript` template. A Blank template with TypeScript enabled. #### 3. Local build config @@ -351,7 +350,7 @@ Copy and paste the following into the newly created `eas.json`: #### 4. Build and emulate -Now let's build the project. You will choose `y` for every answer. This will +Now let's build the project locally. You will choose `y` for every answer. This will take a while to complete. ```bash @@ -387,15 +386,14 @@ already have a Devnet-enabled wallet installed you can skip step 0. You'll need a wallet that supports Devnet to test with. In [our Mobile Wallet Adapter lesson](/content/courses/mobile/mwa-deep-dive) we -created one of these. Let's install it from the solution branch in a different +created one of these. Let's install it from the repo in a different directory from our app: ```bash cd .. -git clone https://github.com/Unboxed-Software/react-native-fake-solana-wallet +git clone https://github.com/XuananLe/react-native-fake-solana-wallet cd react-native-fake-solana-wallet -git checkout solution -npm run install +npm install ``` The wallet should be installed on your emulator or device. Make sure to open the @@ -420,7 +418,7 @@ npm install \ @solana/web3.js \ @solana-mobile/mobile-wallet-adapter-protocol-web3js \ @solana-mobile/mobile-wallet-adapter-protocol \ - react-native-get-random-values \ + expo-crypto \ buffer ``` @@ -460,18 +458,42 @@ export function MainScreen() { } ``` +Next, create `polyfills.ts` for react-native to work with all solana + +```typescript +import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto"; +import { Buffer } from "buffer"; + +global.Buffer = Buffer; + +// getRandomValues polyfill +class Crypto { + getRandomValues = expoCryptoGetRandomValues; +} + +const webCrypto = typeof crypto !== "undefined" ? crypto : new Crypto(); + +(() => { + if (typeof crypto === "undefined") { + Object.defineProperty(window, "crypto", { + configurable: true, + enumerable: true, + get: () => webCrypto, + + }); + } +})(); +``` + Finally, let's change `App.tsx` to wrap our application in the two providers we just created: ```tsx -import "react-native-get-random-values"; -import { StatusBar } from "expo-status-bar"; -import { StyleSheet, Text, View } from "react-native"; import { ConnectionProvider } from "./components/ConnectionProvider"; import { AuthorizationProvider } from "./components/AuthProvider"; import { clusterApiUrl } from "@solana/web3.js"; -import { MainScreen } from "./screens/MainScreen"; -global.Buffer = require("buffer").Buffer; +import { MainScreen } from "./screens/MainScreen" +import "./polyfills" export default function App() { const cluster = "devnet"; @@ -492,21 +514,36 @@ export default function App() { ``` Notice we've added two polyfills above: `buffer` and -`react-native-get-random-values`. These are necessary for the Solana +`expo-crypto`. These are necessary for the Solana dependencies to run correctly. #### 4. Build and run Solana boilerplate +Add these run script to your package.json + +```json + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "build": "npx eas build --profile development --platform android", + "build:local": "npx eas build --profile development --platform android --local", + "test": "echo \"No tests specified\" && exit 0" + }, + +``` + Let's make sure everything is working and compiling correctly. In Expo, anytime you change the dependencies, you'll need to rebuild and re-install the app. **_Optional:_** To avoid possible build version conflicts, you may want to _uninstall_ the previous version before you drag and drop the new one in. -Build: +Build locally: ```bash -npx eas build --profile development --platform android --local +npm run build:local ``` Install: **_Drag_** the resulting build file into your emulator. @@ -514,7 +551,7 @@ Install: **_Drag_** the resulting build file into your emulator. Run: ```bash -npx expo start --dev-client --android +npm run android ``` Everything should compile and you should have a boilerplate Solana Expo app. @@ -541,7 +578,9 @@ npm install assert \ browserify-zlib \ path-browserify \ react-native-url-polyfill \ - @metaplex-foundation/js@0.19.4 + @metaplex-foundation/umi \ + @metaplex-foundation/umi-bundle-defaults \ + @metaplex-foundation/mpl-candy-machine ``` #### 2. Polyfill config @@ -569,10 +608,19 @@ defaultConfig.resolver.extraNodeModules = { url: require.resolve("react-native-url-polyfill"), zlib: require.resolve("browserify-zlib"), path: require.resolve("path-browserify"), + crypto : require.resolve('expo-crypto'), }; // Export the modified configuration -module.exports = defaultConfig; +module.exports = { + ...defaultConfig, + // See more why we have to do here at: https://github.com/metaplex-foundation/umi/issues/94 + resolver: { + ...defaultConfig.resolver, + unstable_enablePackageExports: true, + }, +} + ``` #### 3. Metaplex provider @@ -585,26 +633,15 @@ an `IdentitySigner` for the `Metaplex` object to use. This allows it to call several privileged functions on our behalf: ```tsx -import { - IdentitySigner, - Metaplex, - MetaplexPlugin, -} from "@metaplex-foundation/js"; -import { - transact, - Web3MobileWallet, -} from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; -import { Connection, Transaction } from "@solana/web3.js"; +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplCandyMachine } from '@metaplex-foundation/mpl-candy-machine'; +import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'; +import { transact, Web3MobileWallet } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; +import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; import { useMemo } from "react"; import { Account } from "./AuthProvider"; -export const mobileWalletAdapterIdentity = ( - mwaIdentitySigner: IdentitySigner, -): MetaplexPlugin => ({ - install(metaplex: Metaplex) { - metaplex.identity().setDriver(mwaIdentitySigner); - }, -}); +type Web3JsTransactionOrVersionedTransaction = Transaction | VersionedTransaction; export const useMetaplex = ( connection: Connection, @@ -613,54 +650,46 @@ export const useMetaplex = ( ) => { return useMemo(() => { if (!selectedAccount || !authorizeSession) { - return { mwaIdentitySigner: null, metaplex: null }; + return { umi: null }; } - const mwaIdentitySigner: IdentitySigner = { + const mobileWalletAdapter = { publicKey: selectedAccount.publicKey, signMessage: async (message: Uint8Array): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); - const signedMessages = await wallet.signMessages({ addresses: [selectedAccount.publicKey.toBase58()], payloads: [message], }); - return signedMessages[0]; }); }, - signTransaction: async ( - transaction: Transaction, - ): Promise => { + signTransaction: async (transaction: T): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); - const signedTransactions = await wallet.signTransactions({ transactions: [transaction], }); - - return signedTransactions[0]; + return signedTransactions[0] as T; }); }, - signAllTransactions: async ( - transactions: Transaction[], - ): Promise => { + signAllTransactions: async (transactions: T[]): Promise => { return transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ transactions: transactions, }); - return signedTransactions; + return signedTransactions as T[]; }); }, }; - const metaplex = Metaplex.make(connection).use( - mobileWalletAdapterIdentity(mwaIdentitySigner), - ); + const umi = createUmi(connection.rpcEndpoint) + .use(mplCandyMachine()) + .use(walletAdapterIdentity(mobileWalletAdapter)); - return { metaplex }; + return { umi }; }, [authorizeSession, selectedAccount, connection]); }; ``` @@ -680,15 +709,20 @@ import "react-native-url-polyfill/auto"; import { useConnection } from "./ConnectionProvider"; import { Account, useAuthorization } from "./AuthProvider"; import React, { ReactNode, createContext, useContext, useState } from "react"; -import { useMetaplex } from "./MetaplexProvider"; +import { useUmi } from "./MetaplexProvider"; // Update this import to match your file structure +import { Umi } from "@metaplex-foundation/umi"; export interface NFTProviderProps { children: ReactNode; } -export interface NFTContextState {} +export interface NFTContextState { + umi: Umi | null; +} -const DEFAULT_NFT_CONTEXT_STATE: NFTContextState = {}; +const DEFAULT_NFT_CONTEXT_STATE: NFTContextState = { + umi: null, +}; const NFTContext = createContext(DEFAULT_NFT_CONTEXT_STATE); @@ -698,9 +732,11 @@ export function NFTProvider(props: NFTProviderProps) { const { connection } = useConnection(); const { authorizeSession } = useAuthorization(); const [account, setAccount] = useState(null); - const { metaplex } = useMetaplex(connection, account, authorizeSession); + const { umi } = useUmi(connection, account, authorizeSession); - const state = {}; + const state: NFTContextState = { + umi, + }; return {children}; } @@ -716,13 +752,11 @@ Notice we've added yet another polyfill to the top Now, let's wrap our new `NFTProvider` around `MainScreen` in `App.tsx`: ```tsx -import "react-native-get-random-values"; import { ConnectionProvider } from "./components/ConnectionProvider"; import { AuthorizationProvider } from "./components/AuthProvider"; import { clusterApiUrl } from "@solana/web3.js"; -import { MainScreen } from "./screens/MainScreen"; -import { NFTProvider } from "./components/NFTProvider"; -global.Buffer = require("buffer").Buffer; +import { MainScreen } from "./screens/MainScreen" +import "./polyfills" export default function App() { const cluster = "devnet"; @@ -735,9 +769,7 @@ export default function App() { config={{ commitment: "processed" }} > - - - + ); @@ -851,7 +883,7 @@ device's URI scheme and turn them into Blobs we can the upload to Install it with the following: ```bash -npm i rn-fetch-blob +npm install rn-fetch-blob ``` #### 3. Final build From 471870cff8bb2ca072fa3038efe2bb7bcbbe8c4e Mon Sep 17 00:00:00 2001 From: XuananLe Date: Wed, 11 Sep 2024 09:03:14 +0700 Subject: [PATCH 02/15] Run prettier the README file --- .../mobile/solana-mobile-dapps-with-expo.md | 97 +++++++++++-------- .../onchain-development/anchor-pdas.md | 2 +- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 069e8c716..4bdd002b5 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -37,8 +37,9 @@ lesson will be spent in the lab. ### React Native Expo -Expo is an open-source platform for making universal native apps for Android, iOS, and the web that wrap around React -Native, much like Next.js is a framework built on top of React. +Expo is an open-source platform for making universal native apps for Android, +iOS, and the web that wrap around React Native, much like Next.js is a framework +built on top of React. Expo consists of three main parts: @@ -120,7 +121,12 @@ the following inside this file: } ``` -With the EAS configuration file in place, you can build your project using the ```eas build``` command along with relevant flags to meet any additional requirements. This command submits a job to the EAS Build service, where your APK is built using Expo's cloud infrastructure. If you want to build locally, you can add the ```--local``` flag. For example, the following command builds the project locally with a development profile specifically for Android: +With the EAS configuration file in place, you can build your project using the +`eas build` command along with relevant flags to meet any additional +requirements. This command submits a job to the EAS Build service, where your +APK is built using Expo's cloud infrastructure. If you want to build locally, +you can add the `--local` flag. For example, the following command builds the +project locally with a development profile specifically for Android: ```bash eas build --profile development --platform android --message "Developing on Android!" --local @@ -165,9 +171,12 @@ JS/TS. import { Pedometer } from "expo-sensors"; ``` -Depending on the package, there may be additional setup required. -For example, if you're using the ```expo-camera``` package, you not only need to install the package but also configure the appropriate permissions in your ```app.json``` or ```AndroidManifest.xml``` file for Android and request runtime permissions for accessing the camera. -Be sure to read the [docs](https://docs.expo.dev/versions/latest/) when working with a new package. +Depending on the package, there may be additional setup required. For example, +if you're using the `expo-camera` package, you not only need to install the +package but also configure the appropriate permissions in your `app.json` or +`AndroidManifest.xml` file for Android and request runtime permissions for +accessing the camera. Be sure to read the +[docs](https://docs.expo.dev/versions/latest/) when working with a new package. ### Integrate ecosystem libraries into your Expo app @@ -202,7 +211,8 @@ For a Solana + Expo app, you'll need the following: - `@solana/web3.js`: Solana Web Library for interacting with the Solana network through the [JSON RPC API](https://docs.solana.com/api/http). - `expo-crypto`: Secure random number generator polyfill. - for `web3.js` underlying Crypto library on React Native. (This only works for Expo SDK Version 49+ and Expo Router, so make sure you update) + for `web3.js` underlying Crypto library on React Native. (This only works for + Expo SDK Version 49+ and Expo Router, so make sure you update) - `buffer`: Buffer polyfill needed for `web3.js` on React Native. #### Metaplex Polyfills @@ -210,19 +220,20 @@ For a Solana + Expo app, you'll need the following: If you want to use the Metaplex SDK, you'll need to add the Metaplex library plus a few additional polyfills: -- `@metaplex-foundation/umi` `@metaplex-foundation/umi-bundle-defaults` `@metaplex-foundation/mpl-core` - Metaplex Library +- `@metaplex-foundation/umi` `@metaplex-foundation/umi-bundle-defaults` + `@metaplex-foundation/mpl-core` - Metaplex Library - Several more polyfills - `assert` - `crypto-browserify` - `readable-stream` - `zlib` - - `react-native-url-polyfill` -All of the libraries that the above polyfills are meant to replace are utilized -by the Metaplex libraries in the background. It's unlikely you'll be importing any -of them into your code directly. Because of this, you'll need to register the -polyfills using a `metro.config.js` file. This will ensure that Metaplex uses -the polyfills instead of the usual Node.js libraries that aren't supported in -React Native. Below is an example `metro.config.js` file: + - `react-native-url-polyfill` All of the libraries that the above polyfills + are meant to replace are utilized by the Metaplex libraries in the + background. It's unlikely you'll be importing any of them into your code + directly. Because of this, you'll need to register the polyfills using a + `metro.config.js` file. This will ensure that Metaplex uses the polyfills + instead of the usual Node.js libraries that aren't supported in React + Native. Below is an example `metro.config.js` file: ```js // Import the default Expo Metro config @@ -238,7 +249,7 @@ defaultConfig.resolver.extraNodeModules = { url: require.resolve("react-native-url-polyfill"), zlib: require.resolve("browserify-zlib"), path: require.resolve("path-browserify"), - crypto : require.resolve('expo-crypto') + crypto: require.resolve("expo-crypto"), }; // Export the modified configuration @@ -350,8 +361,8 @@ Copy and paste the following into the newly created `eas.json`: #### 4. Build and emulate -Now let's build the project locally. You will choose `y` for every answer. This will -take a while to complete. +Now let's build the project locally. You will choose `y` for every answer. This +will take a while to complete. ```bash npx eas build --profile development --platform android --local @@ -386,8 +397,8 @@ already have a Devnet-enabled wallet installed you can skip step 0. You'll need a wallet that supports Devnet to test with. In [our Mobile Wallet Adapter lesson](/content/courses/mobile/mwa-deep-dive) we -created one of these. Let's install it from the repo in a different -directory from our app: +created one of these. Let's install it from the repo in a different directory +from our app: ```bash cd .. @@ -479,7 +490,6 @@ const webCrypto = typeof crypto !== "undefined" ? crypto : new Crypto(); configurable: true, enumerable: true, get: () => webCrypto, - }); } })(); @@ -492,8 +502,8 @@ just created: import { ConnectionProvider } from "./components/ConnectionProvider"; import { AuthorizationProvider } from "./components/AuthProvider"; import { clusterApiUrl } from "@solana/web3.js"; -import { MainScreen } from "./screens/MainScreen" -import "./polyfills" +import { MainScreen } from "./screens/MainScreen"; +import "./polyfills"; export default function App() { const cluster = "devnet"; @@ -513,9 +523,8 @@ export default function App() { } ``` -Notice we've added two polyfills above: `buffer` and -`expo-crypto`. These are necessary for the Solana -dependencies to run correctly. +Notice we've added two polyfills above: `buffer` and `expo-crypto`. These are +necessary for the Solana dependencies to run correctly. #### 4. Build and run Solana boilerplate @@ -608,7 +617,7 @@ defaultConfig.resolver.extraNodeModules = { url: require.resolve("react-native-url-polyfill"), zlib: require.resolve("browserify-zlib"), path: require.resolve("path-browserify"), - crypto : require.resolve('expo-crypto'), + crypto: require.resolve("expo-crypto"), }; // Export the modified configuration @@ -619,8 +628,7 @@ module.exports = { ...defaultConfig.resolver, unstable_enablePackageExports: true, }, -} - +}; ``` #### 3. Metaplex provider @@ -633,15 +641,20 @@ an `IdentitySigner` for the `Metaplex` object to use. This allows it to call several privileged functions on our behalf: ```tsx -import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; -import { mplCandyMachine } from '@metaplex-foundation/mpl-candy-machine'; -import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'; -import { transact, Web3MobileWallet } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; +import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; +import { mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine"; +import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; +import { + transact, + Web3MobileWallet, +} from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; import { useMemo } from "react"; import { Account } from "./AuthProvider"; -type Web3JsTransactionOrVersionedTransaction = Transaction | VersionedTransaction; +type Web3JsTransactionOrVersionedTransaction = + | Transaction + | VersionedTransaction; export const useMetaplex = ( connection: Connection, @@ -665,7 +678,11 @@ export const useMetaplex = ( return signedMessages[0]; }); }, - signTransaction: async (transaction: T): Promise => { + signTransaction: async < + T extends Web3JsTransactionOrVersionedTransaction, + >( + transaction: T, + ): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -674,7 +691,11 @@ export const useMetaplex = ( return signedTransactions[0] as T; }); }, - signAllTransactions: async (transactions: T[]): Promise => { + signAllTransactions: async < + T extends Web3JsTransactionOrVersionedTransaction, + >( + transactions: T[], + ): Promise => { return transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -755,8 +776,8 @@ Now, let's wrap our new `NFTProvider` around `MainScreen` in `App.tsx`: import { ConnectionProvider } from "./components/ConnectionProvider"; import { AuthorizationProvider } from "./components/AuthProvider"; import { clusterApiUrl } from "@solana/web3.js"; -import { MainScreen } from "./screens/MainScreen" -import "./polyfills" +import { MainScreen } from "./screens/MainScreen"; +import "./polyfills"; export default function App() { const cluster = "devnet"; diff --git a/content/courses/onchain-development/anchor-pdas.md b/content/courses/onchain-development/anchor-pdas.md index da2475756..a20e1e8ed 100644 --- a/content/courses/onchain-development/anchor-pdas.md +++ b/content/courses/onchain-development/anchor-pdas.md @@ -5,7 +5,7 @@ objectives: - Enable and use the `init_if_needed` constraint - Use the `realloc` constraint to reallocate space on an existing account - Use the `close` constraint to close an existing account -description: +description: "Store arbitrary data on Solana, using PDAs, an inbuilt key-value store." --- From b712823f28d77c5bd1f0228813d048597c8ece25 Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 12 Sep 2024 07:48:29 +0700 Subject: [PATCH 03/15] Update the type Nft We don't use the @metaplex-foundation/js any more so we need to update the type NFT --- .../mobile/solana-mobile-dapps-with-expo.md | 118 ++++++++++++++---- 1 file changed, 92 insertions(+), 26 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 4bdd002b5..1feec3b02 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -603,7 +603,7 @@ touch metro.config.js Copy and paste the following into `metro.config.js`: -```js +```javascript // Import the default Expo Metro config const { getDefaultConfig } = require("@expo/metro-config"); @@ -641,29 +641,25 @@ an `IdentitySigner` for the `Metaplex` object to use. This allows it to call several privileged functions on our behalf: ```tsx -import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; -import { mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine"; -import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; -import { - transact, - Web3MobileWallet, -} from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplCandyMachine } from '@metaplex-foundation/mpl-candy-machine'; +import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'; +import { transact, Web3MobileWallet } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; import { useMemo } from "react"; import { Account } from "./AuthProvider"; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; -type Web3JsTransactionOrVersionedTransaction = - | Transaction - | VersionedTransaction; +type LegacyOrVersionedTransact = Transaction | VersionedTransaction; -export const useMetaplex = ( +export const useUmi = ( connection: Connection, selectedAccount: Account | null, authorizeSession: (wallet: Web3MobileWallet) => Promise, ) => { return useMemo(() => { if (!selectedAccount || !authorizeSession) { - return { umi: null }; + return { mobileWalletAdapter: null, umi: null }; } const mobileWalletAdapter = { @@ -678,11 +674,7 @@ export const useMetaplex = ( return signedMessages[0]; }); }, - signTransaction: async < - T extends Web3JsTransactionOrVersionedTransaction, - >( - transaction: T, - ): Promise => { + signTransaction: async (transaction: T): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -691,11 +683,7 @@ export const useMetaplex = ( return signedTransactions[0] as T; }); }, - signAllTransactions: async < - T extends Web3JsTransactionOrVersionedTransaction, - >( - transactions: T[], - ): Promise => { + signAllTransactions: async (transactions: T[]): Promise => { return transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -708,6 +696,7 @@ export const useMetaplex = ( const umi = createUmi(connection.rpcEndpoint) .use(mplCandyMachine()) + .use(mplTokenMetadata()) .use(walletAdapterIdentity(mobileWalletAdapter)); return { umi }; @@ -720,7 +709,7 @@ export const useMetaplex = ( We're also making a higher-level NFT provider that helps with NFT state management. It combines all three of our previous providers: `ConnectionProvider`, `AuthProvider`, and `MetaplexProvider` to allow us to -create our `Metaplex` object. We will fill this out at a later step; for now, it +create our `Umi` object. We will fill this out at a later step; for now, it makes for a good boilerplate. Let's create the new file `components/NFTProvider.tsx`: @@ -954,15 +943,91 @@ This should have the following fields: `fetch` and `create` - `publicKey: PublicKey | null` - The NFT creator's public key - `isLoading: boolean` - Manages loading state -- `loadedNFTs: (Nft | Sft | SftWithToken | NftWithToken)[] | null` - An array of +- `loadedNFTs: (Nft)[] | null` - An array of the user's snapshot NFTs -- `nftOfTheDay: (Nft | Sft | SftWithToken | NftWithToken) | null` - A reference +- `nftOfTheDay: (Nft) | null` - A reference to the NFT created today - `connect: () => void` - A function for connecting to the Devnet-enabled wallet - `fetchNFTs: () => void` - A function that fetches the user's snapshot NFTs - `createNFT: (name: string, description: string, fileUri: string) => void` - A function that creates a new snapshot NFT +We can define the ```Nft``` type as follow and put it inside a file called ```types.ts``` +```typescript +import { PublicKey } from '@metaplex-foundation/umi'; +import { + Metadata, + TokenStandard, + CollectionDetails, + UseMethod, + Creator, + Collection, + Uses, +} from '@metaplex-foundation/mpl-token-metadata'; +import { Mint } from '@metaplex-foundation/mpl-toolbox'; + +type NftEdition = { + isOriginal: boolean; + largestMintedEdition?: bigint; + printEditionMint?: PublicKey; + printEditionNum?: bigint; +}; + +export type Nft = Omit & { + /** A model identifier to distinguish models in the SDK. */ + readonly model: 'nft'; + + /** The mint address of the NFT. */ + readonly address: PublicKey; + + /** The metadata address of the NFT. */ + readonly metadataAddress: PublicKey; + + /** The mint account of the NFT. */ + readonly mint: Mint; + + /** + * Defines whether the NFT is an original edition or a + * printed edition and provides additional information accordingly. + */ + readonly edition: NftEdition; + + /** The update authority of the NFT. */ + readonly updateAuthority: PublicKey; + + /** The JSON URI of the NFT. */ + readonly uri: string; + + /** The name of the NFT. */ + readonly name: string; + + /** The symbol of the NFT. */ + readonly symbol: string; + + /** The token standard of the NFT. */ + readonly tokenStandard: TokenStandard; + + /** The collection details of the NFT, if any. */ + readonly collectionDetails: CollectionDetails | null; + + /** The use method of the NFT, if any. */ + readonly useMethod: UseMethod | null; + + /** The creators of the NFT. */ + readonly creators: Creator[]; + + /** The collection the NFT belongs to, if any. */ + readonly collection: Collection | null; + + /** The uses of the NFT, if any. */ + readonly uses: Uses | null; + + /** Whether the NFT is mutable. */ + readonly isMutable: boolean; +}; +``` + + ```tsx export interface NFTContextState { metaplex: Metaplex | null; // Holds the metaplex object that we use to call `fetch` and `create` on. @@ -1143,6 +1208,7 @@ import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol"; import { Account, useAuthorization } from "./AuthProvider"; import RNFetchBlob from "rn-fetch-blob"; import { useMetaplex } from "./MetaplexProvider"; +import { Nft } from "../types"; export interface NFTProviderProps { children: ReactNode; From 94825155caa2b089a522d03f9ab15a0afded52df Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 12 Sep 2024 07:52:00 +0700 Subject: [PATCH 04/15] Update solana-mobile-dapps-with-expo.md Fix README format --- .../mobile/solana-mobile-dapps-with-expo.md | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 1feec3b02..490c646f5 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -641,14 +641,17 @@ an `IdentitySigner` for the `Metaplex` object to use. This allows it to call several privileged functions on our behalf: ```tsx -import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; -import { mplCandyMachine } from '@metaplex-foundation/mpl-candy-machine'; -import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'; -import { transact, Web3MobileWallet } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; +import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; +import { mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine"; +import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; +import { + transact, + Web3MobileWallet, +} from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; import { useMemo } from "react"; import { Account } from "./AuthProvider"; -import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; type LegacyOrVersionedTransact = Transaction | VersionedTransaction; @@ -674,7 +677,9 @@ export const useUmi = ( return signedMessages[0]; }); }, - signTransaction: async (transaction: T): Promise => { + signTransaction: async ( + transaction: T, + ): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -683,7 +688,9 @@ export const useUmi = ( return signedTransactions[0] as T; }); }, - signAllTransactions: async (transactions: T[]): Promise => { + signAllTransactions: async ( + transactions: T[], + ): Promise => { return transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); const signedTransactions = await wallet.signTransactions({ @@ -943,18 +950,18 @@ This should have the following fields: `fetch` and `create` - `publicKey: PublicKey | null` - The NFT creator's public key - `isLoading: boolean` - Manages loading state -- `loadedNFTs: (Nft)[] | null` - An array of - the user's snapshot NFTs -- `nftOfTheDay: (Nft) | null` - A reference - to the NFT created today +- `loadedNFTs: (Nft)[] | null` - An array of the user's snapshot NFTs +- `nftOfTheDay: (Nft) | null` - A reference to the NFT created today - `connect: () => void` - A function for connecting to the Devnet-enabled wallet - `fetchNFTs: () => void` - A function that fetches the user's snapshot NFTs - `createNFT: (name: string, description: string, fileUri: string) => void` - A function that creates a new snapshot NFT -We can define the ```Nft``` type as follow and put it inside a file called ```types.ts``` +We can define the `Nft` type as follow and put it inside a file called +`types.ts` + ```typescript -import { PublicKey } from '@metaplex-foundation/umi'; +import { PublicKey } from "@metaplex-foundation/umi"; import { Metadata, TokenStandard, @@ -963,8 +970,8 @@ import { Creator, Collection, Uses, -} from '@metaplex-foundation/mpl-token-metadata'; -import { Mint } from '@metaplex-foundation/mpl-toolbox'; +} from "@metaplex-foundation/mpl-token-metadata"; +import { Mint } from "@metaplex-foundation/mpl-toolbox"; type NftEdition = { isOriginal: boolean; @@ -973,9 +980,9 @@ type NftEdition = { printEditionNum?: bigint; }; -export type Nft = Omit & { +export type Nft = Omit & { /** A model identifier to distinguish models in the SDK. */ - readonly model: 'nft'; + readonly model: "nft"; /** The mint address of the NFT. */ readonly address: PublicKey; @@ -986,7 +993,7 @@ export type Nft = Omit & { /** The mint account of the NFT. */ readonly mint: Mint; - /** + /** * Defines whether the NFT is an original edition or a * printed edition and provides additional information accordingly. */ @@ -1027,7 +1034,6 @@ export type Nft = Omit & { }; ``` - ```tsx export interface NFTContextState { metaplex: Metaplex | null; // Holds the metaplex object that we use to call `fetch` and `create` on. From 9f9f449395599adb7ea8f21bf433b7de2cddabcf Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 12 Sep 2024 15:16:47 +0700 Subject: [PATCH 05/15] Migrate to Pinata Cloud instead of NFT Storage because they stopped their classic service since June 2024 --- .../mobile/solana-mobile-dapps-with-expo.md | 150 +++++++++++------- 1 file changed, 91 insertions(+), 59 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 490c646f5..5ba4c2360 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -11,10 +11,10 @@ description: "How to use Solana in your Expo apps." - Expo is an open-source collection of tools and libraries that wrap around React Native, much like Next.js is a framework built on top of React. -- In addition to simplifying the build/deploy process, Expo provides packages - that give you access to mobile devices' peripherals and capabilities. -- A lot of Solana ecosystem libraries don't support React native out of the box, - but you can typically use them with the right +- Along with simplifying the build and deploy process, Expo offers packages that + allow access to mobile device peripherals and capabilities. +- Many Solana ecosystem libraries don't natively support React Native, but you + can often use them with the appropriate [polyfills](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill). ## Lesson @@ -47,15 +47,15 @@ Expo consists of three main parts: 2. The Expo Go App 3. A suite of libraries that grant access to various mobile device capabilities. -The Expo CLI is a build and debugging tool that helps make all of the magic -happen. Chances are, you'll only have to interact with it when you're building -or starting a development server. It just works. +The Expo CLI is a powerful tool for building and debugging that simplifies the +development process. Chances are, you'll only have to interact with it when +you're building or starting a development server. It just works. The [Expo Go App](https://expo.dev/client) is a really cool piece of tech that allows _most_ apps to be developed without using an emulator or physical device. You download the app, you scan the QR from the build output and then you have a -working dev environment right on your phone. Unfortunately, this will not work -with the Solana mobile SDK. Coming from the +working dev environment right on your phone. However, this doesn't work with the +Solana Mobile SDK. Coming from the [Solana Expo setup article](https://docs.solanamobile.com/react-native/expo): > The traditional Expo Go development flow is only limited to certain @@ -270,8 +270,8 @@ Let's practice this together by building the Mint-A-Day app, where users will able to mint a single NFT snapshot of their lives daily, creating a permanent diary of sorts. -To mint the NFTs we'll be using Metaplex's Javascript SDK along with -[nft.storage](https://nft.storage/) to store images and metadata. All of our +To mint the NFTs we'll be using Metaplex's Umi libraries along with +[Pinata Cloud](https://pinata.cloud/) to store images and metadata. All of our onchain work will be on Devnet. The first half of this lab is cobbling together the needed components to make @@ -469,7 +469,8 @@ export function MainScreen() { } ``` -Next, create `polyfills.ts` for react-native to work with all solana +Next, create file called `polyfills.ts` for react-native to work with all solana +dependencies ```typescript import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto"; @@ -523,24 +524,25 @@ export default function App() { } ``` -Notice we've added two polyfills above: `buffer` and `expo-crypto`. These are -necessary for the Solana dependencies to run correctly. +Notice we've added the polyfills file `polyfills.ts`. These are necessary for +the Solana dependencies to run correctly. #### 4. Build and run Solana boilerplate -Add these run script to your package.json +Add the following convenient run scripts to your `package.json` file. ```json "scripts": { - "start": "expo start", + "start": "expo start --dev-client", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "build": "npx eas build --profile development --platform android", "build:local": "npx eas build --profile development --platform android --local", - "test": "echo \"No tests specified\" && exit 0" - }, - + "build:local:ios": "npx eas build --profile development --platform ios --local", + "test": "echo \"No tests specified\" && exit 0", + "clean": "rm -rf node_modules && npm install" + } ``` Let's make sure everything is working and compiling correctly. In Expo, anytime @@ -574,9 +576,9 @@ you can reference. #### 1. Install Metaplex dependencies -The Metaplex SDK abstracts away a lot of the minutia of working with NFTs, -however it was written largely for Node.js, so we'll need several more polyfills -to make it work: +[Metaplex programs and tools](https://developers.metaplex.com/programs-and-tools) +abstracts away a lot of the minutia of working with NFTs, however it was written +largely for Node.js, so we'll need several more polyfills to make it work: ```bash npm install assert \ @@ -633,12 +635,16 @@ module.exports = { #### 3. Metaplex provider -We're going to create a Metaplex provider file that will help us access a -`Metaplex` object. This `Metaplex` object is what gives us access to all of the -functions we'll need like `fetch` and `create`. To do this we create a new file -`/components/MetaplexProvider.tsx`. Here we pipe our mobile wallet adapter into -an `IdentitySigner` for the `Metaplex` object to use. This allows it to call -several privileged functions on our behalf: +We're going to create a Metaplex provider file that will help us access an `Umi` +object (Read more about `umi` at +[Umi docs](https://developers.metaplex.com/umi)).This `Umi` object, combined +with other libraries such as `@metaplex-foundation/umi-bundle-defaults`, +`@metaplex-foundation/mpl-token-metadata`and +`@metaplex-foundation/mpl-candy-machine`, will give us access to all the +functions we'll need later, like `fetch` and `create`.. To do this we create a +new file `/components/MetaplexProvider.tsx`. Here we pipe our mobile wallet +adapter into the `Umi` object to use. This allows it to call several privileged +functions on our behalf: ```tsx import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; @@ -653,20 +659,27 @@ import { useMemo } from "react"; import { Account } from "./AuthProvider"; import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; +// Type definition for transactions that can be either legacy or versioned type LegacyOrVersionedTransact = Transaction | VersionedTransaction; +// Custom hook to create and configure a Umi instance export const useUmi = ( connection: Connection, selectedAccount: Account | null, authorizeSession: (wallet: Web3MobileWallet) => Promise, ) => { return useMemo(() => { + // If there's no selected account or authorize session function, return null values if (!selectedAccount || !authorizeSession) { return { mobileWalletAdapter: null, umi: null }; } + // Create a mobile wallet adapter object with necessary methods const mobileWalletAdapter = { + // Public key of the selected account publicKey: selectedAccount.publicKey, + + // Method to sign a message signMessage: async (message: Uint8Array): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); @@ -677,6 +690,8 @@ export const useUmi = ( return signedMessages[0]; }); }, + + // Method to sign a single transaction signTransaction: async ( transaction: T, ): Promise => { @@ -688,6 +703,8 @@ export const useUmi = ( return signedTransactions[0] as T; }); }, + + // Method to sign multiple transactions signAllTransactions: async ( transactions: T[], ): Promise => { @@ -701,10 +718,11 @@ export const useUmi = ( }, }; + // Create and configure the Umi instance const umi = createUmi(connection.rpcEndpoint) - .use(mplCandyMachine()) - .use(mplTokenMetadata()) - .use(walletAdapterIdentity(mobileWalletAdapter)); + .use(mplCandyMachine()) // Add Candy Machine plugin + .use(mplTokenMetadata()) // Add Token Metadata plugin + .use(walletAdapterIdentity(mobileWalletAdapter)); // Set wallet adapter return { umi }; }, [authorizeSession, selectedAccount, connection]); @@ -726,7 +744,7 @@ import "react-native-url-polyfill/auto"; import { useConnection } from "./ConnectionProvider"; import { Account, useAuthorization } from "./AuthProvider"; import React, { ReactNode, createContext, useContext, useState } from "react"; -import { useUmi } from "./MetaplexProvider"; // Update this import to match your file structure +import { useUmi } from "./MetaplexProvider"; import { Umi } from "@metaplex-foundation/umi"; export interface NFTProviderProps { @@ -818,12 +836,12 @@ npx expo start --dev-client --android Everything we've done to this point is effectively boilerplate. We need to add the functionality we intend for our Mint-A-Day app to have. Mint-A-day is a -daily snapshot app. It lets users take a snapshot of their life daily in the +daily snapshot app. It allows users take a snapshot of their life daily in the form of minting an NFT. The app will need access to the device's camera and a place to remotely store the captured images. Fortunately, Expo SDK can provide access to the camera and -[NFT.Storage](https://nft.storage) can store your NFT files for free. +[Pinata Cloud](https://pinata.cloud/) can store your NFT files safely. #### 1. Camera setup @@ -855,31 +873,42 @@ as a plugin in `app.json`: } ``` -This particular dependency makes it super simple to use the camera. To allow the -user to take a picture and return the image all you have to do is call the -following: +This dependency makes it incredibly easy to use the camera. To allow the user to +take a picture and return the image, simply call the following: ```tsx +// Launch the camera to take a picture using ImagePicker const result = await ImagePicker.launchCameraAsync({ + // Restrict media types to images only (no videos) mediaTypes: ImagePicker.MediaTypeOptions.Images, + + // Allow the user to edit/crop the image after taking it allowsEditing: true, + + // Specify the aspect ratio of the cropping frame (1:1 for a square) aspect: [1, 1], + + // Set the image quality to maximum (1.0 = highest quality, 0.0 = lowest) quality: 1, }); + +// 'result' will contain information about the captured image +// If the user cancels, result.cancelled will be true, otherwise it will contain the image URI ``` No need to add this anywhere yet - we'll get to it in a few steps. -#### 2. NFT.Storage setup +#### 2. Pinata Cloud setup The last thing we need to do is set up our access to -[nft.storage](https://nft.storage). We'll need to get an API key and add it as -an environment variable, then we need to add one last dependency to convert our -images into a file type we can upload. +[Pinata Cloud](https://pinata.cloud/). We'll need to get an API key and add it +as an environment variable, then we need to add one last dependency to convert +our images into a file type we can upload. -We'll be using NFT.storage to host our NFTs with IPFS since they do this for -free. [Sign up, and create an API key](https://nft.storage/manage/). Keep this -API key private. +We'll be using Pinata Cloud to host our NFTs with IPFS since they do this for +free. +[Sign up, and create an API key](https://app.pinata.cloud/developers/api-keys). +Keep this API key private. Best practices suggest keeping API keys in a `.env` file with `.env` added to your `.gitignore`. It's also a good idea to create a `.env.example` file that @@ -889,13 +918,16 @@ for the project. Create both files, in the root of your directory and add `.env` to your `.gitignore` file. -Then, add your API key to the `.env` file with the name -`EXPO_PUBLIC_NFT_STORAGE_API`. Now you'll be able to access your API key safely -in the application. +Next, add your API key to the `.env` file with the variable name +`EXPO_PUBLIC_PINATA_API`. This allows you to securely access your API key in the +application using `process.env.EXPO_PUBLIC_PINATA_API`, unlike traditional +`import "dotenv/config"` which may require additional polyfills when working +with Expo. For more information on securely storing secrets, refer to the +[Expo documentation on environment variables](https://docs.expo.dev/build-reference/variables/#importing-secrets-from-a-dotenv-file) Lastly, install `rn-fetch-blob`. This package will help us grab images from the device's URI scheme and turn them into Blobs we can the upload to -[NFT.storage](https://nft.storage). +[Pinata Cloud](https://pinata.cloud/). Install it with the following: @@ -936,8 +968,8 @@ The app itself is relatively straightforward. The general flow is: 1. The user connects (authorizes) using the `transact` function and by calling `authorizeSession` inside the callback -2. Our code then uses the `Metaplex` object to fetch all of the NFTs created by - the user +2. Our code then uses the `Umi` object to fetch all of the NFTs created by the + user 3. If an NFT has not been created for the current day, allow the user to take a picture, upload it, and mint it as an NFT @@ -957,7 +989,7 @@ This should have the following fields: - `createNFT: (name: string, description: string, fileUri: string) => void` - A function that creates a new snapshot NFT -We can define the `Nft` type as follow and put it inside a file called +We need to define the `Nft` type as follow and put it inside a file called `types.ts` ```typescript @@ -1103,18 +1135,18 @@ through the code for each of them and then show you the entire file at the end: }; ``` -3. `createNFT` - This function will upload a file to NFT.Storage, and then use +3. `createNFT` - This function will upload a file to Pinata Cloud, and then use Metaplex to create and mint an NFT to your wallet. This comes in three parts, uploading the image, uploading the metadata and then minting the NFT. - To upload to NFT.Storage you just make a POST with your API key and the + To upload to Pinata Cloud you just make a POST with your API key and the image/metadata as the body. We'll create two helper functions for uploading the image and metadata separately, then tie them together into a single `createNFT` function: ```tsx - // https://nft.storage/api-docs/ + // https://docs.pinata.cloud/ const uploadImage = async (fileUri: string): Promise => { const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( fileUri, @@ -1125,7 +1157,7 @@ through the code for each of them and then show you the entire file at the end: const response = await fetch("https://api.nft.storage/upload", { method: "POST", headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_STORAGE_API}`, + Authorization: `Bearer ${process.env.EXPO_PUBLIC_PINANTA_API}`, "Content-Type": "image/jpg", }, body: bytes, @@ -1145,7 +1177,7 @@ through the code for each of them and then show you the entire file at the end: const response = await fetch("https://api.nft.storage/upload", { method: "POST", headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_STORAGE_API}`, + Authorization: `Bearer ${process.env.EXPO_PUBLIC_PINATA_API}`, }, body: JSON.stringify({ name, @@ -1307,7 +1339,7 @@ export function NFTProvider(props: NFTProviderProps) { } }; - // https://nft.storage/api-docs/ + // https://docs.pinata.cloud/ const uploadImage = async (fileUri: string): Promise => { const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( fileUri, @@ -1318,7 +1350,7 @@ export function NFTProvider(props: NFTProviderProps) { const response = await fetch("https://api.nft.storage/upload", { method: "POST", headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_STORAGE_API}`, + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINANTA_API}`, "Content-Type": "image/jpg", }, body: bytes, @@ -1338,7 +1370,7 @@ export function NFTProvider(props: NFTProviderProps) { const response = await fetch("https://api.nft.storage/upload", { method: "POST", headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_STORAGE_API}`, + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_API}`, }, body: JSON.stringify({ name, From c9fe8e8445dedf231fa4dd87334389406ce38530 Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 12 Sep 2024 17:31:46 +0700 Subject: [PATCH 06/15] Update to Pinata Upload Image and Upload Data --- .../mobile/solana-mobile-dapps-with-expo.md | 158 +++++++----------- 1 file changed, 65 insertions(+), 93 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 5ba4c2360..c618d8a79 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -905,8 +905,8 @@ The last thing we need to do is set up our access to as an environment variable, then we need to add one last dependency to convert our images into a file type we can upload. -We'll be using Pinata Cloud to host our NFTs with IPFS since they do this for -free. +We'll be using Pinata Cloud to host our NFTs with IPFS since they do this for a +very cheap price compare to other solutions such as Akord,... [Sign up, and create an API key](https://app.pinata.cloud/developers/api-keys). Keep this API key private. @@ -1099,7 +1099,8 @@ through the code for each of them and then show you the entire file at the end: }; ``` -2. `fetchNFTs` - This function will fetch the NFTs using Metaplex: +2. `fetchNFTs` - This function will fetch the NFTs using + `fetchAllDigitalAssetByCreator`: ```tsx const fetchNFTs = async () => { @@ -1136,94 +1137,82 @@ through the code for each of them and then show you the entire file at the end: ``` 3. `createNFT` - This function will upload a file to Pinata Cloud, and then use - Metaplex to create and mint an NFT to your wallet. This comes in three parts, - uploading the image, uploading the metadata and then minting the NFT. + `createNft` function from to create and mint an NFT to your wallet. This + comes in three parts, uploading the image, uploading the metadata and then + minting the NFT. - To upload to Pinata Cloud you just make a POST with your API key and the - image/metadata as the body. + To upload to Pinata Cloud you need to init `PinataSDK` instance with + `pinataJwt` and `pinataGateway`. After that, you can interact with + their[API](https://docs.pinata.cloud/web3/sdk/getting-started) We'll create two helper functions for uploading the image and metadata separately, then tie them together into a single `createNFT` function: ```tsx - // https://docs.pinata.cloud/ + const pinata = new PinataSDK({ + pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, + pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, + }); const uploadImage = async (fileUri: string): Promise => { const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( fileUri, "base64", ); - const bytes = Buffer.from(imageBytesInBase64, "base64"); - - const response = await fetch("https://api.nft.storage/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_PINANTA_API}`, - "Content-Type": "image/jpg", - }, - body: bytes, - }); - const data = await response.json(); - const cid = data.value.cid; + // pinata.upload.base64 is used to send the Base64-encoded image to Pinata Cloud + // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/base64#base64 + const upload = await pinata.upload.base64(imageBytesInBase64); - return cid as string; + // Return the IPFS hash of the uploaded image (IPFS is a decentralized file storage system) + return upload.IpfsHash; }; - const uploadMetadata = async ( name: string, description: string, imageCID: string, ): Promise => { - const response = await fetch("https://api.nft.storage/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_PINATA_API}`, - }, - body: JSON.stringify({ - name, - description, - image: `https://ipfs.io/ipfs/${imageCID}`, - }), + // pinata.upload.json is used to send the JSON to Pinata Cloud + // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/json + const upload = await pinata.upload.json({ + name: name, + description: description, + imageCID: imageCID, }); - - const data = await response.json(); - const cid = data.value.cid; - - return cid; + return upload.IpfsHash; }; ``` - Minting the NFT after the image and metadata have been uploaded is as simple - as calling `metaplex.nfts().create(...)`. Below shows the `createNFT` - function tying everything together: +Minting the NFT after the image and metadata have been uploaded is as simple as +calling `metaplex.nfts().create(...)`. Below shows the `createNFT` function +tying everything together: - ```tsx - const createNFT = async ( - name: string, - description: string, - fileUri: string, - ) => { - if (!metaplex || !account || isLoading) return; +```tsx +const createNFT = async ( + name: string, + description: string, + fileUri: string, +) => { + if (!metaplex || !account || isLoading) return; - setIsLoading(true); - try { - const imageCID = await uploadImage(fileUri); - const metadataCID = await uploadMetadata(name, description, imageCID); + setIsLoading(true); + try { + const imageCID = await uploadImage(fileUri); + const metadataCID = await uploadMetadata(name, description, imageCID); - const nft = await metaplex.nfts().create({ - uri: `https://ipfs.io/ipfs/${metadataCID}`, - name: name, - sellerFeeBasisPoints: 0, - }); + const nft = await metaplex.nfts().create({ + uri: `https://ipfs.io/ipfs/${metadataCID}`, + name: name, + sellerFeeBasisPoints: 0, + }); - setNftOfTheDay(nft.nft); - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - ``` + setNftOfTheDay(nft.nft); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } +}; +``` We'll put all of the above into the `NFTProvider.tsx` file. All together, this looks as follows: @@ -1339,27 +1328,18 @@ export function NFTProvider(props: NFTProviderProps) { } }; - // https://docs.pinata.cloud/ const uploadImage = async (fileUri: string): Promise => { const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( fileUri, "base64", ); - const bytes = Buffer.from(imageBytesInBase64, "base64"); - - const response = await fetch("https://api.nft.storage/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINANTA_API}`, - "Content-Type": "image/jpg", - }, - body: bytes, - }); - const data = await response.json(); - const cid = data.value.cid; + // pinata.upload.base64 is used to send the Base64-encoded image to Pinata Cloud + // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/base64#base64 + const upload = await pinata.upload.base64(imageBytesInBase64); - return cid as string; + // Return the IPFS hash of the uploaded image (IPFS is a decentralized file storage system) + return upload.IpfsHash; }; const uploadMetadata = async ( @@ -1367,22 +1347,14 @@ export function NFTProvider(props: NFTProviderProps) { description: string, imageCID: string, ): Promise => { - const response = await fetch("https://api.nft.storage/upload", { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_API}`, - }, - body: JSON.stringify({ - name, - description, - image: `https://ipfs.io/ipfs/${imageCID}`, - }), + // pinata.upload.json is used to send the JSON to Pinata Cloud + // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/json + const upload = await pinata.upload.json({ + name: name, + description: description, + imageCID: imageCID, }); - - const data = await response.json(); - const cid = data.value.cid; - - return cid; + return upload.IpfsHash; }; const createNFT = async ( From ac479db222744aaa4a2503480565e1e513bb3a8b Mon Sep 17 00:00:00 2001 From: XuananLe Date: Fri, 13 Sep 2024 00:50:11 +0700 Subject: [PATCH 07/15] Formatting + Remove unnecessary deps --- .../mobile/solana-mobile-dapps-with-expo.md | 620 ++++++++---------- 1 file changed, 278 insertions(+), 342 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index c618d8a79..ddd24248e 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -539,7 +539,6 @@ Add the following convenient run scripts to your `package.json` file. "web": "expo start --web", "build": "npx eas build --profile development --platform android", "build:local": "npx eas build --profile development --platform android --local", - "build:local:ios": "npx eas build --profile development --platform ios --local", "test": "echo \"No tests specified\" && exit 0", "clean": "rm -rf node_modules && npm install" } @@ -591,7 +590,9 @@ npm install assert \ react-native-url-polyfill \ @metaplex-foundation/umi \ @metaplex-foundation/umi-bundle-defaults \ - @metaplex-foundation/mpl-candy-machine + @metaplex-foundation/umi-bundle-defaults \ + @metaplex-foundation/umi-signer-wallet-adapters \ + @metaplex-foundation/umi-web3js-adapters \ ``` #### 2. Polyfill config @@ -648,7 +649,7 @@ functions on our behalf: ```tsx import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; -import { mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine"; +import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; import { transact, @@ -657,29 +658,21 @@ import { import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; import { useMemo } from "react"; import { Account } from "./AuthProvider"; -import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; -// Type definition for transactions that can be either legacy or versioned type LegacyOrVersionedTransact = Transaction | VersionedTransaction; -// Custom hook to create and configure a Umi instance export const useUmi = ( connection: Connection, selectedAccount: Account | null, authorizeSession: (wallet: Web3MobileWallet) => Promise, ) => { return useMemo(() => { - // If there's no selected account or authorize session function, return null values if (!selectedAccount || !authorizeSession) { return { mobileWalletAdapter: null, umi: null }; } - // Create a mobile wallet adapter object with necessary methods const mobileWalletAdapter = { - // Public key of the selected account publicKey: selectedAccount.publicKey, - - // Method to sign a message signMessage: async (message: Uint8Array): Promise => { return await transact(async (wallet: Web3MobileWallet) => { await authorizeSession(wallet); @@ -690,8 +683,6 @@ export const useUmi = ( return signedMessages[0]; }); }, - - // Method to sign a single transaction signTransaction: async ( transaction: T, ): Promise => { @@ -703,8 +694,6 @@ export const useUmi = ( return signedTransactions[0] as T; }); }, - - // Method to sign multiple transactions signAllTransactions: async ( transactions: T[], ): Promise => { @@ -717,12 +706,10 @@ export const useUmi = ( }); }, }; - - // Create and configure the Umi instance + // Add Umi Plugins const umi = createUmi(connection.rpcEndpoint) - .use(mplCandyMachine()) // Add Candy Machine plugin - .use(mplTokenMetadata()) // Add Token Metadata plugin - .use(walletAdapterIdentity(mobileWalletAdapter)); // Set wallet adapter + .use(mplTokenMetadata()) + .use(walletAdapterIdentity(mobileWalletAdapter)); return { umi }; }, [authorizeSession, selectedAccount, connection]); @@ -982,97 +969,24 @@ This should have the following fields: `fetch` and `create` - `publicKey: PublicKey | null` - The NFT creator's public key - `isLoading: boolean` - Manages loading state -- `loadedNFTs: (Nft)[] | null` - An array of the user's snapshot NFTs -- `nftOfTheDay: (Nft) | null` - A reference to the NFT created today +- `loadedNFTs: (DigitalAsset)[] | null` - An array of the user's snapshot NFTs +- `nftOfTheDay: (DigitalAsset) | null` - A reference to the NFT created today - `connect: () => void` - A function for connecting to the Devnet-enabled wallet - `fetchNFTs: () => void` - A function that fetches the user's snapshot NFTs - `createNFT: (name: string, description: string, fileUri: string) => void` - A function that creates a new snapshot NFT -We need to define the `Nft` type as follow and put it inside a file called -`types.ts` - -```typescript -import { PublicKey } from "@metaplex-foundation/umi"; -import { - Metadata, - TokenStandard, - CollectionDetails, - UseMethod, - Creator, - Collection, - Uses, -} from "@metaplex-foundation/mpl-token-metadata"; -import { Mint } from "@metaplex-foundation/mpl-toolbox"; - -type NftEdition = { - isOriginal: boolean; - largestMintedEdition?: bigint; - printEditionMint?: PublicKey; - printEditionNum?: bigint; -}; - -export type Nft = Omit & { - /** A model identifier to distinguish models in the SDK. */ - readonly model: "nft"; - - /** The mint address of the NFT. */ - readonly address: PublicKey; - - /** The metadata address of the NFT. */ - readonly metadataAddress: PublicKey; - - /** The mint account of the NFT. */ - readonly mint: Mint; - - /** - * Defines whether the NFT is an original edition or a - * printed edition and provides additional information accordingly. - */ - readonly edition: NftEdition; - - /** The update authority of the NFT. */ - readonly updateAuthority: PublicKey; - - /** The JSON URI of the NFT. */ - readonly uri: string; - - /** The name of the NFT. */ - readonly name: string; - - /** The symbol of the NFT. */ - readonly symbol: string; - - /** The token standard of the NFT. */ - readonly tokenStandard: TokenStandard; - - /** The collection details of the NFT, if any. */ - readonly collectionDetails: CollectionDetails | null; - - /** The use method of the NFT, if any. */ - readonly useMethod: UseMethod | null; - - /** The creators of the NFT. */ - readonly creators: Creator[]; - - /** The collection the NFT belongs to, if any. */ - readonly collection: Collection | null; - - /** The uses of the NFT, if any. */ - readonly uses: Uses | null; - - /** Whether the NFT is mutable. */ - readonly isMutable: boolean; -}; -``` +The `DigitalAsset` type comes from `@metaplex-foundation/mpl-token-metadata` +that have metadata, off chain metadata, collection data, plugins (including +Attributes), and more ```tsx export interface NFTContextState { metaplex: Metaplex | null; // Holds the metaplex object that we use to call `fetch` and `create` on. publicKey: PublicKey | null; // The public key of the authorized wallet isLoading: boolean; // Loading state - loadedNFTs: (Nft | Sft | SftWithToken | NftWithToken)[] | null; // Array of loaded NFTs that contain metadata - nftOfTheDay: (Nft | Sft | SftWithToken | NftWithToken) | null; // The NFT snapshot created on the current day + loadedNFTs: DigitalAsset[] | null; // Array of loaded NFTs that contain metadata + nftOfTheDay: DigitalAsset | null; // The NFT snapshot created on the current day connect: () => void; // Connects (and authorizes) us to the Devnet-enabled wallet fetchNFTs: () => void; // Fetches the NFTs using the `metaplex` object createNFT: (name: string, description: string, fileUri: string) => void; // Creates the NFT @@ -1102,39 +1016,21 @@ through the code for each of them and then show you the entire file at the end: 2. `fetchNFTs` - This function will fetch the NFTs using `fetchAllDigitalAssetByCreator`: - ```tsx - const fetchNFTs = async () => { - if (!metaplex || !account || isLoading) return; - - setIsLoading(true); - - try { - const nfts = await metaplex.nfts().findAllByCreator({ - creator: account.publicKey, - }); - - const loadedNFTs = await Promise.all( - nfts.map(nft => { - return metaplex.nfts().load({ metadata: nft as Metadata }); - }), - ); - setLoadedNFTs(loadedNFTs); - - // Check if we already took a snapshot today - const nftOfTheDayIndex = loadedNFTs.findIndex(nft => { - return formatDate(new Date(Date.now())) === nft.name; - }); - - if (nftOfTheDayIndex !== -1) { - setNftOfTheDay(loadedNFTs[nftOfTheDayIndex]); - } - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - ``` +```tsx +const fetchNFTs = useCallback(async () => { + if (!umi || !account || isLoading) return; + setIsLoading(true); + try { + const creatorPublicKey = fromWeb3JsPublicKey(account.publicKey); + const nfts = await fetchAllDigitalAssetByCreator(umi, creatorPublicKey); + setLoadedNFTs(nfts); + } catch (error) { + console.error("Failed to fetch NFTs:", error); + } finally { + setIsLoading(false); + } +}, [umi, account, isLoading]); +``` 3. `createNFT` - This function will upload a file to Pinata Cloud, and then use `createNft` function from to create and mint an NFT to your wallet. This @@ -1142,76 +1038,74 @@ through the code for each of them and then show you the entire file at the end: minting the NFT. To upload to Pinata Cloud you need to init `PinataSDK` instance with - `pinataJwt` and `pinataGateway`. After that, you can interact with - their[API](https://docs.pinata.cloud/web3/sdk/getting-started) + `pinataJwt` and `pinataGateway`. After that, you can interact with their + [API](https://docs.pinata.cloud/web3/sdk/getting-started) We'll create two helper functions for uploading the image and metadata separately, then tie them together into a single `createNFT` function: - ```tsx - const pinata = new PinataSDK({ - pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, - pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, - }); - const uploadImage = async (fileUri: string): Promise => { - const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( - fileUri, - "base64", - ); - - // pinata.upload.base64 is used to send the Base64-encoded image to Pinata Cloud - // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/base64#base64 - const upload = await pinata.upload.base64(imageBytesInBase64); - - // Return the IPFS hash of the uploaded image (IPFS is a decentralized file storage system) - return upload.IpfsHash; - }; - const uploadMetadata = async ( - name: string, - description: string, - imageCID: string, - ): Promise => { - // pinata.upload.json is used to send the JSON to Pinata Cloud - // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/json - const upload = await pinata.upload.json({ - name: name, - description: description, - imageCID: imageCID, - }); - return upload.IpfsHash; - }; - ``` - -Minting the NFT after the image and metadata have been uploaded is as simple as -calling `metaplex.nfts().create(...)`. Below shows the `createNFT` function -tying everything together: - ```tsx -const createNFT = async ( - name: string, - description: string, - fileUri: string, -) => { - if (!metaplex || !account || isLoading) return; +const pinata = useMemo( + () => + new PinataSDK({ + pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, + pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, + }), + [], +); +const uploadImage = useCallback( + async (fileUri: string): Promise => { + const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); + const upload = await pinata.upload.base64(imageBytesInBase64); + return upload.IpfsHash; + }, + [pinata], +); - setIsLoading(true); - try { - const imageCID = await uploadImage(fileUri); - const metadataCID = await uploadMetadata(name, description, imageCID); +const uploadMetadata = useCallback( + async ( + name: string, + description: string, + imageCID: string, + ): Promise => { + const upload = await pinata.upload.json({ name, description, imageCID }); + return upload.IpfsHash; + }, + [pinata], +); +``` - const nft = await metaplex.nfts().create({ - uri: `https://ipfs.io/ipfs/${metadataCID}`, - name: name, - sellerFeeBasisPoints: 0, - }); +Minting the NFT after the image and metadata have been uploaded is as simple as +calling ``createNft` from `@metaplex-foundation/mpl-token-metadata`. Below shows +the `createNFT` function tying everything together: - setNftOfTheDay(nft.nft); - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } -}; +```tsx +const createNFT = useCallback( + async (name: string, description: string, fileUri: string) => { + if (!umi || !account || isLoading) return; + setIsLoading(true); + try { + console.log(`Creating NFT...`); + const imageCID = await uploadImage(fileUri); + const metadataCID = await uploadMetadata(name, description, imageCID); + const mint = generateSigner(umi); + const transaction = createNft(umi, { + mint, + name, + uri: `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/${metadataCID}`, + sellerFeeBasisPoints: percentAmount(0), + }); + await transaction.sendAndConfirm(umi); + const createdNft = await fetchDigitalAsset(umi, mint.publicKey); + setNftOfTheDay(createdNft); + } catch (error) { + console.error("Failed to create NFT:", error); + } finally { + setIsLoading(false); + } + }, + [umi, account, isLoading, uploadImage, uploadMetadata], +); ``` We'll put all of the above into the `NFTProvider.tsx` file. All together, this @@ -1219,72 +1113,91 @@ looks as follows: ```tsx import "react-native-url-polyfill/auto"; -import React, { ReactNode, createContext, useContext, useState } from "react"; +import React, { + ReactNode, + createContext, + useContext, + useState, + useCallback, + useMemo, +} from "react"; +import { useConnection } from "./ConnectionProvider"; +import { Account, useAuthorization } from "./AuthProvider"; +import { useUmi } from "./MetaplexProvider"; import { - Metaplex, + publicKey, + Umi, PublicKey, - Metadata, - Nft, - Sft, - SftWithToken, - NftWithToken, -} from "@metaplex-foundation/js"; -import { useConnection } from "./ConnectionProvider"; -import { Connection, clusterApiUrl } from "@solana/web3.js"; + generateSigner, + percentAmount, +} from "@metaplex-foundation/umi"; +import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; +import { clusterApiUrl } from "@solana/web3.js"; import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol"; -import { Account, useAuthorization } from "./AuthProvider"; +import * as web3 from "@solana/web3.js"; import RNFetchBlob from "rn-fetch-blob"; -import { useMetaplex } from "./MetaplexProvider"; -import { Nft } from "../types"; +import { fromWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters"; +import { + createNft, + DigitalAsset, + fetchAllDigitalAssetByCreator, + fetchDigitalAsset, +} from "@metaplex-foundation/mpl-token-metadata"; +import { PinataSDK } from "pinata-web3"; export interface NFTProviderProps { children: ReactNode; } export interface NFTContextState { - metaplex: Metaplex | null; - publicKey: PublicKey | null; - isLoading: boolean; - loadedNFTs: (Nft | Sft | SftWithToken | NftWithToken)[] | null; - nftOfTheDay: (Nft | Sft | SftWithToken | NftWithToken) | null; - connect: () => void; - fetchNFTs: () => void; - createNFT: (name: string, description: string, fileUri: string) => void; + umi: Umi | null; // Holds the Umi object that we use to call `fetch` and `create` on. + publicKey: PublicKey | null; // The public key of the authorized wallet + isLoading: boolean; // Loading state + loadedNFTs: DigitalAsset[] | null; // Array of loaded NFTs that contain metadata + nftOfTheDay: DigitalAsset | null; // The NFT snapshot created on the current day + connect: () => void; // Connects (and authorizes) us to the Devnet-enabled wallet + fetchNFTs: () => void; // Fetches the NFTs using the `metaplex` object + createNFT: (name: string, description: string, fileUri: string) => void; // Creates the NFT } const DEFAULT_NFT_CONTEXT_STATE: NFTContextState = { - metaplex: new Metaplex(new Connection(clusterApiUrl("devnet"))), + umi: createUmi(clusterApiUrl("devnet")), publicKey: null, isLoading: false, loadedNFTs: null, nftOfTheDay: null, - connect: () => PublicKey.default, + connect: () => publicKey("00000000000000000000000000000000"), // Default PublicKey fetchNFTs: () => {}, - createNFT: (name: string, description: string, fileUri: string) => {}, + createNFT: () => {}, }; -const NFTContext = createContext(DEFAULT_NFT_CONTEXT_STATE); - export function formatDate(date: Date) { return `${date.getDate()}.${date.getMonth()}.${date.getFullYear()}`; } +const NFTContext = createContext(DEFAULT_NFT_CONTEXT_STATE); + export function NFTProvider(props: NFTProviderProps) { - const { children } = props; + const pinata = useMemo( + () => + new PinataSDK({ + pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, + pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, + }), + [], + ); + + const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; const { connection } = useConnection(); const { authorizeSession } = useAuthorization(); const [account, setAccount] = useState(null); + const [nftOfTheDay, setNftOfTheDay] = useState(null); + const [loadedNFTs, setLoadedNFTs] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [nftOfTheDay, setNftOfTheDay] = useState< - (Nft | Sft | SftWithToken | NftWithToken) | null - >(null); - const [loadedNFTs, setLoadedNFTs] = useState< - (Nft | Sft | SftWithToken | NftWithToken)[] | null - >(null); - - const { metaplex } = useMetaplex(connection, account, authorizeSession); + const { umi } = useUmi(connection, account, authorizeSession); + const { children } = props; - const connect = () => { + const connect = useCallback(() => { if (isLoading) return; setIsLoading(true); @@ -1294,102 +1207,85 @@ export function NFTProvider(props: NFTProviderProps) { }).finally(() => { setIsLoading(false); }); - }; - - const fetchNFTs = async () => { - if (!metaplex || !account || isLoading) return; + }, [isLoading, authorizeSession]); + const fetchNFTs = useCallback(async () => { + if (!umi || !account || isLoading) return; setIsLoading(true); try { - const nfts = await metaplex.nfts().findAllByCreator({ - creator: account.publicKey, - }); - - const loadedNFTs = await Promise.all( - nfts.map(nft => { - return metaplex.nfts().load({ metadata: nft as Metadata }); - }), - ); - setLoadedNFTs(loadedNFTs); - - // Check if we already took a snapshot today - const nftOfTheDayIndex = loadedNFTs.findIndex(nft => { - return formatDate(new Date(Date.now())) === nft.name; - }); - - if (nftOfTheDayIndex !== -1) { - setNftOfTheDay(loadedNFTs[nftOfTheDayIndex]); - } + const creatorPublicKey = fromWeb3JsPublicKey(account.publicKey); + const nfts = await fetchAllDigitalAssetByCreator(umi, creatorPublicKey); + setLoadedNFTs(nfts); } catch (error) { - console.log(error); + console.error("Failed to fetch NFTs:", error); } finally { setIsLoading(false); } - }; - - const uploadImage = async (fileUri: string): Promise => { - const imageBytesInBase64: string = await RNFetchBlob.fs.readFile( - fileUri, - "base64", - ); - - // pinata.upload.base64 is used to send the Base64-encoded image to Pinata Cloud - // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/base64#base64 - const upload = await pinata.upload.base64(imageBytesInBase64); - - // Return the IPFS hash of the uploaded image (IPFS is a decentralized file storage system) - return upload.IpfsHash; - }; - - const uploadMetadata = async ( - name: string, - description: string, - imageCID: string, - ): Promise => { - // pinata.upload.json is used to send the JSON to Pinata Cloud - // This is based on Pinata's Base64 upload API: https://docs.pinata.cloud/web3/sdk/upload/json - const upload = await pinata.upload.json({ - name: name, - description: description, - imageCID: imageCID, - }); - return upload.IpfsHash; - }; - - const createNFT = async ( - name: string, - description: string, - fileUri: string, - ) => { - if (!metaplex || !account || isLoading) return; - - setIsLoading(true); - try { - const imageCID = await uploadImage(fileUri); - const metadataCID = await uploadMetadata(name, description, imageCID); + }, [umi, account, isLoading]); + const uploadImage = useCallback( + async (fileUri: string): Promise => { + const imageBytesInBase64 = await RNFetchBlob.fs.readFile( + fileUri, + "base64", + ); + const upload = await pinata.upload.base64(imageBytesInBase64); + return upload.IpfsHash; + }, + [pinata], + ); - const nft = await metaplex.nfts().create({ - uri: `https://ipfs.io/ipfs/${metadataCID}`, - name: name, - sellerFeeBasisPoints: 0, - }); + const uploadMetadata = useCallback( + async ( + name: string, + description: string, + imageCID: string, + ): Promise => { + const upload = await pinata.upload.json({ name, description, imageCID }); + return upload.IpfsHash; + }, + [pinata], + ); - setNftOfTheDay(nft.nft); - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; + const createNFT = useCallback( + async (name: string, description: string, fileUri: string) => { + if (!umi || !account || isLoading) return; + setIsLoading(true); + try { + console.log(`Creating NFT...`); + const imageCID = await uploadImage(fileUri); + const metadataCID = await uploadMetadata(name, description, imageCID); + const mint = generateSigner(umi); + const transaction = createNft(umi, { + mint, + name, + uri: ipfsPrefix + metadataCID, + sellerFeeBasisPoints: percentAmount(0), + }); + await transaction.sendAndConfirm(umi); + const createdNft = await fetchDigitalAsset(umi, mint.publicKey); + setNftOfTheDay(createdNft); + } catch (error) { + console.error("Failed to create NFT:", error); + } finally { + setIsLoading(false); + } + }, + [umi, account, isLoading, uploadImage, uploadMetadata], + ); - const publicKey = account?.publicKey ?? null; + const publicKey = useMemo( + () => + account?.publicKey + ? fromWeb3JsPublicKey(account.publicKey as web3.PublicKey) + : null, + [account], + ); - const state = { + const state: NFTContextState = { isLoading, - account, publicKey, - metaplex, + umi, nftOfTheDay, loadedNFTs, connect, @@ -1530,50 +1426,90 @@ export function MainScreen() { const [previousImages, setPreviousImages] = React.useState(DEFAULT_IMAGES); const todaysDate = new Date(Date.now()); + const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; + type NftMetaResponse = { + name: string; + description: string; + imageCID: string; + }; + const fetchMetadata = async (uri: string) => { + try { + const response = await fetch(uri); + const metadata = await response.json(); + return metadata as NftMetaResponse; + } catch (error) { + console.error("Error fetching metadata:", error); + return null; + } + }; useEffect(() => { if (!loadedNFTs) return; - const loadedSnapshots = loadedNFTs.map(loadedNft => { - if (!loadedNft.json) return null; - if (!loadedNft.json.name) return null; - if (!loadedNft.json.description) return null; - if (!loadedNft.json.image) return null; + const loadSnapshots = async () => { + const loadedSnapshots = await Promise.all( + loadedNFTs.map(async loadedNft => { + if (!loadedNft.metadata.name) return null; + if (!loadedNft.metadata.uri) return null; - const uri = loadedNft.json.image; - const unixTime = Number(loadedNft.json.description); + const metadata = await fetchMetadata(loadedNft.metadata.uri); + if (!metadata) return null; - if (!uri) return null; - if (isNaN(unixTime)) return null; + const { imageCID, description } = metadata; + if (!imageCID || !description) return null; - return { - uri: loadedNft.json.image, - date: new Date(unixTime), - } as NFTSnapshot; - }); + const unixTime = Number(description); + if (isNaN(unixTime)) return null; + + return { + uri: ipfsPrefix + imageCID, + date: new Date(unixTime), + } as NFTSnapshot; + }), + ); + + // Filter out null values + const cleanedSnapshots = loadedSnapshots.filter( + (snapshot): snapshot is NFTSnapshot => snapshot !== null, + ); - // Filter out null values - const cleanedSnapshots = loadedSnapshots.filter(loadedSnapshot => { - return loadedSnapshot !== null; - }) as NFTSnapshot[]; + // Sort by date + cleanedSnapshots.sort((a, b) => b.date.getTime() - a.date.getTime()); - // Sort by date - cleanedSnapshots.sort((a, b) => { - return b.date.getTime() - a.date.getTime(); - }); + setPreviousImages(cleanedSnapshots); + }; - setPreviousImages(cleanedSnapshots as NFTSnapshot[]); + loadSnapshots(); }, [loadedNFTs]); useEffect(() => { if (!nftOfTheDay) return; - setCurrentImage({ - uri: nftOfTheDay.json?.image ?? "", - date: todaysDate, - }); - }, [nftOfTheDay]); + const fetchNftOfTheDayMetadata = async () => { + try { + if (!nftOfTheDay.metadata.uri) { + console.error("No metadata URI found for nftOfTheDay"); + return; + } + + const response = await fetchMetadata(nftOfTheDay.metadata.uri); + + if (!response?.imageCID) { + console.error("No image found in nftOfTheDay metadata"); + return; + } + + setCurrentImage({ + uri: ipfsPrefix + response.imageCID, + date: todaysDate, + }); + } catch (error) { + console.error("Error fetching nftOfTheDay metadata:", error); + } + }; + fetchNftOfTheDayMetadata(); + }, [nftOfTheDay, todaysDate]); const mintNFT = async () => { const result = await ImagePicker.launchCameraAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, @@ -1662,7 +1598,7 @@ approve the app. Fetch all of the NFTs by tapping `Fetch NFTs`. Lastly, tap Congratulations! That was not an easy or quick lab. You're doing great if you've made it this far. If you run into any issues, please go back through the lab and/or reference the final solution code on the -[`main` branch in Github](https://github.com/Unboxed-Software/solana-advance-mobile). +[`main` branch in Github](https://github.com/XuananLe/solana-advance-mobile). ## Challenge From c0a7fc581241c0cc2b313c992a37ed46750f1ced Mon Sep 17 00:00:00 2001 From: XuananLe Date: Fri, 13 Sep 2024 16:16:26 +0700 Subject: [PATCH 08/15] Fix NftProvider + Update ContentBox --- .../mobile/solana-mobile-dapps-with-expo.md | 129 +++++++++--------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index ddd24248e..d3aecd6ca 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -38,8 +38,8 @@ lesson will be spent in the lab. ### React Native Expo Expo is an open-source platform for making universal native apps for Android, -iOS, and the web that wrap around React Native, much like Next.js is a framework -built on top of React. +iOS, and the web that wraps around React Native, much like Next.js is a +framework built on top of React. Expo consists of three main parts: @@ -122,11 +122,10 @@ the following inside this file: ``` With the EAS configuration file in place, you can build your project using the -`eas build` command along with relevant flags to meet any additional -requirements. This command submits a job to the EAS Build service, where your -APK is built using Expo's cloud infrastructure. If you want to build locally, -you can add the `--local` flag. For example, the following command builds the -project locally with a development profile specifically for Android: +`eas build`. This submits a job to the EAS Build service, where your APK is +built using Expo's cloud infrastructure. If you want to build locally, you can +add the `--local` flag. For example, the following command builds the project +locally with a development profile specifically for Android: ```bash eas build --profile development --platform android --message "Developing on Android!" --local @@ -176,7 +175,8 @@ if you're using the `expo-camera` package, you not only need to install the package but also configure the appropriate permissions in your `app.json` or `AndroidManifest.xml` file for Android and request runtime permissions for accessing the camera. Be sure to read the -[docs](https://docs.expo.dev/versions/latest/) when working with a new package. +[Expo docs](https://docs.expo.dev/versions/latest/) when working with a new +package. ### Integrate ecosystem libraries into your Expo app @@ -210,9 +210,10 @@ For a Solana + Expo app, you'll need the following: as `Transaction` and `Uint8Array`. - `@solana/web3.js`: Solana Web Library for interacting with the Solana network through the [JSON RPC API](https://docs.solana.com/api/http). -- `expo-crypto`: Secure random number generator polyfill. - for `web3.js` underlying Crypto library on React Native. (This only works for - Expo SDK Version 49+ and Expo Router, so make sure you update) +- `expo-crypto` is a secure random number generator polyfill used in React + Native for web3.js's underlying Crypto library. This feature is supported only + in Expo SDK version 49+ and requires Expo Router. Make sure your setup is + updated to meet these requirements. - `buffer`: Buffer polyfill needed for `web3.js` on React Native. #### Metaplex Polyfills @@ -271,8 +272,10 @@ able to mint a single NFT snapshot of their lives daily, creating a permanent diary of sorts. To mint the NFTs we'll be using Metaplex's Umi libraries along with -[Pinata Cloud](https://pinata.cloud/) to store images and metadata. All of our -onchain work will be on Devnet. +[Pinata Cloud](https://pinata.cloud/) to store images and metadata. We are using +Pinata in this tutorial, but +[there are many good solutions for store images for long-term storage](https://solana.com/developers/guides/getstarted/how-to-create-a-token#create-and-upload-image-and-offchain-metadata). +All of our onchain work will be on Devnet. The first half of this lab is cobbling together the needed components to make Expo, Solana, and Metaplex all work together. We'll do this modularly so you'll @@ -319,7 +322,7 @@ Let’s create our app with the following: ```bash npx create-expo-app --template blank-typescript solana-expo cd solana-expo -npx expo install expo-dev-client # A library that allows creating a development build and includes useful development tools. It is optional but recommended. +npx expo install expo-dev-client # This installs a library that enables the creation of custom development builds, providing useful tools for debugging and testing. While optional, it is recommended for a smoother development experience. ``` This uses `create-expo-app` to generate a new scaffold for us based on the @@ -473,6 +476,7 @@ Next, create file called `polyfills.ts` for react-native to work with all solana dependencies ```typescript +// In this case, we polyfill the global Crypto object with getRandomValues from expo-crypto. import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto"; import { Buffer } from "buffer"; @@ -626,9 +630,11 @@ defaultConfig.resolver.extraNodeModules = { // Export the modified configuration module.exports = { ...defaultConfig, - // See more why we have to do here at: https://github.com/metaplex-foundation/umi/issues/94 resolver: { ...defaultConfig.resolver, + // This issue is caused because the @metaplex-foundation/umi package uses Package Exports to export the umi/serializers submodule. + + // See more why we have to do here at: https://github.com/metaplex-foundation/umi/issues/94 unstable_enablePackageExports: true, }, }; @@ -636,16 +642,14 @@ module.exports = { #### 3. Metaplex provider -We're going to create a Metaplex provider file that will help us access an `Umi` -object (Read more about `umi` at -[Umi docs](https://developers.metaplex.com/umi)).This `Umi` object, combined -with other libraries such as `@metaplex-foundation/umi-bundle-defaults`, -`@metaplex-foundation/mpl-token-metadata`and -`@metaplex-foundation/mpl-candy-machine`, will give us access to all the -functions we'll need later, like `fetch` and `create`.. To do this we create a -new file `/components/MetaplexProvider.tsx`. Here we pipe our mobile wallet -adapter into the `Umi` object to use. This allows it to call several privileged -functions on our behalf: +We'll be creating NFTs using +[Metaplex's MPL Token Metadata library](https://developers.metaplex.com/token-metadata), +leveraging the `Umi` object, a tool commonly used in many Metaplex applications. +This combination will give us access to key functions like `fetch` and `create` +that are essential for NFT creation. To set this up, we will create a new file, +`/components/MetaplexProvider.tsx`, where we'll connect our mobile wallet +adapter to the `Umi` object. This allows us to execute privileged actions, such +as interacting with token metadata, on our behalf. ```tsx import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; @@ -774,11 +778,12 @@ Notice we've added yet another polyfill to the top Now, let's wrap our new `NFTProvider` around `MainScreen` in `App.tsx`: ```tsx +import "./polyfills"; import { ConnectionProvider } from "./components/ConnectionProvider"; import { AuthorizationProvider } from "./components/AuthProvider"; import { clusterApiUrl } from "@solana/web3.js"; import { MainScreen } from "./screens/MainScreen"; -import "./polyfills"; +import { NFTProvider } from "./components/NFTProvider"; export default function App() { const cluster = "devnet"; @@ -791,7 +796,9 @@ export default function App() { config={{ commitment: "processed" }} > - + + + ); @@ -893,9 +900,7 @@ as an environment variable, then we need to add one last dependency to convert our images into a file type we can upload. We'll be using Pinata Cloud to host our NFTs with IPFS since they do this for a -very cheap price compare to other solutions such as Akord,... -[Sign up, and create an API key](https://app.pinata.cloud/developers/api-keys). -Keep this API key private. +very cheap price. Remember to keep this API key private. Best practices suggest keeping API keys in a `.env` file with `.env` added to your `.gitignore`. It's also a good idea to create a `.env.example` file that @@ -977,8 +982,8 @@ This should have the following fields: function that creates a new snapshot NFT The `DigitalAsset` type comes from `@metaplex-foundation/mpl-token-metadata` -that have metadata, off chain metadata, collection data, plugins (including -Attributes), and more +that have metadata, off-chain metadata, collection data, plugins (including +Attributes), and more. ```tsx export interface NFTContextState { @@ -1045,6 +1050,8 @@ const fetchNFTs = useCallback(async () => { separately, then tie them together into a single `createNFT` function: ```tsx +const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; + const pinata = useMemo( () => new PinataSDK({ @@ -1076,7 +1083,7 @@ const uploadMetadata = useCallback( ``` Minting the NFT after the image and metadata have been uploaded is as simple as -calling ``createNft` from `@metaplex-foundation/mpl-token-metadata`. Below shows +calling `createNft` from `@metaplex-foundation/mpl-token-metadata`. Below shows the `createNFT` function tying everything together: ```tsx @@ -1092,7 +1099,7 @@ const createNFT = useCallback( const transaction = createNft(umi, { mint, name, - uri: `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/${metadataCID}`, + uri: ipfsPrefix + metadataCID, sellerFeeBasisPoints: percentAmount(0), }); await transaction.sendAndConfirm(umi); @@ -1112,38 +1119,37 @@ We'll put all of the above into the `NFTProvider.tsx` file. All together, this looks as follows: ```tsx -import "react-native-url-polyfill/auto"; -import React, { - ReactNode, - createContext, - useContext, - useState, - useCallback, - useMemo, -} from "react"; -import { useConnection } from "./ConnectionProvider"; -import { Account, useAuthorization } from "./AuthProvider"; -import { useUmi } from "./MetaplexProvider"; import { - publicKey, - Umi, + DigitalAsset, + createNft, + fetchAllDigitalAssetByCreator, + fetchDigitalAsset, +} from "@metaplex-foundation/mpl-token-metadata"; +import { PublicKey, + Umi, generateSigner, percentAmount, + publicKey, } from "@metaplex-foundation/umi"; import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; -import { clusterApiUrl } from "@solana/web3.js"; -import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol"; -import * as web3 from "@solana/web3.js"; -import RNFetchBlob from "rn-fetch-blob"; import { fromWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters"; -import { - createNft, - DigitalAsset, - fetchAllDigitalAssetByCreator, - fetchDigitalAsset, -} from "@metaplex-foundation/mpl-token-metadata"; +import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol"; +import { clusterApiUrl, PublicKey as solanaPublicKey } from "@solana/web3.js"; import { PinataSDK } from "pinata-web3"; +import React, { + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import "react-native-url-polyfill/auto"; +import RNFetchBlob from "rn-fetch-blob"; +import { Account, useAuthorization } from "./AuthProvider"; +import { useConnection } from "./ConnectionProvider"; +import { useUmi } from "./MetaplexProvider"; export interface NFTProviderProps { children: ReactNode; @@ -1178,6 +1184,7 @@ export function formatDate(date: Date) { const NFTContext = createContext(DEFAULT_NFT_CONTEXT_STATE); export function NFTProvider(props: NFTProviderProps) { + const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; const pinata = useMemo( () => new PinataSDK({ @@ -1186,8 +1193,6 @@ export function NFTProvider(props: NFTProviderProps) { }), [], ); - - const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; const { connection } = useConnection(); const { authorizeSession } = useAuthorization(); const [account, setAccount] = useState(null); @@ -1277,7 +1282,7 @@ export function NFTProvider(props: NFTProviderProps) { const publicKey = useMemo( () => account?.publicKey - ? fromWeb3JsPublicKey(account.publicKey as web3.PublicKey) + ? fromWeb3JsPublicKey(account.publicKey as solanaPublicKey) : null, [account], ); @@ -1598,7 +1603,7 @@ approve the app. Fetch all of the NFTs by tapping `Fetch NFTs`. Lastly, tap Congratulations! That was not an easy or quick lab. You're doing great if you've made it this far. If you run into any issues, please go back through the lab and/or reference the final solution code on the -[`main` branch in Github](https://github.com/XuananLe/solana-advance-mobile). +[`main` branch in Github](https://github.com/solana-developers/mobile-apps-with-expo). ## Challenge From 27dd35f1f4099c75a075e9706dc4bc32f94e33b8 Mon Sep 17 00:00:00 2001 From: XuananLe Date: Wed, 25 Sep 2024 13:01:36 +0700 Subject: [PATCH 09/15] Update Code + Address all the issues --- .../mobile/solana-mobile-dapps-with-expo.md | 468 ++++++++++-------- 1 file changed, 254 insertions(+), 214 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index d3aecd6ca..cc80ea5c8 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -103,7 +103,7 @@ the following inside this file: ```json { "cli": { - "version": ">= 5.2.0" + "version": ">= 3.12.0" }, "build": { "development": { @@ -237,24 +237,16 @@ plus a few additional polyfills: Native. Below is an example `metro.config.js` file: ```js -// Import the default Expo Metro config -const { getDefaultConfig } = require("@expo/metro-config"); - -// Get the default Expo Metro configuration -const defaultConfig = getDefaultConfig(__dirname); - -// Customize the configuration to include your extra node modules -defaultConfig.resolver.extraNodeModules = { - crypto: require.resolve("crypto-browserify"), - stream: require.resolve("readable-stream"), - url: require.resolve("react-native-url-polyfill"), - zlib: require.resolve("browserify-zlib"), - path: require.resolve("path-browserify"), - crypto: require.resolve("expo-crypto"), -}; +// Learn more https://docs.expo.io/guides/customizing-metro +const { getDefaultConfig } = require("expo/metro-config"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +// Add polyfill resolvers +config.resolver.extraNodeModules.crypto = require.resolve("expo-crypto"); -// Export the modified configuration -module.exports = defaultConfig; +module.exports = config; ``` ### Putting it all together @@ -311,7 +303,7 @@ First sign up for an [EAS account](https://expo.dev/eas). Then, install the EAS CLI and log in: ```bash -npm install --global eas-cli +npm install -g eas-cli eas login ``` @@ -405,9 +397,9 @@ from our app: ```bash cd .. -git clone https://github.com/XuananLe/react-native-fake-solana-wallet +git clone https://github.com/solana-developers/react-native-fake-solana-wallet cd react-native-fake-solana-wallet -npm install +yarn ``` The wallet should be installed on your emulator or device. Make sure to open the @@ -428,7 +420,7 @@ all Solana mobile apps. This will include some polyfills that allow otherwise incompatible packages to work with React native: ```bash -npm install \ +yarn add \ @solana/web3.js \ @solana-mobile/mobile-wallet-adapter-protocol-web3js \ @solana-mobile/mobile-wallet-adapter-protocol \ @@ -445,16 +437,16 @@ Create two new folders: `components` and `screens`. We are going to use some boilerplate code from the [first Mobile lesson](/content/courses/mobile/basic-solana-mobile). We will be -copying over `components/AuthProvider.tsx` and +copying over `components/AuthorizationProvider.tsx` and `components/ConnectionProvider.tsx`. These files provide us with a `Connection` object as well as some helper functions that authorize our dapp. -Create file `components/AuthProvider.tsx` and copy the contents -[of our existing Auth Provider from Github](https://raw.githubusercontent.com/Unboxed-Software/solana-advance-mobile/main/components/AuthProvider.tsx) +Create file `components/AuthorizationProvider.tsx` and copy the contents +[of our existing Auth Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/AuthorizationProvider.tsx) into the new file. Secondly, create file `components/ConnectionProvider.tsx` and copy the contents -[of our existing Connection Provider from Github](https://raw.githubusercontent.com/Unboxed-Software/solana-advance-mobile/main/components/ConnectionProvider.tsx) +[of our existing Connection Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/ConnectionProvider.tsx) into the new file. Now let's create a boilerplate for our main screen in `screens/MainScreen.tsx`: @@ -476,28 +468,31 @@ Next, create file called `polyfills.ts` for react-native to work with all solana dependencies ```typescript -// In this case, we polyfill the global Crypto object with getRandomValues from expo-crypto. import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto"; import { Buffer } from "buffer"; +// Set global Buffer global.Buffer = Buffer; -// getRandomValues polyfill +// Define Crypto class with getRandomValues method class Crypto { getRandomValues = expoCryptoGetRandomValues; } -const webCrypto = typeof crypto !== "undefined" ? crypto : new Crypto(); +// Check if crypto is already defined in the global scope +const hasInbuiltWebCrypto = typeof window.crypto !== "undefined"; -(() => { - if (typeof crypto === "undefined") { - Object.defineProperty(window, "crypto", { - configurable: true, - enumerable: true, - get: () => webCrypto, - }); - } -})(); +// Use existing crypto if available, otherwise create a new Crypto instance +const webCrypto = hasInbuiltWebCrypto ? window.crypto : new Crypto(); + +// Polyfill crypto object if it's not already defined +if (!hasInbuiltWebCrypto) { + Object.defineProperty(window, "crypto", { + configurable: true, + enumerable: true, + get: () => webCrypto, + }); +} ``` Finally, let's change `App.tsx` to wrap our application in the two providers we @@ -505,7 +500,7 @@ just created: ```tsx import { ConnectionProvider } from "./components/ConnectionProvider"; -import { AuthorizationProvider } from "./components/AuthProvider"; +import { AuthorizationProvider } from "./components/AuthorizationProvider"; import { clusterApiUrl } from "@solana/web3.js"; import { MainScreen } from "./screens/MainScreen"; import "./polyfills"; @@ -544,7 +539,7 @@ Add the following convenient run scripts to your `package.json` file. "build": "npx eas build --profile development --platform android", "build:local": "npx eas build --profile development --platform android --local", "test": "echo \"No tests specified\" && exit 0", - "clean": "rm -rf node_modules && npm install" + "clean": "rm -rf node_modules && yarn" } ``` @@ -557,7 +552,7 @@ _uninstall_ the previous version before you drag and drop the new one in. Build locally: ```bash -npm run build:local +yarn run build:local ``` Install: **_Drag_** the resulting build file into your emulator. @@ -565,7 +560,7 @@ Install: **_Drag_** the resulting build file into your emulator. Run: ```bash -npm run android +yarn run android ``` Everything should compile and you should have a boilerplate Solana Expo app. @@ -584,7 +579,7 @@ abstracts away a lot of the minutia of working with NFTs, however it was written largely for Node.js, so we'll need several more polyfills to make it work: ```bash -npm install assert \ +yarn add assert \ util \ crypto-browserify \ stream-browserify \ @@ -594,9 +589,10 @@ npm install assert \ react-native-url-polyfill \ @metaplex-foundation/umi \ @metaplex-foundation/umi-bundle-defaults \ - @metaplex-foundation/umi-bundle-defaults \ @metaplex-foundation/umi-signer-wallet-adapters \ - @metaplex-foundation/umi-web3js-adapters \ + @metaplex-foundation/umi-web3js-adapters \ + @metaplex-foundation/mpl-token-metadata \ + @metaplex-foundation/mpl-candy-machine ``` #### 2. Polyfill config @@ -611,33 +607,16 @@ touch metro.config.js Copy and paste the following into `metro.config.js`: ```javascript -// Import the default Expo Metro config -const { getDefaultConfig } = require("@expo/metro-config"); - -// Get the default Expo Metro configuration -const defaultConfig = getDefaultConfig(__dirname); - -// Customize the configuration to include your extra node modules -defaultConfig.resolver.extraNodeModules = { - crypto: require.resolve("crypto-browserify"), - stream: require.resolve("readable-stream"), - url: require.resolve("react-native-url-polyfill"), - zlib: require.resolve("browserify-zlib"), - path: require.resolve("path-browserify"), - crypto: require.resolve("expo-crypto"), -}; +// Learn more https://docs.expo.io/guides/customizing-metro +const { getDefaultConfig } = require("expo/metro-config"); -// Export the modified configuration -module.exports = { - ...defaultConfig, - resolver: { - ...defaultConfig.resolver, - // This issue is caused because the @metaplex-foundation/umi package uses Package Exports to export the umi/serializers submodule. +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); - // See more why we have to do here at: https://github.com/metaplex-foundation/umi/issues/94 - unstable_enablePackageExports: true, - }, -}; +// Add polyfill resolvers +config.resolver.extraNodeModules.crypto = require.resolve("expo-crypto"); + +module.exports = config; ``` #### 3. Metaplex provider @@ -647,84 +626,75 @@ We'll be creating NFTs using leveraging the `Umi` object, a tool commonly used in many Metaplex applications. This combination will give us access to key functions like `fetch` and `create` that are essential for NFT creation. To set this up, we will create a new file, -`/components/MetaplexProvider.tsx`, where we'll connect our mobile wallet -adapter to the `Umi` object. This allows us to execute privileged actions, such -as interacting with token metadata, on our behalf. +`/components/UmiProvider.tsx`, where we'll connect our mobile wallet adapter to +the `Umi` object. This allows us to execute privileged actions, such as +interacting with token metadata, on our behalf. ```tsx +import { createContext, ReactNode, useContext } from "react"; +import type { Umi } from "@metaplex-foundation/umi"; +import { + createNoopSigner, + publicKey, + signerIdentity, +} from "@metaplex-foundation/umi"; import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; -import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; -import { - transact, - Web3MobileWallet, -} from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"; -import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; -import { useMemo } from "react"; -import { Account } from "./AuthProvider"; - -type LegacyOrVersionedTransact = Transaction | VersionedTransaction; - -export const useUmi = ( - connection: Connection, - selectedAccount: Account | null, - authorizeSession: (wallet: Web3MobileWallet) => Promise, -) => { - return useMemo(() => { - if (!selectedAccount || !authorizeSession) { - return { mobileWalletAdapter: null, umi: null }; - } +import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata"; +import { mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine"; +import { useAuthorization } from "./AuthorizationProvider"; - const mobileWalletAdapter = { - publicKey: selectedAccount.publicKey, - signMessage: async (message: Uint8Array): Promise => { - return await transact(async (wallet: Web3MobileWallet) => { - await authorizeSession(wallet); - const signedMessages = await wallet.signMessages({ - addresses: [selectedAccount.publicKey.toBase58()], - payloads: [message], - }); - return signedMessages[0]; - }); - }, - signTransaction: async ( - transaction: T, - ): Promise => { - return await transact(async (wallet: Web3MobileWallet) => { - await authorizeSession(wallet); - const signedTransactions = await wallet.signTransactions({ - transactions: [transaction], - }); - return signedTransactions[0] as T; - }); - }, - signAllTransactions: async ( - transactions: T[], - ): Promise => { - return transact(async (wallet: Web3MobileWallet) => { - await authorizeSession(wallet); - const signedTransactions = await wallet.signTransactions({ - transactions: transactions, - }); - return signedTransactions as T[]; - }); - }, - }; - // Add Umi Plugins - const umi = createUmi(connection.rpcEndpoint) - .use(mplTokenMetadata()) - .use(walletAdapterIdentity(mobileWalletAdapter)); +type UmiContext = { + umi: Umi | null; +}; + +const DEFAULT_CONTEXT: UmiContext = { + umi: null, +}; + +export const UmiContext = createContext(DEFAULT_CONTEXT); - return { umi }; - }, [authorizeSession, selectedAccount, connection]); +export const UmiProvider = ({ + endpoint, + children, +}: { + endpoint: string; + children: ReactNode; +}) => { + const { selectedAccount } = useAuthorization(); + console.log("selectedAccount", JSON.stringify(selectedAccount, null, 2)); + const umi = createUmi(endpoint) + .use(mplTokenMetadata()) + .use(mplCandyMachine()); + if (selectedAccount === null) { + const noopSigner = createNoopSigner( + publicKey("11111111111111111111111111111111"), + ); + umi.use(signerIdentity(noopSigner)); + } else { + umi.use(walletAdapterIdentity(selectedAccount)); + } + + return {children}; }; + +export function useUmi(): Umi { + const umi = useContext(UmiContext).umi; + if (!umi) { + throw new Error( + "Umi context was not initialized. " + + "Did you forget to wrap your app with ?", + ); + } + return umi; +} ``` #### 4. NFT Provider We're also making a higher-level NFT provider that helps with NFT state management. It combines all three of our previous providers: -`ConnectionProvider`, `AuthProvider`, and `MetaplexProvider` to allow us to +`ConnectionProvider`, `AuthorizationProvider`, and `UmiProvider` to allow us to create our `Umi` object. We will fill this out at a later step; for now, it makes for a good boilerplate. @@ -733,9 +703,9 @@ Let's create the new file `components/NFTProvider.tsx`: ```tsx import "react-native-url-polyfill/auto"; import { useConnection } from "./ConnectionProvider"; -import { Account, useAuthorization } from "./AuthProvider"; +import { Account, useAuthorization } from "./AuthorizationProvider"; import React, { ReactNode, createContext, useContext, useState } from "react"; -import { useUmi } from "./MetaplexProvider"; +import { useUmi } from "./UmiProvider"; import { Umi } from "@metaplex-foundation/umi"; export interface NFTProviderProps { @@ -780,7 +750,7 @@ Now, let's wrap our new `NFTProvider` around `MainScreen` in `App.tsx`: ```tsx import "./polyfills"; import { ConnectionProvider } from "./components/ConnectionProvider"; -import { AuthorizationProvider } from "./components/AuthProvider"; +import { AuthorizationProvider } from "./components/AuthorizationProvider"; import { clusterApiUrl } from "@solana/web3.js"; import { MainScreen } from "./screens/MainScreen"; import { NFTProvider } from "./components/NFTProvider"; @@ -796,8 +766,7 @@ export default function App() { config={{ commitment: "processed" }} > - - + @@ -924,7 +893,7 @@ device's URI scheme and turn them into Blobs we can the upload to Install it with the following: ```bash -npm install rn-fetch-blob +yarn add rn-fetch-blob ``` #### 3. Final build @@ -1044,30 +1013,68 @@ const fetchNFTs = useCallback(async () => { To upload to Pinata Cloud you need to init `PinataSDK` instance with `pinataJwt` and `pinataGateway`. After that, you can interact with their - [API](https://docs.pinata.cloud/web3/sdk/getting-started) + [API](https://docs.pinata.cloud/web3/sdk/getting-started) to perform uploads. We'll create two helper functions for uploading the image and metadata separately, then tie them together into a single `createNFT` function: ```tsx const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; +async function uploadBase64(base64String: string) { + try { + const buffer = Buffer.from(base64String, "base64"); + const blob = new Blob([buffer]); + const file = new File([blob], "file"); + const data = new FormData(); + data.append("file", file); + + const upload = await fetch( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, + }, + ); + const uploadRes = await upload.json(); + return uploadRes; + } catch (error) { + console.log(error); + } +} -const pinata = useMemo( - () => - new PinataSDK({ - pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, - pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, - }), - [], -); -const uploadImage = useCallback( - async (fileUri: string): Promise => { - const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); - const upload = await pinata.upload.base64(imageBytesInBase64); - return upload.IpfsHash; - }, - [pinata], -); +async function uploadMetadataJson( + name: string, + description: string, + imageCID: string, +) { + const data = JSON.stringify({ + pinataContent: { + name, + description, + imageCID, + }, + }); + + const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, + }); + + const resData = await res.json(); + return resData; +} +const uploadImage = useCallback(async (fileUri: string): Promise => { + const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); + const upload = await uploadBase64(imageBytesInBase64); + return upload.IpfsHash; +}, []); const uploadMetadata = useCallback( async ( @@ -1075,10 +1082,17 @@ const uploadMetadata = useCallback( description: string, imageCID: string, ): Promise => { - const upload = await pinata.upload.json({ name, description, imageCID }); - return upload.IpfsHash; + const data = JSON.stringify({ + pinataContent: { + name, + description, + imageCID, + }, + }); + const uploadRes = await uploadMetadataJson(name, description, imageCID); + return uploadRes.IpfsHash; }, - [pinata], + [], ); ``` @@ -1119,6 +1133,7 @@ We'll put all of the above into the `NFTProvider.tsx` file. All together, this looks as follows: ```tsx +import "react-native-url-polyfill/auto"; import { DigitalAsset, createNft, @@ -1130,13 +1145,9 @@ import { Umi, generateSigner, percentAmount, - publicKey, } from "@metaplex-foundation/umi"; -import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; import { fromWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters"; -import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol"; import { clusterApiUrl, PublicKey as solanaPublicKey } from "@solana/web3.js"; -import { PinataSDK } from "pinata-web3"; import React, { ReactNode, createContext, @@ -1145,11 +1156,11 @@ import React, { useMemo, useState, } from "react"; -import "react-native-url-polyfill/auto"; import RNFetchBlob from "rn-fetch-blob"; -import { Account, useAuthorization } from "./AuthProvider"; import { useConnection } from "./ConnectionProvider"; -import { useUmi } from "./MetaplexProvider"; +import { useUmi } from "./UmiProvider"; +import { useMobileWallet } from "../utils/useMobileWallet"; +import { Account, useAuthorization } from "./AuthorizationProvider"; export interface NFTProviderProps { children: ReactNode; @@ -1166,53 +1177,74 @@ export interface NFTContextState { createNFT: (name: string, description: string, fileUri: string) => void; // Creates the NFT } -const DEFAULT_NFT_CONTEXT_STATE: NFTContextState = { - umi: createUmi(clusterApiUrl("devnet")), - publicKey: null, - isLoading: false, - loadedNFTs: null, - nftOfTheDay: null, - connect: () => publicKey("00000000000000000000000000000000"), // Default PublicKey - fetchNFTs: () => {}, - createNFT: () => {}, -}; - export function formatDate(date: Date) { return `${date.getDate()}.${date.getMonth()}.${date.getFullYear()}`; } -const NFTContext = createContext(DEFAULT_NFT_CONTEXT_STATE); +const NFTContext = createContext(null); export function NFTProvider(props: NFTProviderProps) { const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; - const pinata = useMemo( - () => - new PinataSDK({ - pinataJwt: process.env.EXPO_PUBLIC_PINATA_JWT, - pinataGateway: process.env.EXPO_PUBLIC_PINATA_GATEWAY, - }), - [], - ); const { connection } = useConnection(); - const { authorizeSession } = useAuthorization(); + const { authorizeSession, deauthorizeSession } = useAuthorization(); const [account, setAccount] = useState(null); const [nftOfTheDay, setNftOfTheDay] = useState(null); const [loadedNFTs, setLoadedNFTs] = useState(null); const [isLoading, setIsLoading] = useState(false); - const { umi } = useUmi(connection, account, authorizeSession); + const umi = useUmi(); const { children } = props; - const connect = useCallback(() => { - if (isLoading) return; + const { connect } = useMobileWallet(); + async function uploadBase64(base64String: string) { + try { + const buffer = Buffer.from(base64String, "base64"); + const blob = new Blob([buffer]); + const file = new File([blob], "file"); + const data = new FormData(); + data.append("file", file); + + const upload = await fetch( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, + }, + ); + const uploadRes = await upload.json(); + return uploadRes; + } catch (error) { + console.log(error); + } + } - setIsLoading(true); - transact(async wallet => { - const auth = await authorizeSession(wallet); - setAccount(auth); - }).finally(() => { - setIsLoading(false); + async function uploadMetadataJson( + name: string, + description: string, + imageCID: string, + ) { + const data = JSON.stringify({ + pinataContent: { + name, + description, + imageCID, + }, }); - }, [isLoading, authorizeSession]); + + const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, + }); + + const resData = await res.json(); + return resData; + } const fetchNFTs = useCallback(async () => { if (!umi || !account || isLoading) return; @@ -1228,17 +1260,12 @@ export function NFTProvider(props: NFTProviderProps) { setIsLoading(false); } }, [umi, account, isLoading]); - const uploadImage = useCallback( - async (fileUri: string): Promise => { - const imageBytesInBase64 = await RNFetchBlob.fs.readFile( - fileUri, - "base64", - ); - const upload = await pinata.upload.base64(imageBytesInBase64); - return upload.IpfsHash; - }, - [pinata], - ); + + const uploadImage = useCallback(async (fileUri: string): Promise => { + const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); + const upload = await uploadBase64(imageBytesInBase64); + return upload.IpfsHash; + }, []); const uploadMetadata = useCallback( async ( @@ -1246,10 +1273,17 @@ export function NFTProvider(props: NFTProviderProps) { description: string, imageCID: string, ): Promise => { - const upload = await pinata.upload.json({ name, description, imageCID }); - return upload.IpfsHash; + const data = JSON.stringify({ + pinataContent: { + name, + description, + imageCID, + }, + }); + const uploadRes = await uploadMetadataJson(name, description, imageCID); + return uploadRes.IpfsHash; }, - [pinata], + [], ); const createNFT = useCallback( @@ -1301,7 +1335,13 @@ export function NFTProvider(props: NFTProviderProps) { return {children}; } -export const useNFT = (): NFTContextState => useContext(NFTContext); +export const useNFT = (): NFTContextState => { + const context = useContext(NFTContext); + if (!context) { + throw new Error("useNFT must be used within an NFTProvider"); + } + return context; +}; ``` #### 2. Main Screen From 1de22c96d54cb5625dd01430535ab3f112238ba0 Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 26 Sep 2024 14:52:47 +0700 Subject: [PATCH 10/15] Small Fix + remove rn-fetch-blob because we don't need anymore. --- .../mobile/solana-mobile-dapps-with-expo.md | 173 +++++++++--------- 1 file changed, 88 insertions(+), 85 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index cc80ea5c8..8df617d42 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -336,7 +336,7 @@ Copy and paste the following into the newly created `eas.json`: ```json { "cli": { - "version": ">= 5.2.0" + "version": ">= 3.12.0" }, "build": { "development": { @@ -886,16 +886,6 @@ application using `process.env.EXPO_PUBLIC_PINATA_API`, unlike traditional with Expo. For more information on securely storing secrets, refer to the [Expo documentation on environment variables](https://docs.expo.dev/build-reference/variables/#importing-secrets-from-a-dotenv-file) -Lastly, install `rn-fetch-blob`. This package will help us grab images from the -device's URI scheme and turn them into Blobs we can the upload to -[Pinata Cloud](https://pinata.cloud/). - -Install it with the following: - -```bash -yarn add rn-fetch-blob -``` - #### 3. Final build Build and reinstall if you want to make sure it's all working. This is the last @@ -939,8 +929,8 @@ The app itself is relatively straightforward. The general flow is: `NFTProvider.tsx` will control the state with our custom `NFTProviderContext`. This should have the following fields: -- `metaplex: Metaplex | null` - Holds the metaplex object that we use to call - `fetch` and `create` +- `umi: Umi | null` - Holds the metaplex object that we use to call `fetch` and + `create` - `publicKey: PublicKey | null` - The NFT creator's public key - `isLoading: boolean` - Manages loading state - `loadedNFTs: (DigitalAsset)[] | null` - An array of the user's snapshot NFTs @@ -1009,39 +999,46 @@ const fetchNFTs = useCallback(async () => { 3. `createNFT` - This function will upload a file to Pinata Cloud, and then use `createNft` function from to create and mint an NFT to your wallet. This comes in three parts, uploading the image, uploading the metadata and then - minting the NFT. - - To upload to Pinata Cloud you need to init `PinataSDK` instance with - `pinataJwt` and `pinataGateway`. After that, you can interact with their - [API](https://docs.pinata.cloud/web3/sdk/getting-started) to perform uploads. + minting the NFT. To upload to Pinata Cloud, you can use their + [HTTP API endpoint](https://docs.pinata.cloud/api-reference/endpoint/upload-a-file), + allowing interaction with their API for file uploads. We'll create two helper functions for uploading the image and metadata separately, then tie them together into a single `createNFT` function: ```tsx const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; -async function uploadBase64(base64String: string) { +async function uploadImageFromURI(fileUri: string) { try { - const buffer = Buffer.from(base64String, "base64"); - const blob = new Blob([buffer]); - const file = new File([blob], "file"); - const data = new FormData(); - data.append("file", file); + console.log("fileURI", fileUri); + const form = new FormData(); + const randomFileName = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`; + + form.append("file", { + uri: Platform.OS === "android" ? fileUri : fileUri.replace("file://", ""), + type: "image/jpeg", // Adjust the type as necessary + name: randomFileName, // Adjust the name as necessary + }); - const upload = await fetch( - "https://api.pinata.cloud/pinning/pinFileToIPFS", - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, - }, - body: data, + const options = { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + "Content-Type": "multipart/form-data", }, + body: form, + }; + + const response = await fetch( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + options, ); - const uploadRes = await upload.json(); - return uploadRes; + const responseJson = await response.json(); + return responseJson; } catch (error) { - console.log(error); + console.error("Upload failed:", error); + } finally { + console.log("Upload process completed."); } } @@ -1050,29 +1047,33 @@ async function uploadMetadataJson( description: string, imageCID: string, ) { + const randomFileName = `metadata_${Date.now()}_${Math.floor(Math.random() * 10000)}.json`; const data = JSON.stringify({ pinataContent: { name, description, imageCID, }, + pinataMetadata: { + name: randomFileName, + }, }); - const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, }, body: data, }); - const resData = await res.json(); + return resData; } + const uploadImage = useCallback(async (fileUri: string): Promise => { - const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); - const upload = await uploadBase64(imageBytesInBase64); + const upload = await uploadImageFromURI(fileUri); return upload.IpfsHash; }, []); @@ -1082,13 +1083,6 @@ const uploadMetadata = useCallback( description: string, imageCID: string, ): Promise => { - const data = JSON.stringify({ - pinataContent: { - name, - description, - imageCID, - }, - }); const uploadRes = await uploadMetadataJson(name, description, imageCID); return uploadRes.IpfsHash; }, @@ -1153,14 +1147,14 @@ import React, { createContext, useCallback, useContext, + useEffect, useMemo, useState, } from "react"; -import RNFetchBlob from "rn-fetch-blob"; -import { useConnection } from "./ConnectionProvider"; import { useUmi } from "./UmiProvider"; import { useMobileWallet } from "../utils/useMobileWallet"; import { Account, useAuthorization } from "./AuthorizationProvider"; +import { Platform } from "react-native"; export interface NFTProviderProps { children: ReactNode; @@ -1185,73 +1179,88 @@ const NFTContext = createContext(null); export function NFTProvider(props: NFTProviderProps) { const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; - const { connection } = useConnection(); - const { authorizeSession, deauthorizeSession } = useAuthorization(); const [account, setAccount] = useState(null); const [nftOfTheDay, setNftOfTheDay] = useState(null); const [loadedNFTs, setLoadedNFTs] = useState(null); const [isLoading, setIsLoading] = useState(false); const umi = useUmi(); const { children } = props; - const { connect } = useMobileWallet(); - async function uploadBase64(base64String: string) { + + async function uploadImageFromURI(fileUri: string) { try { - const buffer = Buffer.from(base64String, "base64"); - const blob = new Blob([buffer]); - const file = new File([blob], "file"); - const data = new FormData(); - data.append("file", file); + console.log("fileURI", fileUri); + const form = new FormData(); + const randomFileName = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`; + + // @ts-ignore + form.append("file", { + uri: + Platform.OS === "android" ? fileUri : fileUri.replace("file://", ""), + type: "image/jpeg", // Adjust the type as necessary + name: randomFileName, // Adjust the name as necessary + }); - const upload = await fetch( - "https://api.pinata.cloud/pinning/pinFileToIPFS", - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, - }, - body: data, + const options = { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + "Content-Type": "multipart/form-data", }, + body: form, + }; + + const response = await fetch( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + options, ); - const uploadRes = await upload.json(); - return uploadRes; + const responseJson = await response.json(); + console.log(responseJson.IpfsHash); + + return responseJson; } catch (error) { - console.log(error); + console.error("Upload failed:", error); + } finally { + console.log("Upload process completed."); } } async function uploadMetadataJson( - name: string, - description: string, - imageCID: string, + name = "Pinnie", + description = "A really sweet NFT of Pinnie the Pinata", + imageCID = "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4", ) { + const randomFileName = `metadata_${Date.now()}_${Math.floor(Math.random() * 10000)}.json`; const data = JSON.stringify({ pinataContent: { name, description, imageCID, }, + pinataMetadata: { + name: randomFileName, + }, }); - const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, }, body: data, }); - const resData = await res.json(); + return resData; } const fetchNFTs = useCallback(async () => { if (!umi || !account || isLoading) return; setIsLoading(true); - try { const creatorPublicKey = fromWeb3JsPublicKey(account.publicKey); + console.log("Creator", creatorPublicKey); const nfts = await fetchAllDigitalAssetByCreator(umi, creatorPublicKey); setLoadedNFTs(nfts); } catch (error) { @@ -1262,8 +1271,7 @@ export function NFTProvider(props: NFTProviderProps) { }, [umi, account, isLoading]); const uploadImage = useCallback(async (fileUri: string): Promise => { - const imageBytesInBase64 = await RNFetchBlob.fs.readFile(fileUri, "base64"); - const upload = await uploadBase64(imageBytesInBase64); + const upload = await uploadImageFromURI(fileUri); return upload.IpfsHash; }, []); @@ -1273,13 +1281,6 @@ export function NFTProvider(props: NFTProviderProps) { description: string, imageCID: string, ): Promise => { - const data = JSON.stringify({ - pinataContent: { - name, - description, - imageCID, - }, - }); const uploadRes = await uploadMetadataJson(name, description, imageCID); return uploadRes.IpfsHash; }, @@ -1294,6 +1295,7 @@ export function NFTProvider(props: NFTProviderProps) { console.log(`Creating NFT...`); const imageCID = await uploadImage(fileUri); const metadataCID = await uploadMetadata(name, description, imageCID); + console.log(metadataCID); const mint = generateSigner(umi); const transaction = createNft(umi, { mint, @@ -1302,6 +1304,7 @@ export function NFTProvider(props: NFTProviderProps) { sellerFeeBasisPoints: percentAmount(0), }); await transaction.sendAndConfirm(umi); + console.log("Hello 999 anh em"); const createdNft = await fetchDigitalAsset(umi, mint.publicKey); setNftOfTheDay(createdNft); } catch (error) { From a1b1137f948bb5790717105e4b490203caac686d Mon Sep 17 00:00:00 2001 From: XuananLe Date: Thu, 26 Sep 2024 15:04:21 +0700 Subject: [PATCH 11/15] Remove redundant console.log() --- content/courses/mobile/solana-mobile-dapps-with-expo.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 8df617d42..1de859971 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -662,7 +662,6 @@ export const UmiProvider = ({ children: ReactNode; }) => { const { selectedAccount } = useAuthorization(); - console.log("selectedAccount", JSON.stringify(selectedAccount, null, 2)); const umi = createUmi(endpoint) .use(mplTokenMetadata()) .use(mplCandyMachine()); @@ -1010,7 +1009,6 @@ const fetchNFTs = useCallback(async () => { const ipfsPrefix = `https://${process.env.EXPO_PUBLIC_NFT_PINATA_GATEWAY_URL}/ipfs/`; async function uploadImageFromURI(fileUri: string) { try { - console.log("fileURI", fileUri); const form = new FormData(); const randomFileName = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`; @@ -1189,7 +1187,6 @@ export function NFTProvider(props: NFTProviderProps) { async function uploadImageFromURI(fileUri: string) { try { - console.log("fileURI", fileUri); const form = new FormData(); const randomFileName = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`; @@ -1260,7 +1257,6 @@ export function NFTProvider(props: NFTProviderProps) { setIsLoading(true); try { const creatorPublicKey = fromWeb3JsPublicKey(account.publicKey); - console.log("Creator", creatorPublicKey); const nfts = await fetchAllDigitalAssetByCreator(umi, creatorPublicKey); setLoadedNFTs(nfts); } catch (error) { @@ -1295,7 +1291,6 @@ export function NFTProvider(props: NFTProviderProps) { console.log(`Creating NFT...`); const imageCID = await uploadImage(fileUri); const metadataCID = await uploadMetadata(name, description, imageCID); - console.log(metadataCID); const mint = generateSigner(umi); const transaction = createNft(umi, { mint, @@ -1304,7 +1299,6 @@ export function NFTProvider(props: NFTProviderProps) { sellerFeeBasisPoints: percentAmount(0), }); await transaction.sendAndConfirm(umi); - console.log("Hello 999 anh em"); const createdNft = await fetchDigitalAsset(umi, mint.publicKey); setNftOfTheDay(createdNft); } catch (error) { From fd7d04ddf6030a128790720f919e439f6b9ce3e6 Mon Sep 17 00:00:00 2001 From: XuananLe Date: Fri, 27 Sep 2024 01:18:24 +0700 Subject: [PATCH 12/15] Small Fix Env variable --- .../mobile/solana-mobile-dapps-with-expo.md | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 1de859971..6dd775fac 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -879,10 +879,11 @@ Create both files, in the root of your directory and add `.env` to your `.gitignore` file. Next, add your API key to the `.env` file with the variable name -`EXPO_PUBLIC_PINATA_API`. This allows you to securely access your API key in the -application using `process.env.EXPO_PUBLIC_PINATA_API`, unlike traditional -`import "dotenv/config"` which may require additional polyfills when working -with Expo. For more information on securely storing secrets, refer to the +`EXPO_PUBLIC_NFT_PINATA_JWT`. This allows you to securely access your API key in +the application using `process.env.EXPO_PUBLIC_NFT_PINATA_JWT`, unlike +traditional `import "dotenv/config"` which may require additional polyfills when +working with Expo. For more information on securely storing secrets, refer to +the [Expo documentation on environment variables](https://docs.expo.dev/build-reference/variables/#importing-secrets-from-a-dotenv-file) #### 3. Final build @@ -1183,8 +1184,17 @@ export function NFTProvider(props: NFTProviderProps) { const [isLoading, setIsLoading] = useState(false); const umi = useUmi(); const { children } = props; - const { connect } = useMobileWallet(); + const connect = () => { + if (isLoading) return; + setIsLoading(true); + transact(async wallet => { + const auth = await authorizeSession(wallet); + setAccount(auth); + }).finally(() => { + setIsLoading(false); + }); + }; async function uploadImageFromURI(fileUri: string) { try { const form = new FormData(); From b49cde671a9c2616ed0b24daa63d717e82ece37d Mon Sep 17 00:00:00 2001 From: XuananLe Date: Mon, 30 Sep 2024 18:02:12 +0700 Subject: [PATCH 13/15] Update Docs + Small fix --- .../mobile/solana-mobile-dapps-with-expo.md | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 6dd775fac..5fc39d7b8 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -103,7 +103,7 @@ the following inside this file: ```json { "cli": { - "version": ">= 3.12.0" + "version": ">= 5.12.0" }, "build": { "development": { @@ -121,7 +121,7 @@ the following inside this file: } ``` -With the EAS configuration file in place, you can build your project using the +With the EAS configuration file in place, you can build your project using `eas build`. This submits a job to the EAS Build service, where your APK is built using Expo's cloud infrastructure. If you want to build locally, you can add the `--local` flag. For example, the following command builds the project @@ -441,12 +441,13 @@ copying over `components/AuthorizationProvider.tsx` and `components/ConnectionProvider.tsx`. These files provide us with a `Connection` object as well as some helper functions that authorize our dapp. -Create file `components/AuthorizationProvider.tsx` and copy the contents -[of our existing Auth Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/AuthorizationProvider.tsx) +Create file `components/AuthorizationProvider.tsx` and copy the contents of +[our existing Auth Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/AuthorizationProvider.tsx) into the new file. Secondly, create file `components/ConnectionProvider.tsx` and copy the contents -[of our existing Connection Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/ConnectionProvider.tsx) +of +[our existing Connection Provider from Github](https://raw.githubusercontent.com/solana-developers/mobile-apps-with-expo/main/components/ConnectionProvider.tsx) into the new file. Now let's create a boilerplate for our main screen in `screens/MainScreen.tsx`: @@ -464,10 +465,10 @@ export function MainScreen() { } ``` -Next, create file called `polyfills.ts` for react-native to work with all solana -dependencies +Next, create file called `polyfills.ts` for react-native to work with all the +solana dependencies -```typescript +```typescript filename="polyfills.ts" import { getRandomValues as expoCryptoGetRandomValues } from "expo-crypto"; import { Buffer } from "buffer"; @@ -765,7 +766,8 @@ export default function App() { config={{ commitment: "processed" }} > - + + @@ -1057,16 +1059,19 @@ async function uploadMetadataJson( name: randomFileName, }, }); - const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + const response = await fetch( + "https://api.pinata.cloud/pinning/pinJSONToIPFS", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, }, - body: data, - }); - const resData = await res.json(); + ); + const responseBody = await response.json(); return resData; } @@ -1082,8 +1087,12 @@ const uploadMetadata = useCallback( description: string, imageCID: string, ): Promise => { - const uploadRes = await uploadMetadataJson(name, description, imageCID); - return uploadRes.IpfsHash; + const uploadResponse = await uploadMetadataJson( + name, + description, + imageCID, + ); + return uploadResponse.IpfsHash; }, [], ); @@ -1200,6 +1209,7 @@ export function NFTProvider(props: NFTProviderProps) { const form = new FormData(); const randomFileName = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`; + // In React Native, especially when working with form data and files, you may need to send files using an object that contains a URI (file path), especially on Android and iOS platforms. However, this structure may not be recognized by TypeScript's strict type checking // @ts-ignore form.append("file", { uri: @@ -1233,8 +1243,8 @@ export function NFTProvider(props: NFTProviderProps) { } async function uploadMetadataJson( - name = "Pinnie", - description = "A really sweet NFT of Pinnie the Pinata", + name = "Solanify", + description = "A truly sweet NFT of your day.", imageCID = "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4", ) { const randomFileName = `metadata_${Date.now()}_${Math.floor(Math.random() * 10000)}.json`; @@ -1248,16 +1258,19 @@ export function NFTProvider(props: NFTProviderProps) { name: randomFileName, }, }); - const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + const response = await fetch( + "https://api.pinata.cloud/pinning/pinJSONToIPFS", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: `Bearer ${process.env.EXPO_PUBLIC_NFT_PINATA_JWT}`, + }, + body: data, }, - body: data, - }); - const resData = await res.json(); + ); + const responseBody = await response.json(); return resData; } @@ -1287,8 +1300,12 @@ export function NFTProvider(props: NFTProviderProps) { description: string, imageCID: string, ): Promise => { - const uploadRes = await uploadMetadataJson(name, description, imageCID); - return uploadRes.IpfsHash; + const uploadResponse = await uploadMetadataJson( + name, + description, + imageCID, + ); + return uploadResponse.IpfsHash; }, [], ); From 3053af2f0016c4460679dc482b2603f87cbcf796 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Wed, 2 Oct 2024 12:07:31 +1000 Subject: [PATCH 14/15] Update content/courses/mobile/solana-mobile-dapps-with-expo.md --- content/courses/mobile/solana-mobile-dapps-with-expo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index 46a4c9a36..bbc57687b 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -1272,7 +1272,7 @@ export function NFTProvider(props: NFTProviderProps) { ); const responseBody = await response.json(); - return resData; + return responseBody; } const fetchNFTs = useCallback(async () => { From a84b16a07fdb17fa2752eb3a2fc4b18b4e6bb67a Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Wed, 2 Oct 2024 12:07:49 +1000 Subject: [PATCH 15/15] Update content/courses/mobile/solana-mobile-dapps-with-expo.md --- content/courses/mobile/solana-mobile-dapps-with-expo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/mobile/solana-mobile-dapps-with-expo.md b/content/courses/mobile/solana-mobile-dapps-with-expo.md index bbc57687b..90d1bece8 100644 --- a/content/courses/mobile/solana-mobile-dapps-with-expo.md +++ b/content/courses/mobile/solana-mobile-dapps-with-expo.md @@ -1073,7 +1073,7 @@ async function uploadMetadataJson( ); const responseBody = await response.json(); - return resData; + return responseBody; } const uploadImage = useCallback(async (fileUri: string): Promise => {