├── .devcontainer ├── .cargo-bpf │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── Dockerfile ├── README.md ├── beginner-crash-course │ └── devcontainer.json ├── deep-dive-into-programs │ └── devcontainer.json ├── ship-an-xnft │ └── devcontainer.json └── storefront-solanapay │ └── devcontainer.json ├── .docs ├── delhi.JPG ├── metacamp.jpeg ├── slides-icon.svg ├── solana-icon.png ├── usc.jpeg ├── usc2.jpg └── youtube-icon.png ├── .gitignore ├── .prettierignore ├── CONTRIBUTING.md ├── INSTRUCTORS.md ├── LICENSE ├── README.md ├── instructors └── notes │ ├── how-tos.md │ ├── metrics.md │ ├── prerequisites.md │ ├── recording.md │ └── workshop-format.md └── workshops ├── battle-royale └── README.md ├── beginner-crash-course ├── .docs │ ├── ServerRPCs.png │ ├── cookbook_qr.png │ ├── key.png │ ├── keypairs.jpeg │ ├── linux_files.png │ ├── serialization.png │ ├── sol.png │ ├── solpg_qr.png │ ├── wallet.jpg │ └── workshop_qr.png ├── CONDUCTING.md ├── README.md ├── solution │ ├── client-examples │ │ ├── .gitignore │ │ ├── assets │ │ │ ├── my-nft.json │ │ │ └── my-token.json │ │ ├── package.json │ │ ├── scripts │ │ │ ├── accounts.ts │ │ │ └── tokens.ts │ │ ├── util │ │ │ ├── local-configs.ts │ │ │ ├── log.ts │ │ │ └── transaction.ts │ │ └── yarn.lock │ ├── hello-world-again │ │ ├── .gitignore │ │ ├── package.json │ │ ├── program │ │ │ ├── Cargo.lock │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── tests │ │ │ ├── instructions.ts │ │ │ ├── test.ts │ │ │ └── tsconfig.test.json │ │ └── yarn.lock │ └── hello-world │ │ ├── .gitignore │ │ ├── package.json │ │ ├── program │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ │ ├── tests │ │ ├── test.ts │ │ └── tsconfig.test.json │ │ └── yarn.lock └── starter │ ├── client-examples │ ├── .gitignore │ ├── assets │ │ ├── my-nft.json │ │ └── my-token.json │ ├── package.json │ ├── scripts │ │ ├── accounts.ts │ │ └── tokens.ts │ ├── util │ │ ├── local-configs.ts │ │ ├── log.ts │ │ └── transaction.ts │ └── yarn.lock │ ├── hello-world-again │ ├── .gitignore │ ├── package.json │ ├── program │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── tests │ │ ├── instructions.ts │ │ ├── test.ts │ │ └── tsconfig.test.json │ └── yarn.lock │ └── hello-world │ ├── .gitignore │ ├── package.json │ ├── program │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── tests │ ├── test.ts │ └── tsconfig.test.json │ └── yarn.lock ├── deep-dive-into-programs └── README.md ├── getting-started-101 ├── CONDUCTING.md └── README.md ├── nft-minter ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github │ ├── dependabot.yml │ └── workflows │ │ └── frontend.yml ├── .gitignore ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── solanaLogo.png │ └── vercel.svg ├── src │ ├── components │ │ ├── AppBar.tsx │ │ ├── ContentContainer.tsx │ │ ├── Footer.tsx │ │ ├── NetworkSwitcher.tsx │ │ ├── NftMinter.tsx │ │ ├── Notification.tsx │ │ ├── RequestAirdrop.tsx │ │ ├── SendTransaction.tsx │ │ ├── SendVersionedTransaction.tsx │ │ ├── SignMessage.tsx │ │ ├── Text │ │ │ └── index.tsx │ │ └── nav-element │ │ │ └── index.tsx │ ├── contexts │ │ ├── AutoConnectProvider.tsx │ │ ├── ContextProvider.tsx │ │ └── NetworkConfigurationProvider.tsx │ ├── hooks │ │ └── useQueryContext.tsx │ ├── models │ │ └── types.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── upload.ts │ │ └── index.tsx │ ├── stores │ │ ├── useNotificationStore.tsx │ │ └── useUserSOLBalanceStore.tsx │ ├── styles │ │ └── globals.css │ ├── utils │ │ ├── explorer.ts │ │ ├── index.tsx │ │ ├── metaplex.ts │ │ └── notifications.tsx │ └── views │ │ └── index.tsx ├── tailwind.config.js ├── tsconfig.json └── yarn.lock ├── ship-an-xnft ├── README.md ├── code.md ├── solution │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── app.json │ ├── assets │ │ └── icon.png │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ └── screens │ │ │ ├── FriendsScreen.tsx │ │ │ └── HomeScreen.tsx │ ├── template.html │ ├── tsconfig.json │ └── webpack.config.js └── starter │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── app.json │ ├── assets │ └── icon.png │ ├── babel.config.js │ ├── package.json │ ├── src │ └── App.tsx │ ├── template.html │ ├── tsconfig.json │ └── webpack.config.js ├── solana-journal └── README.md ├── solana-twitter └── README.md ├── storefront-solanapay ├── .gitignore ├── CONDUCTING.md ├── README.md ├── demo │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── WORK_NOTES.md │ ├── app │ │ ├── globals.css │ │ ├── head.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ └── api │ │ │ └── transaction.ts │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── next.svg │ │ ├── pizza.png │ │ ├── pizzeria.jpeg │ │ ├── pizzeria.jpg │ │ ├── thirteen.svg │ │ └── vercel.svg │ ├── src │ │ ├── components │ │ │ └── PayQR.tsx │ │ ├── idl │ │ │ └── pizza_program.ts │ │ └── util │ │ │ ├── const.ts │ │ │ └── order.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── yarn.lock └── program │ ├── .gitignore │ ├── .prettierignore │ ├── Anchor.toml │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── package.json │ ├── programs │ └── pizza_program │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src │ │ ├── dot │ │ ├── mod.rs │ │ └── program.rs │ │ └── lib.rs │ ├── programs_py │ ├── pizza_program.py │ └── seahorse │ │ ├── __init__.py │ │ └── prelude.py │ ├── tests │ └── pizza_program.ts │ ├── tsconfig.json │ └── yarn.lock ├── svg-generator └── README.md ├── token-swap └── README.md └── tug-of-war ├── .gitignore ├── .vscode └── launch.json ├── CONDUCTING.md ├── README.md ├── next ├── .eslintrc.json ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── WORK_NOTES.md ├── app │ ├── globals.css │ ├── head.tsx │ ├── layout.tsx │ └── page.tsx ├── next.config.js ├── package.json ├── pages │ └── api │ │ └── transaction.ts ├── postcss.config.js ├── public │ ├── bg.jpg │ ├── favicon.ico │ ├── icon.png │ ├── next.svg │ ├── thirteen.svg │ └── vercel.svg ├── src │ ├── Wallet.tsx │ ├── components │ │ └── PayQR.tsx │ └── util │ │ ├── const.ts │ │ ├── move.ts │ │ └── transfer.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock └── program ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── package.json ├── src └── lib.rs ├── tests └── tug-of-war.ts └── yarn.lock /.devcontainer/.cargo-bpf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bpf-setup" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "lib"] -------------------------------------------------------------------------------- /.devcontainer/.cargo-bpf/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn add(left: usize, right: usize) -> usize { 2 | left + right 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::*; 8 | 9 | #[test] 10 | fn it_works() { 11 | let result = add(2, 2); 12 | assert_eq!(result, 4); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt update && apt upgrade -y 4 | RUN apt install -y bash bzip2 wget curl gcc-multilib libssl-dev 5 | RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - 6 | RUN apt-get install -y nodejs 7 | RUN npm i -g yarn 8 | RUN apt update && apt upgrade -y 9 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q 10 | RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb 11 | RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb 12 | RUN wget -o solana-release.tar.bz2 https://github.com/solana-labs/solana/releases/download/v1.14.13/solana-release-x86_64-unknown-linux-gnu.tar.bz2 13 | RUN tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2 14 | RUN mv solana-release /root/solana-release 15 | 16 | ENV HOME=/root 17 | ENV PATH=$HOME/.cargo/bin:$HOME/solana-release/bin:$PATH 18 | 19 | COPY .cargo-bpf /tmp/bpf-setup 20 | WORKDIR /tmp/bpf-setup 21 | RUN cargo build-bpf && cargo build-sbf 22 | WORKDIR / 23 | RUN rm -rf /tmp/bpf-setup 24 | 25 | RUN solana config set -ud 26 | 27 | ENTRYPOINT solana-keygen new --no-bip39-passphrase -s -o $HOME/.config/solana/id.json &&\ 28 | solana config set --keypair $HOME/.config/solana/id.json &&\ 29 | (solana airdrop 2 || sh) &&\ 30 | sh -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # Workshop Dev Containers 2 | 3 | ## The Workshop Images 4 | 5 | > If you're building any of these images on Mac or Windows (not WSL), you'll have to add `--platform linux/amd64` to your `docker build` command. -------------------------------------------------------------------------------- /.devcontainer/beginner-crash-course/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beginner-crash-course", 3 | "image": "docker.io/solanadevelopers/solana-workshop-image:0.0.1", 4 | "workspaceFolder": "/workspaces/workshops/workshops/beginner-crash-course/starter", 5 | "features": { 6 | } 7 | } -------------------------------------------------------------------------------- /.devcontainer/deep-dive-into-programs/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-dive-into-programs", 3 | "image": "docker.io/solanadevelopers/solana-workshop-image:0.0.1", 4 | "workspaceFolder": "/workspaces/workshops/workshops/deep-dive-into-programs/starter", 5 | "features": { 6 | } 7 | } -------------------------------------------------------------------------------- /.devcontainer/ship-an-xnft/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ship-an-xnft", 3 | "image": "docker.io/solanadevelopers/solana-workshop-image:0.0.1", 4 | "workspaceFolder": "/workspaces/workshops/workshops/ship-an-xnft/starter", 5 | "features": { 6 | } 7 | } -------------------------------------------------------------------------------- /.devcontainer/storefront-solanapay/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storefront-solanapay", 3 | "image": "docker.io/solanadevelopers/solana-workshop-image:0.0.1", 4 | "workspaceFolder": "/workspaces/workshops/workshops/storefront-solanapay/starter", 5 | "features": { 6 | } 7 | } -------------------------------------------------------------------------------- /.docs/delhi.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/delhi.JPG -------------------------------------------------------------------------------- /.docs/metacamp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/metacamp.jpeg -------------------------------------------------------------------------------- /.docs/slides-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.docs/solana-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/solana-icon.png -------------------------------------------------------------------------------- /.docs/usc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/usc.jpeg -------------------------------------------------------------------------------- /.docs/usc2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/usc2.jpg -------------------------------------------------------------------------------- /.docs/youtube-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/.docs/youtube-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /*/**/.DS_Store 3 | test-ledger/ 4 | /*/**/test-ledger/ 5 | .vercel 6 | workshops/tug-of-war/next/.env 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all Markdown files: 2 | *.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **🔑 🔑 The [Solana Beginner Crash Course](./workshops/beginner-crash-course) workshop was just recently completed. Let this serve as a template for all future workshops, including associated documentation and recordings.** 4 | 5 | --- 6 | 7 | These workshops serve as great real estate for plucking reference examples on how to do specific things on Solana for [Solana Cookbook](https://solanacookbook.com/). 8 | 9 | To read how you can pluck reference examples from these workshops, see the [Plucking Reference Examples](#plucking-reference-examples) section below. 10 | 11 | > Whether you're creating a new workshop or updating an existing one, submit a PR for review. 12 | 13 | ## 🚀 Creating New Workshops 14 | Feel free to create new workshops in this repository using the following branch format: 15 | * `new//` 16 | * Example: `new/realbuffalojoe/svg-generator` 17 | 18 | ## 🔧 Modifying Existing Workshops 19 | There are 2 types of changes you can make to existing workshops, and you must follow the below branch format: 20 | 1. 🕷️ Bugfixes: `/bugfix//` 21 | * Example: Fixing UI rendering on Battle Royale: `battle-royale/bugfix/realbuffalojoe/fixing-ui-rendering` 22 | 2. 💡 Features: `/feature//` 23 | * Example: Adding SPL tokens to Solana Twitter: `solana-twitter/feature/realbuffalojoe/adding-spl-tokens` 24 | 25 | ## 🗒️ Plucking Reference Examples 26 | If you spot some code within any of these workshops that you think would serve as a good reference example for Solana Cookbook, you can submit an issue describing what you want to see and add the **(from workshop)** label -------------------------------------------------------------------------------- /INSTRUCTORS.md: -------------------------------------------------------------------------------- 1 | # Workshop Instructors 2 | 3 | Notes on conducting workshops can be found within each workshop in the `CONDUCTING.md` file. 4 | 5 | ### Sharing Results & Feedback 6 | 7 | Please share any results, feedback, or ideas in the [`instructors/` folder](./instructors/). 8 | 9 | We want to collaborate on the following concepts: 10 | * Which workshops had the most impact 11 | * The best format for conducting workshops 12 | * The best format for recording workshops 13 | * How we can track metrics on workshop engagement, post-workshop engagement, etc. 14 | * How we can determine new workshop ideas 15 | * and more -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Solana Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Workshops 2 | 3 | 🧑‍🚀 Hello fren, have you attended any of these workshops? 4 | 5 | 10 | 15 | 20 | 25 | 26 | --- 27 | 28 | ### ✏️ Contributing 29 | Please see [CONTRIBUTING](./CONTRIBUTING.md) for instructions on contributing new workshops or modifying existing ones. 30 | 31 | ### 📗 Learning 32 | You can find details in each workshop's `README.md` for lessons you can learn from each workshop. 33 | 34 | --- 35 | 36 | | Workshop | Description | Author | 37 | | -------- | ----------- | ------ | 38 | | [✏️ Getting Started with Solana 101](./workshops/getting-started-101) | A non-technical presentation on Solana basics. | Colin Ogoo | 39 | | [🏎️ Solana for Beginners Crash Course](./workshops/beginner-crash-course) | A crash course for beginners. Covers basics such as transactions, accounts, tokens, and a HelloWorld smart contract (program). | Joe Caulfield | 40 | | [⚙️ Deep Dive into Writing Solana Programs](./workshops/deep-dive-into-programs) | An extensive look at writing Solana Programs in Rust, with a preview of different frameworks like Anchor and Seahorse. | Joe Caulfield | 41 | | [🎑 NFT Minter](./workshops/nft-minter) | A simple web page for uploading an image and minting it into an NFT. | Joe Caulfield | 42 | | [🎆 SVG Generator](./workshops/svg-generator) | Rendering an SVG image from randomized strings stored in a Solana on-chain account. | Courtney Jensen & Joe Caulfield | 43 | | [📓 Solana Journal](./workshops/solana-journal) | A simple journal entry dApp writing entries to the Solana blockchain. | Joe Caulfield | 44 | | [🐦 Solana Twitter](./workshops/solana-twitter) | A mock Twitter social media site built on top of on-chain data. | Joe Caulfield | 45 | | [🪓 Battle Royale](./workshops/battle-royale) | A fun little game leveraging on-chain data. | Joe Caulfield | 46 | | [🪙 Token Swap](./workshops/token-swap) | Token swapping platform mimicing a DEX. | Joe Caulfield | 47 | | [🍕 Building a Storefront with Solana Pay](./workshops/storefront-solanapay) | A storefront dApp capable of taking payments of SPL tokens using the Solana Pay SDK and Transaction Requests | Joe Caulfield
Valentin Madrid | 48 | | [☄️ Ship Your First xNFT](./workshops/ship-an-xnft) | Building an xNFT and deploying it to Backpack | Valentin Madrid | 49 | -------------------------------------------------------------------------------- /instructors/notes/how-tos.md: -------------------------------------------------------------------------------- 1 | ### Importing Code from GitHub into SolPG 2 | 3 | You can import code from GitHub into SolPG by clicking the little GitHub icon in the top left-hand corner. 4 | 5 | > Sometimes you may need to create a new project to see this icon. 6 | 7 | Once you click the icon, SolPG will give you instructions on how to import different types of programs into SolPG. 8 | 9 | The key is to make sure you're importing the url that points to the root directory of your Rust code. -------------------------------------------------------------------------------- /instructors/notes/metrics.md: -------------------------------------------------------------------------------- 1 | # 📊 Metrics 2 | 3 | We want to collect feedback & track retention/engagement after we’ve conducted these workshops. 4 | 5 | Let’s keep this simple for now: 6 | * If we have everyone fork the stripped repo, we can track forks during the workshop & look at commits on those forks 7 | * If we have participants submit homework features, we can have our submission app track how many wallets & program IDs submitted new Program IDs. 8 | * If we send out a simple form at the end of the event - HH, hackathon, etc. - to collect anyone’s feedback I don’t think that’s much of an ask either. -------------------------------------------------------------------------------- /instructors/notes/prerequisites.md: -------------------------------------------------------------------------------- 1 | # 💼 Prerequisites for Workshop Participants 2 | 3 | Below are some prerequisites that could be shared with any upcoming workshop audiences before the session. 4 | 5 | All resources can be found [here](). 6 | 7 | ### 👝 Wallets 8 | * [👻 Phantom](https://phantom.app/) 9 | * [☄️ Solflare](https://solflare.com/) 10 | * [☀️ Glow](https://glow.app/) 11 | 12 | ### 💻 Dev Tools 13 | * 🏖️ [Solana Playground](https://beta.solpg.io/) 14 | * 🌐 [NodeJS](https://nodejs.org/en/download/) 15 | * ⚙️ [Rust](https://rustup.rs/) 16 | * 👾 [Solana](https://docs.solana.com/cli/install-solana-cli-tools) 17 | 18 | ### 📚 Resources 19 | * [📖 Solana Core Docs](https://docs.solana.com) 20 | * [📘 Solana Cookbook](https://solanacookbook.com/) 21 | * [📕 Program Examples](https://github.com/solana-developers/program-examples) 22 | * [📗 Web3 Examples](https://github.com/solana-developers/web3-examples) 23 | * [🎬 Solana Bytes](https://www.youtube.com/playlist?list=PLilwLeBwGuK51Ji870apdb88dnBr1Xqhm) 24 | * [🎬 Coding & Crypto](https://www.youtube.com/playlist?list=PLUBKxx7QjtVnU3hkPc8GF1Jh4DE7cf4n1) 25 | 26 | ### 🎓 Courses 27 | * 🚀 [Buildspace Core](https://buildspace.so/solana-core) 28 | * 📦 [Unboxed Software](https://github.com/Unboxed-Software/solana-course) 29 | -------------------------------------------------------------------------------- /instructors/notes/recording.md: -------------------------------------------------------------------------------- 1 | # 🎥 Recording Workshops 2 | 3 | When recording workshops, we want to keep in mind the following requirements: 4 | * Have a good camera for your headshot 5 | * Have a good microphone for clear, clean audio 6 | * Record using a platform such as OBS where you can feature your screen as the main capture, with a headshot recording in one of the corners of the screen 7 | 8 | You can choose how to record your workshops. Ideally, all live sessions where workshops are conducted should be recorded, but you can optionally also record a scripted version without an audience. -------------------------------------------------------------------------------- /instructors/notes/workshop-format.md: -------------------------------------------------------------------------------- 1 | # Workshop Format 2 | 3 | We can choose to experiment with various forms of workshops, such as: 4 | 5 | - Q&A format with low to no coding 6 | - Step-by-step workshops where different branches show different stages of the build 7 | - ie. branches: `step-1`, `step-2`, `step-3`, `final` 8 | - Interactive dApps hosted on Vercel that participants can play with 9 | 10 | Feel free to brainstorm new workshops, try things out, and jam on ideas within this page. 11 | -------------------------------------------------------------------------------- /workshops/battle-royale/README.md: -------------------------------------------------------------------------------- 1 | # Battle Royale 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | Coming soon! | Coming soon! | 7 | 8 | ## 📗 Learn -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/ServerRPCs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/ServerRPCs.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/cookbook_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/cookbook_qr.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/key.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/keypairs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/keypairs.jpeg -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/linux_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/linux_files.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/serialization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/serialization.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/sol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/sol.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/solpg_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/solpg_qr.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/wallet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/wallet.jpg -------------------------------------------------------------------------------- /workshops/beginner-crash-course/.docs/workshop_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/beginner-crash-course/.docs/workshop_qr.png -------------------------------------------------------------------------------- /workshops/beginner-crash-course/CONDUCTING.md: -------------------------------------------------------------------------------- 1 | # How to Conduct This Workshop 2 | 3 | ### [youtube Video Tutorial](https://www.youtube.com/watch?v=u1HyjeBs3xk) 4 | 5 | ### [slides Presentation Slides](https://docs.google.com/presentation/d/1itl6GW7P8NmpALKKAki5q4FdQp75FNh1/edit?usp=sharing&ouid=110179409255229051861&rtpof=true&sd=true) 6 | 7 | ## 📋 Step-By-Step Tutorial 8 | 9 | First thing's first, make sure people have the proper local configurations: 10 | * 🌐 [NodeJS](https://nodejs.org/en/download/) 11 | * 👾 [Solana](https://docs.solana.com/cli/install-solana-cli-tools) 12 | 13 | Optionally, they can have Rust installed, but if not they can use Solana Playground: 14 | * ⚙️ [Rust](https://rustup.rs/) 15 | * 🏖️ [Solana Playground](https://beta.solpg.io/) 16 | 17 | Open up the slide deck [linked above](#img-srcdocsslides-iconsvg-altslides-width20-aligncenter-presentation-slideshttpsdocsgooglecompresentationd1itl6gw7p8nmpalkkaki5q4fdqp75fnh1edituspsharingouid110179409255229051861rtpoftruesdtrue) and share your screen. 18 | 19 | Open up the source code on-screen - you can either use GitHub or VS Code. 20 | 21 | Now step through the workshop's [README](./README.md) and display the slides on-screen. 22 | 23 | You'll just want to explain whatever you're covering as you go, and whenever you want to show code flip from the slide deck to the source code. 24 | 25 | **→ Running Code Locally:** 26 | Participants can run the code in the workshop locally, or in Solana Playground by just cloning the code or importing into SolPG. 27 | 28 | See [Importing Code from GitHub into SolPG](../../instructors/notes/how-tos.md#importing-code-from-github-into-solpg) in the instructor docs. 29 | 30 | ### 🏆 Challenges: 31 | * Instructions & Transactions 32 | * After learning how to conduct a `createAccount` instruction, build a transaction inside the `transfer` function to conduct a transfer of SOL between two parties. 33 | * Writing Programs 34 | * After learning how to write `hello-world`, build a transaction with an instruction that will invoke our program. 35 | * After learning how to write `hello-world-again` and serialize our `SayHello` instruction on the client-side, implement the client-side serialization of `SayGoodbye` to build a transaction with an instruction that will invoke our program's `SayGoodbye` operation. 36 | * Tokens 37 | * AFter learning how to create & mint tokens, build a transaction inside the `transfer` function to conduct a transfer of each type of SPL Token between two parties. -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/assets/my-nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Homer NFT", 3 | "symbol": "HOMR", 4 | "description": "Joe's custom NFT", 5 | "image": "https://www.nicepng.com/png/detail/552-5523019_homer-simpsons-png.png" 6 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/assets/my-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solana Gold", 3 | "symbol": "SGOLD", 4 | "description": "Joe's custom SPL token", 5 | "image": "https://toppng.com/uploads/preview/gold-coins-11530998393xtf85riude.png" 6 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@metaplex-foundation/mpl-token-metadata": "^2.5.2", 4 | "@solana/spl-token": "^0.3.7", 5 | "@solana/web3.js": "^1.73.0", 6 | "ts-node": "^10.9.1", 7 | "typescript": "^4.9.4" 8 | }, 9 | "scripts": { 10 | "accounts": "ts-node ./scripts/accounts.ts", 11 | "tokens": "ts-node ./scripts/tokens.ts" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/scripts/accounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | SystemProgram, 6 | } from "@solana/web3.js" 7 | import { 8 | loadKeypairFromFile 9 | } from '../util/local-configs' 10 | import { 11 | logAccountInfo, 12 | logBalance, 13 | logNewKeypair, 14 | logTransaction, 15 | newLogSection, 16 | } from '../util/log' 17 | import { 18 | buildTransaction 19 | } from '../util/transaction' 20 | 21 | 22 | const connection = new Connection( 23 | "https://api.devnet.solana.com", 24 | { 25 | commitment: 'confirmed', 26 | confirmTransactionInitialTimeout: 60000, 27 | }, 28 | ) 29 | 30 | const payer = loadKeypairFromFile( 31 | require('os').homedir() + '/.config/solana/id.json' 32 | ) 33 | 34 | const keypairA = Keypair.generate() 35 | const keypairB = Keypair.generate() 36 | 37 | async function createAccount(accountName: string, keypair: Keypair) { 38 | const lamports = 39 | await connection.getMinimumBalanceForRentExemption(0) + 100000 40 | const createAccountInstruction = SystemProgram.createAccount({ 41 | fromPubkey: payer.publicKey, 42 | newAccountPubkey: keypair.publicKey, 43 | lamports, 44 | space: 0, 45 | programId: SystemProgram.programId, 46 | }) 47 | const createAccountTransaction = await buildTransaction( 48 | connection, 49 | payer.publicKey, 50 | [payer, keypair], 51 | [createAccountInstruction] 52 | ) 53 | const signature = await connection.sendTransaction(createAccountTransaction) 54 | 55 | newLogSection() 56 | logNewKeypair(keypair) 57 | await logTransaction(connection, signature) 58 | await logBalance(accountName, connection, keypair.publicKey) 59 | } 60 | 61 | async function getAccountData(publicKey: PublicKey) { 62 | const accountInfo = await connection.getAccountInfo(publicKey) 63 | newLogSection() 64 | logAccountInfo(accountInfo) 65 | } 66 | 67 | async function transferSol(fromKeypair: Keypair, toPublicKey: PublicKey) { 68 | 69 | newLogSection() 70 | console.log("Starting balances:"); 71 | await logBalance("Account A", connection, keypairA.publicKey) 72 | await logBalance("Account B", connection, keypairB.publicKey) 73 | 74 | const transferInstruction = SystemProgram.transfer({ 75 | fromPubkey: fromKeypair.publicKey, 76 | toPubkey: toPublicKey, 77 | lamports: 90000, 78 | }) 79 | const transferTransaction = await buildTransaction( 80 | connection, 81 | fromKeypair.publicKey, 82 | [fromKeypair], 83 | [transferInstruction] 84 | ) 85 | const signature = await connection.sendTransaction(transferTransaction) 86 | await logTransaction(connection, signature) 87 | 88 | console.log("Ending balances:"); 89 | await logBalance("Account A", connection, keypairA.publicKey) 90 | await logBalance("Account B", connection, keypairB.publicKey) 91 | } 92 | 93 | async function accountsScript() { 94 | await createAccount("Account A", keypairA); 95 | await getAccountData(keypairA.publicKey); 96 | await createAccount("Account B", keypairB); 97 | await transferSol(keypairA, keypairB.publicKey); 98 | } 99 | 100 | accountsScript() 101 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/util/local-configs.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from "@solana/web3.js"; 2 | 3 | export function loadKeypairFromFile(path: string): Keypair { 4 | return Keypair.fromSecretKey( 5 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 6 | ) 7 | }; 8 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/util/log.ts: -------------------------------------------------------------------------------- 1 | import { Account, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; 2 | import { 3 | AccountInfo, 4 | Connection, 5 | Keypair, 6 | LAMPORTS_PER_SOL, 7 | PublicKey 8 | } from "@solana/web3.js" 9 | 10 | export function newLogSection() { 11 | console.log("-----------------------------------------------------") 12 | } 13 | 14 | export async function logAccountInfo(accountInfo: AccountInfo | null) { 15 | console.log("Account Info:") 16 | console.log(accountInfo) 17 | } 18 | 19 | export function logNewKeypair(keypair: Keypair) { 20 | console.log("Created a new keypair.") 21 | console.log(` New account Public Key: ${keypair.publicKey}`); 22 | } 23 | 24 | export async function logTransaction(connection: Connection, signature: string) { 25 | await connection.confirmTransaction(signature) 26 | console.log("Transaction successful.") 27 | console.log(` Transaction signature: ${signature}`); 28 | } 29 | 30 | export async function logBalance(accountName: string, connection: Connection, pubkey: PublicKey) { 31 | const balance = await connection.getBalance(pubkey) 32 | console.log(` ${accountName}:`); 33 | console.log(` Account Pubkey: ${pubkey.toString()} SOL`); 34 | console.log(` Account Balance: ${balance / LAMPORTS_PER_SOL} SOL`); 35 | } 36 | 37 | export function logNewMint(mintPubkey: PublicKey, decimals: number) { 38 | console.log("Created a new mint.") 39 | console.log(` New mint Public Key: ${mintPubkey}`); 40 | console.log(` Mint type: ${decimals === 0 ? 'NFT' : 'SPL Token'}`); 41 | } 42 | 43 | export async function logTokenBalance(accountName: string, associatedTokenAccount: Account) { 44 | console.log(` ${accountName}:`); 45 | console.log(` ATA Pubkey : ${associatedTokenAccount.address}`); 46 | console.log(` Mint : ${associatedTokenAccount.mint}`); 47 | console.log(` Token Balance : ${associatedTokenAccount.amount}`); 48 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/client-examples/util/transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | TransactionInstruction, 6 | TransactionMessage, 7 | VersionedTransaction 8 | } from "@solana/web3.js" 9 | 10 | 11 | export async function buildTransaction( 12 | connection: Connection, 13 | payer: PublicKey, 14 | signers: Keypair[], 15 | instructions: TransactionInstruction[] 16 | ): Promise { 17 | 18 | let blockhash = await connection 19 | .getLatestBlockhash() 20 | .then((res) => res.blockhash) 21 | 22 | const messageV0 = new TransactionMessage({ 23 | payerKey: payer, 24 | recentBlockhash: blockhash, 25 | instructions, 26 | }).compileToV0Message() 27 | 28 | const tx = new VersionedTransaction(messageV0) 29 | 30 | signers.forEach(s => tx.sign([s])) 31 | 32 | return tx 33 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | program/target/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "yarn run ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/test.ts" 4 | }, 5 | "dependencies": { 6 | "@solana/web3.js": "^1.47.3", 7 | "fs": "^0.0.1-security" 8 | }, 9 | "devDependencies": { 10 | "@types/bn.js": "^5.1.0", 11 | "@types/chai": "^4.3.1", 12 | "@types/mocha": "^9.1.1", 13 | "chai": "^4.3.4", 14 | "mocha": "^9.0.3", 15 | "ts-mocha": "^10.0.0", 16 | "typescript": "^4.3.5" 17 | } 18 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | borsh = "0.9.3" 8 | borsh-derive = "0.9.3" 9 | solana-program = "1.14.12" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{ BorshDeserialize, BorshSerialize }; 2 | use solana_program::{ 3 | account_info::{AccountInfo, next_account_info}, 4 | entrypoint, 5 | entrypoint::ProgramResult, 6 | msg, 7 | pubkey::Pubkey, 8 | }; 9 | 10 | 11 | #[derive(BorshSerialize, BorshDeserialize)] 12 | pub enum MyInstruction { 13 | SayHelloInstruction(SayHelloArgs), 14 | SayGoodbyeInstruction(SayGoodbyeArgs), 15 | } 16 | 17 | #[derive(BorshSerialize, BorshDeserialize)] 18 | pub struct SayHelloArgs { 19 | hello_message: String, 20 | } 21 | 22 | #[derive(BorshSerialize, BorshDeserialize)] 23 | pub struct SayGoodbyeArgs { 24 | goodbye_message: String, 25 | second_goodbye_message: String, 26 | } 27 | 28 | fn say_hello(args: SayHelloArgs) { 29 | msg!("{}", args.hello_message); 30 | } 31 | 32 | fn say_goodbye(args: SayGoodbyeArgs) { 33 | msg!("{}", args.goodbye_message); 34 | msg!("{}", args.second_goodbye_message); 35 | } 36 | 37 | 38 | entrypoint!(hello_world); 39 | 40 | fn hello_world( 41 | program_id: &Pubkey, 42 | accounts: &[AccountInfo], 43 | instruction_data: &[u8], 44 | ) -> ProgramResult { 45 | 46 | let instruction = MyInstruction::try_from_slice(&instruction_data)?; 47 | 48 | match instruction { 49 | MyInstruction::SayHelloInstruction(args) => say_hello(args), 50 | MyInstruction::SayGoodbyeInstruction(args) => say_goodbye(args), 51 | }; 52 | 53 | msg!("Our program's Program ID: {}", &program_id); 54 | 55 | let accounts_iter = &mut accounts.iter(); 56 | let payer = next_account_info(accounts_iter)?; 57 | 58 | msg!("Payer Address: {}", payer.key); 59 | 60 | Ok(()) 61 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/tests/instructions.ts: -------------------------------------------------------------------------------- 1 | import * as borsh from "borsh" 2 | import { Buffer } from "buffer" 3 | import { 4 | PublicKey, 5 | TransactionInstruction 6 | } from '@solana/web3.js' 7 | 8 | 9 | export enum MyInstruction { 10 | SayHello, 11 | SayGoodbye, 12 | } 13 | 14 | 15 | export class SayHello { 16 | 17 | instruction: MyInstruction 18 | hello_message: string 19 | 20 | constructor(props: { 21 | instruction: MyInstruction, 22 | hello_message: string, 23 | }) { 24 | this.instruction = props.instruction 25 | this.hello_message = props.hello_message 26 | } 27 | 28 | toBuffer() { 29 | return Buffer.from(borsh.serialize(SayHelloSchema, this)) 30 | } 31 | } 32 | 33 | export const SayHelloSchema = new Map([ 34 | [ SayHello, { 35 | kind: 'struct', 36 | fields: [ 37 | ['instruction', 'u8'], 38 | ['hello_message', 'string'], 39 | ], 40 | }] 41 | ]) 42 | 43 | export function createSayHelloInstruction( 44 | payer: PublicKey, 45 | programId: PublicKey, 46 | helloMessage: string, 47 | ): TransactionInstruction { 48 | 49 | const myInstructionObject = new SayHello({ 50 | instruction: MyInstruction.SayHello, 51 | hello_message: helloMessage, 52 | }) 53 | 54 | return new TransactionInstruction({ 55 | keys: [ 56 | {pubkey: payer, isSigner: true, isWritable: true}, 57 | ], 58 | programId: programId, 59 | data: myInstructionObject.toBuffer(), 60 | }) 61 | } 62 | 63 | 64 | export class SayGoodbye { 65 | 66 | instruction: MyInstruction 67 | goodbye_message: string 68 | second_goodbye_message: string 69 | 70 | constructor(props: { 71 | instruction: MyInstruction, 72 | goodbye_message: string, 73 | second_goodbye_message: string, 74 | }) { 75 | this.instruction = props.instruction 76 | this.goodbye_message = props.goodbye_message 77 | this.second_goodbye_message = props.second_goodbye_message 78 | } 79 | 80 | toBuffer() { 81 | return Buffer.from(borsh.serialize(SayGoodbyeSchema, this)) 82 | } 83 | } 84 | 85 | export const SayGoodbyeSchema = new Map([ 86 | [ SayGoodbye, { 87 | kind: 'struct', 88 | fields: [ 89 | ['instruction', 'u8'], 90 | ['goodbye_message', 'string'], 91 | ['second_goodbye_message', 'string'], 92 | ], 93 | }] 94 | ]) 95 | 96 | export function createSayGoodbyeInstruction( 97 | payer: PublicKey, 98 | programId: PublicKey, 99 | goodbyeMessage: string, 100 | secondGoodbyeMessage: string, 101 | ): TransactionInstruction { 102 | 103 | const myInstructionObject = new SayGoodbye({ 104 | instruction: MyInstruction.SayGoodbye, 105 | goodbye_message: goodbyeMessage, 106 | second_goodbye_message: secondGoodbyeMessage, 107 | }) 108 | 109 | return new TransactionInstruction({ 110 | keys: [ 111 | {pubkey: payer, isSigner: true, isWritable: true}, 112 | ], 113 | programId: programId, 114 | data: myInstructionObject.toBuffer(), 115 | }) 116 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | } from 'mocha' 5 | import { 6 | Connection, 7 | Keypair, 8 | sendAndConfirmTransaction, 9 | Transaction, 10 | } from '@solana/web3.js' 11 | import { createSayGoodbyeInstruction, createSayHelloInstruction } from './instructions' 12 | 13 | 14 | function loadKeypairFromFile(path: string): Keypair { 15 | return Keypair.fromSecretKey( 16 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 17 | ) 18 | } 19 | 20 | 21 | describe("hello-solana", () => { 22 | 23 | const connection = new Connection(`https://api.devnet.solana.com`, 'confirmed') 24 | const payer = loadKeypairFromFile(require('os').homedir() + '/.config/solana/id.json') 25 | const program = loadKeypairFromFile('./program/target/deploy/program-keypair.json') 26 | 27 | it("Say hello!", async () => { 28 | 29 | let ix = createSayHelloInstruction( 30 | payer.publicKey, 31 | program.publicKey, 32 | "Hello, World!", 33 | ) 34 | 35 | await sendAndConfirmTransaction( 36 | connection, 37 | new Transaction().add(ix), 38 | [payer] 39 | ) 40 | }) 41 | 42 | it("Say goodbye!", async () => { 43 | 44 | let ix = createSayGoodbyeInstruction( 45 | payer.publicKey, 46 | program.publicKey, 47 | "Goodbye, World!", 48 | "See You Next Time!", 49 | ) 50 | 51 | await sendAndConfirmTransaction( 52 | connection, 53 | new Transaction().add(ix), 54 | [payer] 55 | ) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world-again/tests/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | program/target/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "yarn run ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/test.ts" 4 | }, 5 | "dependencies": { 6 | "@solana/web3.js": "^1.47.3", 7 | "fs": "^0.0.1-security" 8 | }, 9 | "devDependencies": { 10 | "@types/bn.js": "^5.1.0", 11 | "@types/chai": "^4.3.1", 12 | "@types/mocha": "^9.1.1", 13 | "chai": "^4.3.4", 14 | "mocha": "^9.0.3", 15 | "ts-mocha": "^10.0.0", 16 | "typescript": "^4.3.5" 17 | } 18 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-program = "1.14.12" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "lib"] -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::{AccountInfo, next_account_info}, 3 | entrypoint, 4 | entrypoint::ProgramResult, 5 | msg, 6 | pubkey::Pubkey, 7 | }; 8 | 9 | 10 | // Tells Solana that the entrypoint to this program 11 | // is the "hello_world" function. 12 | // 13 | entrypoint!(hello_world); 14 | 15 | 16 | fn hello_world( 17 | program_id: &Pubkey, 18 | accounts: &[AccountInfo], 19 | _instruction_data: &[u8], 20 | ) -> ProgramResult { 21 | 22 | msg!("Hello, Solana!"); 23 | msg!("Our program's Program ID: {}", &program_id); 24 | 25 | let accounts_iter = &mut accounts.iter(); 26 | let payer = next_account_info(accounts_iter)?; 27 | 28 | msg!("Payer Address: {}", payer.key); 29 | 30 | Ok(()) 31 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | } from 'mocha' 5 | import { 6 | Connection, 7 | Keypair, 8 | PublicKey, 9 | sendAndConfirmTransaction, 10 | Transaction, 11 | TransactionInstruction, 12 | } from '@solana/web3.js' 13 | 14 | 15 | function loadKeypairFromFile(path: string): Keypair { 16 | return Keypair.fromSecretKey( 17 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 18 | ) 19 | } 20 | 21 | 22 | describe("hello-solana", () => { 23 | 24 | const connection = new Connection(`https://api.devnet.solana.com`, 'confirmed') 25 | const payer = loadKeypairFromFile(require('os').homedir() + '/.config/solana/id.json') 26 | const program = loadKeypairFromFile('./program/target/deploy/program-keypair.json') 27 | 28 | it("Say hello!", async () => { 29 | 30 | let ix = new TransactionInstruction({ 31 | keys: [ 32 | {pubkey: payer.publicKey, isSigner: true, isWritable: true} 33 | ], 34 | // programId: program.publicKey, 35 | programId: new PublicKey('3whVZdg3oSi1Wskt25dYLrqpLcXmTtXoyUyUrEjkcpNY'), 36 | data: Buffer.alloc(0), // No data 37 | }) 38 | 39 | await sendAndConfirmTransaction( 40 | connection, 41 | new Transaction().add(ix), 42 | [payer] 43 | ) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/solution/hello-world/tests/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/assets/my-nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My NFT", 3 | "symbol": "MYNFT", 4 | "description": "My custom NFT", 5 | "image": "https://www.nicepng.com/png/detail/552-5523019_homer-simpsons-png.png" 6 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/assets/my-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My Token", 3 | "symbol": "MYTKN", 4 | "description": "My custom SPL token", 5 | "image": "https://toppng.com/uploads/preview/gold-coins-11530998393xtf85riude.png" 6 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@metaplex-foundation/mpl-token-metadata": "^2.5.2", 4 | "@solana/spl-token": "^0.3.7", 5 | "@solana/web3.js": "^1.73.0", 6 | "ts-node": "^10.9.1", 7 | "typescript": "^4.9.4" 8 | }, 9 | "scripts": { 10 | "accounts": "ts-node ./scripts/accounts.ts", 11 | "tokens": "ts-node ./scripts/tokens.ts" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/scripts/accounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | SystemProgram, 6 | VersionedMessage, 7 | VersionedTransaction, 8 | } from "@solana/web3.js" 9 | import { 10 | loadKeypairFromFile 11 | } from '../util/local-configs' 12 | import { 13 | logAccountInfo, 14 | logBalance, 15 | logNewKeypair, 16 | logTransaction, 17 | newLogSection, 18 | } from '../util/log' 19 | import { 20 | buildTransaction 21 | } from '../util/transaction' 22 | 23 | 24 | const connection = new Connection( 25 | "https://api.devnet.solana.com", 26 | { 27 | commitment: 'confirmed', 28 | confirmTransactionInitialTimeout: 60000, 29 | }, 30 | ) 31 | 32 | const payer = loadKeypairFromFile( 33 | require('os').homedir() + '/.config/solana/id.json' 34 | ) 35 | 36 | const keypairA = Keypair.generate() 37 | const keypairB = Keypair.generate() 38 | 39 | async function createAccount(accountName: string, keypair: Keypair) { 40 | const lamports = 41 | await connection.getMinimumBalanceForRentExemption(0) + 100000 42 | const createAccountInstruction = SystemProgram.createAccount({ 43 | fromPubkey: payer.publicKey, 44 | newAccountPubkey: keypair.publicKey, 45 | lamports, 46 | space: 0, 47 | programId: SystemProgram.programId, 48 | }) 49 | const createAccountTransaction = await buildTransaction( 50 | connection, 51 | payer.publicKey, 52 | [payer, keypair], 53 | [createAccountInstruction] 54 | ) 55 | const signature = await connection.sendTransaction(createAccountTransaction) 56 | 57 | newLogSection() 58 | logNewKeypair(keypair) 59 | await logTransaction(connection, signature) 60 | await logBalance(accountName, connection, keypair.publicKey) 61 | } 62 | 63 | async function getAccountData(publicKey: PublicKey) { 64 | const accountInfo = await connection.getAccountInfo(publicKey) 65 | newLogSection() 66 | logAccountInfo(accountInfo) 67 | } 68 | 69 | async function transferSol(fromKeypair: Keypair, toPublicKey: PublicKey) { 70 | 71 | newLogSection() 72 | console.log("Starting balances:"); 73 | await logBalance("Account A", connection, keypairA.publicKey) 74 | await logBalance("Account B", connection, keypairB.publicKey) 75 | 76 | // Build a transaction that will transfer SOL between two parties 77 | // 78 | 79 | // const signature = await connection.sendTransaction(transferTransaction) 80 | // await logTransaction(connection, signature) 81 | 82 | console.log("Ending balances:"); 83 | await logBalance("Account A", connection, keypairA.publicKey) 84 | await logBalance("Account B", connection, keypairB.publicKey) 85 | } 86 | 87 | async function accountsScript() { 88 | await createAccount("Account A", keypairA); 89 | await getAccountData(keypairA.publicKey); 90 | await createAccount("Account B", keypairB); 91 | // await transferSol(keypairA, keypairB.publicKey); 92 | } 93 | 94 | accountsScript() 95 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/util/local-configs.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from "@solana/web3.js"; 2 | 3 | export function loadKeypairFromFile(path: string): Keypair { 4 | return Keypair.fromSecretKey( 5 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 6 | ) 7 | }; 8 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/util/log.ts: -------------------------------------------------------------------------------- 1 | import { Account, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; 2 | import { 3 | AccountInfo, 4 | Connection, 5 | Keypair, 6 | LAMPORTS_PER_SOL, 7 | PublicKey 8 | } from "@solana/web3.js" 9 | 10 | export function newLogSection() { 11 | console.log("-----------------------------------------------------") 12 | } 13 | 14 | export async function logAccountInfo(accountInfo: AccountInfo | null) { 15 | console.log("Account Info:") 16 | console.log(accountInfo) 17 | } 18 | 19 | export function logNewKeypair(keypair: Keypair) { 20 | console.log("Created a new keypair.") 21 | console.log(` New account Public Key: ${keypair.publicKey}`); 22 | } 23 | 24 | export async function logTransaction(connection: Connection, signature: string) { 25 | await connection.confirmTransaction(signature) 26 | console.log("Transaction successful.") 27 | console.log(` Transaction signature: ${signature}`); 28 | } 29 | 30 | export async function logBalance(accountName: string, connection: Connection, pubkey: PublicKey) { 31 | const balance = await connection.getBalance(pubkey) 32 | console.log(` ${accountName}:`); 33 | console.log(` Account Pubkey: ${pubkey.toString()} SOL`); 34 | console.log(` Account Balance: ${balance / LAMPORTS_PER_SOL} SOL`); 35 | } 36 | 37 | export function logNewMint(mintPubkey: PublicKey, decimals: number) { 38 | console.log("Created a new mint.") 39 | console.log(` New mint Public Key: ${mintPubkey}`); 40 | console.log(` Mint type: ${decimals === 0 ? 'NFT' : 'SPL Token'}`); 41 | } 42 | 43 | export async function logTokenBalance(accountName: string, associatedTokenAccount: Account) { 44 | console.log(` ${accountName}:`); 45 | console.log(` ATA Pubkey : ${associatedTokenAccount.address}`); 46 | console.log(` Mint : ${associatedTokenAccount.mint}`); 47 | console.log(` Token Balance : ${associatedTokenAccount.amount}`); 48 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/client-examples/util/transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | TransactionInstruction, 6 | TransactionMessage, 7 | VersionedTransaction 8 | } from "@solana/web3.js" 9 | 10 | 11 | export async function buildTransaction( 12 | connection: Connection, 13 | payer: PublicKey, 14 | signers: Keypair[], 15 | instructions: TransactionInstruction[] 16 | ): Promise { 17 | 18 | let blockhash = await connection 19 | .getLatestBlockhash() 20 | .then((res) => res.blockhash) 21 | 22 | const messageV0 = new TransactionMessage({ 23 | payerKey: payer, 24 | recentBlockhash: blockhash, 25 | instructions, 26 | }).compileToV0Message() 27 | 28 | const tx = new VersionedTransaction(messageV0) 29 | 30 | signers.forEach(s => tx.sign([s])) 31 | 32 | return tx 33 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | program/target/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "yarn run ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/test.ts" 4 | }, 5 | "dependencies": { 6 | "@solana/web3.js": "^1.47.3", 7 | "fs": "^0.0.1-security" 8 | }, 9 | "devDependencies": { 10 | "@types/bn.js": "^5.1.0", 11 | "@types/chai": "^4.3.1", 12 | "@types/mocha": "^9.1.1", 13 | "chai": "^4.3.4", 14 | "mocha": "^9.0.3", 15 | "ts-mocha": "^10.0.0", 16 | "typescript": "^4.3.5" 17 | } 18 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | borsh = "0.9.3" 8 | borsh-derive = "0.9.3" 9 | solana-program = "1.14.12" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{ BorshDeserialize, BorshSerialize }; 2 | use solana_program::{ 3 | account_info::{AccountInfo, next_account_info}, 4 | entrypoint, 5 | entrypoint::ProgramResult, 6 | msg, 7 | pubkey::Pubkey, 8 | }; 9 | 10 | 11 | #[derive(BorshSerialize, BorshDeserialize)] 12 | pub enum MyInstruction { 13 | SayHelloInstruction(SayHelloArgs), 14 | SayGoodbyeInstruction(SayGoodbyeArgs), 15 | } 16 | 17 | #[derive(BorshSerialize, BorshDeserialize)] 18 | pub struct SayHelloArgs { 19 | hello_message: String, 20 | } 21 | 22 | #[derive(BorshSerialize, BorshDeserialize)] 23 | pub struct SayGoodbyeArgs { 24 | goodbye_message: String, 25 | second_goodbye_message: String, 26 | } 27 | 28 | fn say_hello(args: SayHelloArgs) { 29 | msg!("{}", args.hello_message); 30 | } 31 | 32 | fn say_goodbye(args: SayGoodbyeArgs) { 33 | msg!("{}", args.goodbye_message); 34 | msg!("{}", args.second_goodbye_message); 35 | } 36 | 37 | 38 | entrypoint!(hello_world); 39 | 40 | fn hello_world( 41 | program_id: &Pubkey, 42 | accounts: &[AccountInfo], 43 | instruction_data: &[u8], 44 | ) -> ProgramResult { 45 | 46 | let instruction = MyInstruction::try_from_slice(&instruction_data)?; 47 | 48 | match instruction { 49 | MyInstruction::SayHelloInstruction(args) => say_hello(args), 50 | MyInstruction::SayGoodbyeInstruction(args) => say_goodbye(args), 51 | }; 52 | 53 | msg!("Our program's Program ID: {}", &program_id); 54 | 55 | let accounts_iter = &mut accounts.iter(); 56 | let payer = next_account_info(accounts_iter)?; 57 | 58 | msg!("Payer Address: {}", payer.key); 59 | 60 | Ok(()) 61 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/tests/instructions.ts: -------------------------------------------------------------------------------- 1 | import * as borsh from "borsh" 2 | import { Buffer } from "buffer" 3 | import { 4 | PublicKey, 5 | TransactionInstruction 6 | } from '@solana/web3.js' 7 | 8 | 9 | export enum MyInstruction { 10 | SayHello, 11 | SayGoodbye, 12 | } 13 | 14 | 15 | export class SayHello { 16 | 17 | instruction: MyInstruction 18 | hello_message: string 19 | 20 | constructor(props: { 21 | instruction: MyInstruction, 22 | hello_message: string, 23 | }) { 24 | this.instruction = props.instruction 25 | this.hello_message = props.hello_message 26 | } 27 | 28 | toBuffer() { 29 | return Buffer.from(borsh.serialize(SayHelloSchema, this)) 30 | } 31 | } 32 | 33 | export const SayHelloSchema = new Map([ 34 | [ SayHello, { 35 | kind: 'struct', 36 | fields: [ 37 | ['instruction', 'u8'], 38 | ['hello_message', 'string'], 39 | ], 40 | }] 41 | ]) 42 | 43 | export function createSayHelloInstruction( 44 | payer: PublicKey, 45 | programId: PublicKey, 46 | helloMessage: string, 47 | ): TransactionInstruction { 48 | 49 | const myInstructionObject = new SayHello({ 50 | instruction: MyInstruction.SayHello, 51 | hello_message: helloMessage, 52 | }) 53 | 54 | return new TransactionInstruction({ 55 | keys: [ 56 | {pubkey: payer, isSigner: true, isWritable: true}, 57 | ], 58 | programId: programId, 59 | data: myInstructionObject.toBuffer(), 60 | }) 61 | } 62 | 63 | 64 | // Implement the client-side serialization of `SayGoodbye` to build a transaction with an instruction 65 | // that will invoke our program's `SayGoodbye` operation. 66 | 67 | export class SayGoodbye { 68 | 69 | instruction: MyInstruction 70 | goodbye_message: string 71 | second_goodbye_message: string 72 | } 73 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | } from 'mocha' 5 | import { 6 | Connection, 7 | Keypair, 8 | sendAndConfirmTransaction, 9 | Transaction, 10 | } from '@solana/web3.js' 11 | import { createSayGoodbyeInstruction, createSayHelloInstruction } from './instructions' 12 | 13 | 14 | function loadKeypairFromFile(path: string): Keypair { 15 | return Keypair.fromSecretKey( 16 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 17 | ) 18 | } 19 | 20 | 21 | describe("hello-solana", () => { 22 | 23 | const connection = new Connection(`https://api.devnet.solana.com`, 'confirmed') 24 | const payer = loadKeypairFromFile(require('os').homedir() + '/.config/solana/id.json') 25 | const program = loadKeypairFromFile('./program/target/deploy/program-keypair.json') 26 | 27 | it("Say hello!", async () => { 28 | 29 | let ix = createSayHelloInstruction( 30 | payer.publicKey, 31 | program.publicKey, 32 | "Hello, World!", 33 | ) 34 | 35 | await sendAndConfirmTransaction( 36 | connection, 37 | new Transaction().add(ix), 38 | [payer] 39 | ) 40 | }) 41 | 42 | it("Say goodbye!", async () => { 43 | 44 | let ix = createSayGoodbyeInstruction( 45 | payer.publicKey, 46 | program.publicKey, 47 | "Goodbye, World!", 48 | "See You Next Time!", 49 | ) 50 | 51 | await sendAndConfirmTransaction( 52 | connection, 53 | new Transaction().add(ix), 54 | [payer] 55 | ) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world-again/tests/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | program/target/ -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "yarn run ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/test.ts" 4 | }, 5 | "dependencies": { 6 | "@solana/web3.js": "^1.47.3", 7 | "fs": "^0.0.1-security" 8 | }, 9 | "devDependencies": { 10 | "@types/bn.js": "^5.1.0", 11 | "@types/chai": "^4.3.1", 12 | "@types/mocha": "^9.1.1", 13 | "chai": "^4.3.4", 14 | "mocha": "^9.0.3", 15 | "ts-mocha": "^10.0.0", 16 | "typescript": "^4.3.5" 17 | } 18 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-program = "1.14.12" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "lib"] -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::{AccountInfo, next_account_info}, 3 | entrypoint, 4 | entrypoint::ProgramResult, 5 | msg, 6 | pubkey::Pubkey, 7 | }; 8 | 9 | 10 | // Tells Solana that the entrypoint to this program 11 | // is the "hello_world" function. 12 | // 13 | entrypoint!(hello_world); 14 | 15 | 16 | fn hello_world( 17 | program_id: &Pubkey, 18 | accounts: &[AccountInfo], 19 | _instruction_data: &[u8], 20 | ) -> ProgramResult { 21 | 22 | msg!("Hello, Solana!"); 23 | msg!("Our program's Program ID: {}", &program_id); 24 | 25 | let accounts_iter = &mut accounts.iter(); 26 | let payer = next_account_info(accounts_iter)?; 27 | 28 | msg!("Payer Address: {}", payer.key); 29 | 30 | Ok(()) 31 | } -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | it, 3 | describe, 4 | } from 'mocha' 5 | import { 6 | Connection, 7 | Keypair, 8 | PublicKey, 9 | sendAndConfirmTransaction, 10 | Transaction, 11 | TransactionInstruction, 12 | } from '@solana/web3.js' 13 | 14 | 15 | function loadKeypairFromFile(path: string): Keypair { 16 | return Keypair.fromSecretKey( 17 | Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8"))) 18 | ) 19 | } 20 | 21 | 22 | describe("hello-solana", () => { 23 | 24 | const connection = new Connection(`https://api.devnet.solana.com`, 'confirmed') 25 | const payer = loadKeypairFromFile(require('os').homedir() + '/.config/solana/id.json') 26 | const program = loadKeypairFromFile('./program/target/deploy/program-keypair.json') 27 | 28 | it("Say hello!", async () => { 29 | 30 | // Build a transaction with an instruction that will invoke our program. 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /workshops/beginner-crash-course/starter/hello-world/tests/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } -------------------------------------------------------------------------------- /workshops/deep-dive-into-programs/README.md: -------------------------------------------------------------------------------- 1 | # Deep Dive into Writing Solana Programs 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | Coming soon! | Coming soon! | 7 | 8 | ## 📗 Learn -------------------------------------------------------------------------------- /workshops/getting-started-101/CONDUCTING.md: -------------------------------------------------------------------------------- 1 | # How to Conduct This Workshop 2 | 3 | ### [youtube Video Tutorial](https://github.com/solana-developers) 4 | 5 | ### [slides Presentation Slides](https://github.com/solana-developers) 6 | 7 | ### 📋 Step-By-Step Tutorial -------------------------------------------------------------------------------- /workshops/getting-started-101/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Solana 101 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | [Joe Caulfield](https://twitter.com/realbuffalojoe) | Encode Hackathon 2023 | 7 | | [youtube](https://github.com/solana-developers) | [Joe Caulfield](https://twitter.com/realbuffalojoe) | RealityHack MIT 2023 | 8 | 9 | ## 📗 Learn -------------------------------------------------------------------------------- /workshops/nft-minter/.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["next/babel"] } 2 | -------------------------------------------------------------------------------- /workshops/nft-minter/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __generated__ -------------------------------------------------------------------------------- /workshops/nft-minter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /workshops/nft-minter/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "cargo" 14 | directory: "/program" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /workshops/nft-minter/.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/cache@v2 15 | with: 16 | path: "**/node_modules" 17 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16" 21 | - run: yarn install 22 | - name: Build 23 | run: yarn build 24 | -------------------------------------------------------------------------------- /workshops/nft-minter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | 40 | # logs 41 | *.log 42 | 43 | 44 | 45 | /public/uploads/* -------------------------------------------------------------------------------- /workshops/nft-minter/README.md: -------------------------------------------------------------------------------- 1 | # NFT Minter 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | Coming soon! | Coming soon! | 7 | 8 | ## 📗 Learn 9 | 10 | https://collections.metaplex.com/ -------------------------------------------------------------------------------- /workshops/nft-minter/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /workshops/nft-minter/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | env: { 5 | RPC_ENDPOINT: process.env.RPC_ENDPOINT, 6 | } 7 | } 8 | 9 | module.exports = nextConfig 10 | -------------------------------------------------------------------------------- /workshops/nft-minter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-dapp-next", 3 | "version": "0.2.0", 4 | "author": "Solana Maintainers ", 5 | "license": "MIT", 6 | "private": false, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@heroicons/react": "^1.0.5", 15 | "@metaplex-foundation/js": "^0.18.0", 16 | "@metaplex-foundation/mpl-token-metadata": "^2.8.2", 17 | "@noble/ed25519": "^1.7.1", 18 | "@solana/spl-token": "^0.3.7", 19 | "@solana/wallet-adapter-base": "^0.9.20", 20 | "@solana/wallet-adapter-react": "^0.15.28", 21 | "@solana/wallet-adapter-react-ui": "^0.9.27", 22 | "@solana/wallet-adapter-wallets": "^0.19.11", 23 | "@solana/web3.js": "^1.73.0", 24 | "@tailwindcss/typography": "^0.5.9", 25 | "daisyui": "^1.24.3", 26 | "date-fns": "^2.29.3", 27 | "formidable": "^2.1.1", 28 | "immer": "^9.0.12", 29 | "next": "^13.1.5", 30 | "next-compose-plugins": "^2.2.1", 31 | "next-transpile-modules": "^10.0.0", 32 | "react": "^18.2.0", 33 | "react-dom": "^18.2.0", 34 | "zustand": "^3.6.9" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^18.11.18", 38 | "@types/react": "^18.0.27", 39 | "autoprefixer": "^10.4.2", 40 | "eslint": "8.7.0", 41 | "eslint-config-next": "^13.1.5", 42 | "postcss": "^8.4.5", 43 | "tailwindcss": "^3.2.4", 44 | "typescript": "^4.9.4" 45 | }, 46 | "engines": { 47 | "node": ">=16" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /workshops/nft-minter/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /workshops/nft-minter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/nft-minter/public/favicon.ico -------------------------------------------------------------------------------- /workshops/nft-minter/public/solanaLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/nft-minter/public/solanaLogo.png -------------------------------------------------------------------------------- /workshops/nft-minter/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/ContentContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Link from "next/link"; 3 | import Text from './Text'; 4 | import NavElement from './nav-element'; 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | export const ContentContainer: React.FC = ({ children }) => { 10 | 11 | return ( 12 |
13 | 14 |
15 | {children} 16 |
17 | {/* SideBar / Drawer */} 18 |
19 | 20 | 21 |
    22 |
  • 23 | Menu 24 |
  • 25 |
  • 26 | 30 |
  • 31 |
  • 32 | 36 |
  • 37 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/NetworkSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import { useNetworkConfiguration } from '../contexts/NetworkConfigurationProvider'; 4 | 5 | const NetworkSwitcher: FC = () => { 6 | const { networkConfiguration, setNetworkConfiguration } = useNetworkConfiguration(); 7 | 8 | console.log(networkConfiguration); 9 | 10 | return ( 11 | 23 | ); 24 | }; 25 | 26 | export default dynamic(() => Promise.resolve(NetworkSwitcher), { 27 | ssr: false 28 | }) -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/RequestAirdrop.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { LAMPORTS_PER_SOL, TransactionSignature } from '@solana/web3.js'; 3 | import { FC, useCallback } from 'react'; 4 | import { notify } from "../utils/notifications"; 5 | import useUserSOLBalanceStore from '../stores/useUserSOLBalanceStore'; 6 | 7 | export const RequestAirdrop: FC = () => { 8 | const { connection } = useConnection(); 9 | const { publicKey } = useWallet(); 10 | const { getUserSOLBalance } = useUserSOLBalanceStore(); 11 | 12 | const onClick = useCallback(async () => { 13 | if (!publicKey) { 14 | console.log('error', 'Wallet not connected!'); 15 | notify({ type: 'error', message: 'error', description: 'Wallet not connected!' }); 16 | return; 17 | } 18 | 19 | let signature: TransactionSignature = ''; 20 | 21 | try { 22 | signature = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL); 23 | 24 | // Get the lates block hash to use on our transaction and confirmation 25 | let latestBlockhash = await connection.getLatestBlockhash() 26 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed'); 27 | 28 | notify({ type: 'success', message: 'Airdrop successful!', txid: signature }); 29 | 30 | getUserSOLBalance(publicKey, connection); 31 | } catch (error: any) { 32 | notify({ type: 'error', message: `Airdrop failed!`, description: error?.message, txid: signature }); 33 | console.log('error', `Airdrop failed! ${error?.message}`, signature); 34 | } 35 | }, [publicKey, connection, getUserSOLBalance]); 36 | 37 | return ( 38 | 39 |
40 |
41 |
43 | 44 | 51 |
52 |
53 | 54 | 55 | ); 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/SendTransaction.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { Keypair, SystemProgram, Transaction, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js'; 3 | import { FC, useCallback } from 'react'; 4 | import { notify } from "../utils/notifications"; 5 | 6 | export const SendTransaction: FC = () => { 7 | const { connection } = useConnection(); 8 | const { publicKey, sendTransaction } = useWallet(); 9 | 10 | const onClick = useCallback(async () => { 11 | if (!publicKey) { 12 | notify({ type: 'error', message: `Wallet not connected!` }); 13 | console.log('error', `Send Transaction: Wallet not connected!`); 14 | return; 15 | } 16 | 17 | let signature: TransactionSignature = ''; 18 | try { 19 | 20 | // Create instructions to send, in this case a simple transfer 21 | const instructions = [ 22 | SystemProgram.transfer({ 23 | fromPubkey: publicKey, 24 | toPubkey: Keypair.generate().publicKey, 25 | lamports: 1_000_000, 26 | }), 27 | ]; 28 | 29 | // Get the lates block hash to use on our transaction and confirmation 30 | let latestBlockhash = await connection.getLatestBlockhash() 31 | 32 | // Create a new TransactionMessage with version and compile it to legacy 33 | const messageLegacy = new TransactionMessage({ 34 | payerKey: publicKey, 35 | recentBlockhash: latestBlockhash.blockhash, 36 | instructions, 37 | }).compileToLegacyMessage(); 38 | 39 | // Create a new VersionedTransacction which supports legacy and v0 40 | const transation = new VersionedTransaction(messageLegacy) 41 | 42 | // Send transaction and await for signature 43 | signature = await sendTransaction(transation, connection); 44 | 45 | // Send transaction and await for signature 46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed'); 47 | 48 | console.log(signature); 49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature }); 50 | } catch (error: any) { 51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature }); 52 | console.log('error', `Transaction failed! ${error?.message}`, signature); 53 | return; 54 | } 55 | }, [publicKey, notify, connection, sendTransaction]); 56 | 57 | return ( 58 |
59 |
60 |
62 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/SendVersionedTransaction.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { Keypair, SystemProgram, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js'; 3 | import { FC, useCallback } from 'react'; 4 | import { notify } from "../utils/notifications"; 5 | 6 | export const SendVersionedTransaction: FC = () => { 7 | const { connection } = useConnection(); 8 | const { publicKey, sendTransaction } = useWallet(); 9 | 10 | const onClick = useCallback(async () => { 11 | if (!publicKey) { 12 | notify({ type: 'error', message: `Wallet not connected!` }); 13 | console.log('error', `Send Transaction: Wallet not connected!`); 14 | return; 15 | } 16 | 17 | let signature: TransactionSignature = ''; 18 | try { 19 | 20 | // Create instructions to send, in this case a simple transfer 21 | const instructions = [ 22 | SystemProgram.transfer({ 23 | fromPubkey: publicKey, 24 | toPubkey: Keypair.generate().publicKey, 25 | lamports: 1_000_000, 26 | }), 27 | ]; 28 | 29 | // Get the lates block hash to use on our transaction and confirmation 30 | let latestBlockhash = await connection.getLatestBlockhash() 31 | 32 | // Create a new TransactionMessage with version and compile it to version 0 33 | const messageV0 = new TransactionMessage({ 34 | payerKey: publicKey, 35 | recentBlockhash: latestBlockhash.blockhash, 36 | instructions, 37 | }).compileToV0Message(); 38 | 39 | // Create a new VersionedTransacction to support the v0 message 40 | const transation = new VersionedTransaction(messageV0) 41 | 42 | // Send transaction and await for signature 43 | signature = await sendTransaction(transation, connection); 44 | 45 | // Await for confirmation 46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed'); 47 | 48 | console.log(signature); 49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature }); 50 | } catch (error: any) { 51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature }); 52 | console.log('error', `Transaction failed! ${error?.message}`, signature); 53 | return; 54 | } 55 | }, [publicKey, notify, connection, sendTransaction]); 56 | 57 | return ( 58 |
59 |
60 |
62 | 73 |
74 |
75 | ); 76 | }; -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/SignMessage.tsx: -------------------------------------------------------------------------------- 1 | // TODO: SignMessage 2 | import { verify } from '@noble/ed25519'; 3 | import { useWallet } from '@solana/wallet-adapter-react'; 4 | import bs58 from 'bs58'; 5 | import { FC, useCallback } from 'react'; 6 | import { notify } from "../utils/notifications"; 7 | 8 | export const SignMessage: FC = () => { 9 | const { publicKey, signMessage } = useWallet(); 10 | 11 | const onClick = useCallback(async () => { 12 | try { 13 | // `publicKey` will be null if the wallet isn't connected 14 | if (!publicKey) throw new Error('Wallet not connected!'); 15 | // `signMessage` will be undefined if the wallet doesn't support it 16 | if (!signMessage) throw new Error('Wallet does not support message signing!'); 17 | // Encode anything as bytes 18 | const message = new TextEncoder().encode('Hello, world!'); 19 | // Sign the bytes using the wallet 20 | const signature = await signMessage(message); 21 | // Verify that the bytes were signed using the private key that matches the known public key 22 | if (!verify(signature, message, publicKey.toBytes())) throw new Error('Invalid signature!'); 23 | notify({ type: 'success', message: 'Sign message successful!', txid: bs58.encode(signature) }); 24 | } catch (error: any) { 25 | notify({ type: 'error', message: `Sign Message failed!`, description: error?.message }); 26 | console.log('error', `Sign Message failed! ${error?.message}`); 27 | } 28 | }, [publicKey, notify, signMessage]); 29 | 30 | return ( 31 |
32 |
33 |
35 | 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/Text/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { cn } from 'utils'; 4 | 5 | /** 6 | * Properties for a card component. 7 | */ 8 | type TextProps = { 9 | variant: 10 | | 'big-heading' 11 | | 'heading' 12 | | 'sub-heading' 13 | | 'nav-heading' 14 | | 'nav' 15 | | 'input' 16 | | 'label'; 17 | className?: string; 18 | href?: string; 19 | children?: React.ReactNode; 20 | id?: string; 21 | }; 22 | 23 | /** 24 | * Pre-defined styling, according to agreed-upon design-system. 25 | */ 26 | const variants = { 27 | heading: 'text-3xl font-medium', 28 | 'sub-heading': 'text-2xl font-medium', 29 | 'nav-heading': 'text-lg font-medium sm:text-xl', 30 | nav: 'font-medium', 31 | paragraph: 'text-lg', 32 | 'sub-paragraph': 'text-base font-medium text-inherit', 33 | input: 'text-sm uppercase tracking-wide', 34 | label: 'text-xs uppercase tracking-wide', 35 | }; 36 | 37 | /** 38 | * Definition of a card component,the main purpose of 39 | * which is to neatly display information. Can be both 40 | * interactive and static. 41 | * 42 | * @param variant Variations relating to pre-defined styling of the element. 43 | * @param className Custom classes to be applied to the element. 44 | * @param children Child elements to be rendered within the component. 45 | */ 46 | const Text = ({ variant, className, href, children }: TextProps) => ( 47 |

48 | {href ? ( 49 | 50 | {children} 51 | 52 | ) : ( 53 | children 54 | )} 55 |

56 | ); 57 | 58 | export default Text; -------------------------------------------------------------------------------- /workshops/nft-minter/src/components/nav-element/index.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-empty */ 2 | import Link from 'next/link'; 3 | import Text from '../Text'; 4 | import { cn } from '../../utils'; 5 | import { useRouter } from 'next/router'; 6 | import { useEffect, useRef } from 'react'; 7 | 8 | type NavElementProps = { 9 | label: string; 10 | href: string; 11 | as?: string; 12 | scroll?: boolean; 13 | chipLabel?: string; 14 | disabled?: boolean; 15 | navigationStarts?: () => void; 16 | }; 17 | 18 | const NavElement = ({ 19 | label, 20 | href, 21 | as, 22 | scroll, 23 | disabled, 24 | navigationStarts = () => {}, 25 | }: NavElementProps) => { 26 | const router = useRouter(); 27 | const isActive = href === router.asPath || (as && as === router.asPath); 28 | const divRef = useRef(null); 29 | 30 | useEffect(() => { 31 | if (divRef.current) { 32 | divRef.current.className = cn( 33 | 'h-0.5 w-1/4 transition-all duration-300 ease-out', 34 | isActive 35 | ? '!w-full bg-gradient-to-l from-fuchsia-500 to-pink-500 ' 36 | : 'group-hover:w-1/2 group-hover:bg-fuchsia-500', 37 | ); 38 | } 39 | }, [isActive]); 40 | 41 | return ( 42 | navigationStarts()} 53 | > 54 |
55 | {label} 56 |
57 |
58 | 59 | ); 60 | }; 61 | 62 | export default NavElement; 63 | 64 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/contexts/AutoConnectProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@solana/wallet-adapter-react'; 2 | import { createContext, FC, ReactNode, useContext } from 'react'; 3 | 4 | export interface AutoConnectContextState { 5 | autoConnect: boolean; 6 | setAutoConnect(autoConnect: boolean): void; 7 | } 8 | 9 | export const AutoConnectContext = createContext({} as AutoConnectContextState); 10 | 11 | export function useAutoConnect(): AutoConnectContextState { 12 | return useContext(AutoConnectContext); 13 | } 14 | 15 | export const AutoConnectProvider: FC<{ children: ReactNode }> = ({ children }) => { 16 | // TODO: fix auto connect to actual reconnect on refresh/other. 17 | // TODO: make switch/slider settings 18 | // const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', false); 19 | const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', true); 20 | 21 | return ( 22 | {children} 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/contexts/ContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { WalletAdapterNetwork, WalletError } from '@solana/wallet-adapter-base'; 2 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 3 | import { 4 | PhantomWalletAdapter, 5 | SolflareWalletAdapter, 6 | SolletExtensionWalletAdapter, 7 | SolletWalletAdapter, 8 | TorusWalletAdapter, 9 | // LedgerWalletAdapter, 10 | // SlopeWalletAdapter, 11 | } from '@solana/wallet-adapter-wallets'; 12 | import { FC, ReactNode, useCallback, useMemo } from 'react'; 13 | import { AutoConnectProvider, useAutoConnect } from './AutoConnectProvider'; 14 | import { notify } from "../utils/notifications"; 15 | import { NetworkConfigurationProvider, useNetworkConfiguration } from './NetworkConfigurationProvider'; 16 | import dynamic from "next/dynamic"; 17 | 18 | const ReactUIWalletModalProviderDynamic = dynamic( 19 | async () => 20 | (await import("@solana/wallet-adapter-react-ui")).WalletModalProvider, 21 | { ssr: false } 22 | ); 23 | 24 | const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => { 25 | const { autoConnect } = useAutoConnect(); 26 | const { networkConfiguration } = useNetworkConfiguration(); 27 | const network = networkConfiguration as WalletAdapterNetwork; 28 | const endpoint = useMemo(() => process.env.RPC_ENDPOINT, []); 29 | 30 | console.log(network); 31 | 32 | const wallets = useMemo( 33 | () => [ 34 | new PhantomWalletAdapter(), 35 | new SolflareWalletAdapter(), 36 | new SolletWalletAdapter({ network }), 37 | new SolletExtensionWalletAdapter({ network }), 38 | new TorusWalletAdapter(), 39 | // new LedgerWalletAdapter(), 40 | // new SlopeWalletAdapter(), 41 | ], 42 | [network] 43 | ); 44 | 45 | const onError = useCallback( 46 | (error: WalletError) => { 47 | notify({ type: 'error', message: error.message ? `${error.name}: ${error.message}` : error.name }); 48 | console.error(error); 49 | }, 50 | [] 51 | ); 52 | 53 | return ( 54 | // TODO: updates needed for updating and referencing endpoint: wallet adapter rework 55 | 56 | 57 | 58 | {children} 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export const ContextProvider: FC<{ children: ReactNode }> = ({ children }) => { 66 | return ( 67 | <> 68 | 69 | 70 | {children} 71 | 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/contexts/NetworkConfigurationProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@solana/wallet-adapter-react'; 2 | import { createContext, FC, ReactNode, useContext } from 'react'; 3 | 4 | 5 | export interface NetworkConfigurationState { 6 | networkConfiguration: string; 7 | setNetworkConfiguration(networkConfiguration: string): void; 8 | } 9 | 10 | export const NetworkConfigurationContext = createContext({} as NetworkConfigurationState); 11 | 12 | export function useNetworkConfiguration(): NetworkConfigurationState { 13 | return useContext(NetworkConfigurationContext); 14 | } 15 | 16 | export const NetworkConfigurationProvider: FC<{ children: ReactNode }> = ({ children }) => { 17 | const [networkConfiguration, setNetworkConfiguration] = useLocalStorage("network", "devnet"); 18 | 19 | return ( 20 | {children} 21 | ); 22 | }; -------------------------------------------------------------------------------- /workshops/nft-minter/src/hooks/useQueryContext.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import { EndpointTypes } from '../models/types' 3 | 4 | export default function useQueryContext() { 5 | const router = useRouter() 6 | const { cluster } = router.query 7 | 8 | const endpoint = cluster ? (cluster as EndpointTypes) : 'mainnet' 9 | const hasClusterOption = endpoint !== 'mainnet' 10 | const fmtUrlWithCluster = (url) => { 11 | if (hasClusterOption) { 12 | const mark = url.includes('?') ? '&' : '?' 13 | return decodeURIComponent(`${url}${mark}cluster=${endpoint}`) 14 | } 15 | return url 16 | } 17 | 18 | return { 19 | fmtUrlWithCluster, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/models/types.ts: -------------------------------------------------------------------------------- 1 | export type EndpointTypes = 'mainnet' | 'devnet' | 'localnet' 2 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | import { FC } from 'react'; 4 | import { ContextProvider } from '../contexts/ContextProvider'; 5 | import { AppBar } from '../components/AppBar'; 6 | import { ContentContainer } from '../components/ContentContainer'; 7 | import { Footer } from '../components/Footer'; 8 | import Notifications from '../components/Notification' 9 | require('@solana/wallet-adapter-react-ui/styles.css'); 10 | require('../styles/globals.css'); 11 | 12 | const App: FC = ({ Component, pageProps }) => { 13 | return ( 14 | <> 15 | 16 | NFT Minter 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | ); 31 | }; 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx: DocumentContext) { 5 | const initialProps = await Document.getInitialProps(ctx) 6 | 7 | return initialProps 8 | } 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default MyDocument; 26 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/pages/api/upload.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import formidable from "formidable"; 3 | import fs from "fs"; 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false 8 | } 9 | }; 10 | 11 | export default function uploadFile(req, res) { 12 | const form = new formidable.IncomingForm(); 13 | form.parse(req, async function (err, fields, files) { 14 | saveFile(files.file); 15 | return res.status(201).send(""); 16 | }); 17 | }; 18 | 19 | const saveFile = (file) => { 20 | const data = fs.readFileSync(file.filepath); 21 | fs.writeFileSync(`./public/uploads/nft-img.${file.originalFilename.split('.').pop()}`, data); 22 | fs.unlinkSync(file.filepath); 23 | return; 24 | }; 25 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { HomeView } from "../views"; 4 | 5 | const Home: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | NFT Minter 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Home; 21 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/stores/useNotificationStore.tsx: -------------------------------------------------------------------------------- 1 | import create, { State } from "zustand"; 2 | import produce from "immer"; 3 | 4 | interface NotificationStore extends State { 5 | notifications: Array<{ 6 | type: string 7 | message: string 8 | description?: string 9 | txid?: string 10 | }> 11 | set: (x: any) => void 12 | } 13 | 14 | const useNotificationStore = create((set, _get) => ({ 15 | notifications: [], 16 | set: (fn) => set(produce(fn)), 17 | })) 18 | 19 | export default useNotificationStore 20 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/stores/useUserSOLBalanceStore.tsx: -------------------------------------------------------------------------------- 1 | import create, { State } from 'zustand' 2 | import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js' 3 | 4 | interface UserSOLBalanceStore extends State { 5 | balance: number; 6 | getUserSOLBalance: (publicKey: PublicKey, connection: Connection) => void 7 | } 8 | 9 | const useUserSOLBalanceStore = create((set, _get) => ({ 10 | balance: 0, 11 | getUserSOLBalance: async (publicKey, connection) => { 12 | let balance = 0; 13 | try { 14 | balance = await connection.getBalance( 15 | publicKey, 16 | 'confirmed' 17 | ); 18 | balance = balance / LAMPORTS_PER_SOL; 19 | } catch (e) { 20 | console.log(`error getting balance: `, e); 21 | } 22 | set((s) => { 23 | s.balance = balance; 24 | console.log(`balance updated, `, balance); 25 | }) 26 | }, 27 | })); 28 | 29 | export default useUserSOLBalanceStore; -------------------------------------------------------------------------------- /workshops/nft-minter/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 11 | background-image: radial-gradient( 12 | circle farthest-side at 50% 100%,rgba(1, 9, 18, 0), 13 | rgba(1, 6, 14, 0.6) 36%, 14 | rgba(1, 14, 29, 0.6) 55%, 15 | rgba(49, 18, 93, 0.4)); 16 | } 17 | 18 | a { 19 | color: inherit; 20 | text-decoration: none; 21 | } 22 | 23 | * { 24 | box-sizing: border-box; 25 | } 26 | 27 | /* example: override wallet button style */ 28 | .wallet-adapter-button:not([disabled]):hover { 29 | background-color: #707070; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/utils/explorer.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Transaction } from '@solana/web3.js' 2 | import base58 from 'bs58' 3 | 4 | export function getExplorerUrl( 5 | endpoint: string, 6 | viewTypeOrItemAddress: 'inspector' | PublicKey | string, 7 | itemType = 'address' // | 'tx' | 'block' 8 | ) { 9 | const getClusterUrlParam = () => { 10 | let cluster = '' 11 | if (endpoint === 'localnet') { 12 | cluster = `custom&customUrl=${encodeURIComponent( 13 | 'http://127.0.0.1:8899' 14 | )}` 15 | } else if (endpoint === 'https://api.devnet.solana.com') { 16 | cluster = 'devnet' 17 | } 18 | 19 | return cluster ? `?cluster=${cluster}` : '' 20 | } 21 | 22 | return `https://explorer.solana.com/${itemType}/${viewTypeOrItemAddress}${getClusterUrlParam()}` 23 | } -------------------------------------------------------------------------------- /workshops/nft-minter/src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | // Concatenates classes into a single className string 4 | const cn = (...args: string[]) => args.join(' '); 5 | 6 | const formatDate = (date: string) => format(new Date(date), 'MM/dd/yyyy h:mm:ss'); 7 | 8 | /** 9 | * Formats number as currency string. 10 | * 11 | * @param number Number to format. 12 | */ 13 | const numberToCurrencyString = (number: number) => 14 | number.toLocaleString('en-US'); 15 | 16 | /** 17 | * Returns a number whose value is limited to the given range. 18 | * 19 | * Example: limit the output of this computation to between 0 and 255 20 | * (x * 255).clamp(0, 255) 21 | * 22 | * @param {Number} min The lower boundary of the output range 23 | * @param {Number} max The upper boundary of the output range 24 | * @returns A number in the range [min, max] 25 | * @type Number 26 | */ 27 | const clamp = (current, min, max) => Math.min(Math.max(current, min), max); 28 | 29 | export { 30 | cn, 31 | formatDate, 32 | numberToCurrencyString, 33 | clamp, 34 | }; 35 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/utils/metaplex.ts: -------------------------------------------------------------------------------- 1 | import { bundlrStorage, Metaplex, toMetaplexFileFromBrowser, walletAdapterIdentity } from "@metaplex-foundation/js"; 2 | import { WalletContextState } from "@solana/wallet-adapter-react"; 3 | import { Connection, PublicKey } from "@solana/web3.js"; 4 | 5 | export async function mintWithMetaplexJs( 6 | connection: Connection, 7 | networkConfiguration: string, 8 | wallet: WalletContextState, 9 | name: string, 10 | symbol: string, 11 | description: string, 12 | collection: PublicKey, 13 | image: File, 14 | ): Promise<[string, string]> { 15 | 16 | const metaplex = Metaplex.make(connection) 17 | .use(walletAdapterIdentity(wallet)) 18 | .use(bundlrStorage({ address: `https://${networkConfiguration}.bundlr.network` })); 19 | const { uri } = await metaplex.nfts().uploadMetadata({ 20 | name, 21 | symbol, 22 | description, 23 | image: await toMetaplexFileFromBrowser(image), 24 | }); 25 | const { nft, response } = await metaplex.nfts().create({ 26 | name, 27 | symbol, 28 | uri: uri, 29 | sellerFeeBasisPoints: 0, 30 | tokenOwner: wallet.publicKey, 31 | mintTokens: true, 32 | collection, 33 | }); 34 | return [nft.address.toBase58(), response.signature]; 35 | } 36 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/utils/notifications.tsx: -------------------------------------------------------------------------------- 1 | import useNotificationStore from "../stores/useNotificationStore"; 2 | 3 | export function notify(newNotification: { 4 | type?: string 5 | message: string 6 | description?: string 7 | txid?: string 8 | }) { 9 | const { 10 | notifications, 11 | set: setNotificationStore, 12 | } = useNotificationStore.getState() 13 | 14 | setNotificationStore((state: { notifications: any[] }) => { 15 | state.notifications = [ 16 | ...notifications, 17 | { type: 'success', ...newNotification }, 18 | ] 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /workshops/nft-minter/src/views/index.tsx: -------------------------------------------------------------------------------- 1 | // Next, React 2 | import { FC, useEffect, useState } from 'react'; 3 | import Link from 'next/link'; 4 | 5 | // Wallet 6 | import { useWallet, useConnection } from '@solana/wallet-adapter-react'; 7 | 8 | // Components 9 | import { RequestAirdrop } from '../components/RequestAirdrop'; 10 | 11 | // Store 12 | import useUserSOLBalanceStore from '../stores/useUserSOLBalanceStore'; 13 | import { NftMinter } from 'components/NftMinter'; 14 | 15 | export const HomeView: FC = ({ }) => { 16 | const wallet = useWallet(); 17 | const { connection } = useConnection(); 18 | 19 | const balance = useUserSOLBalanceStore((s) => s.balance) 20 | const { getUserSOLBalance } = useUserSOLBalanceStore() 21 | 22 | useEffect(() => { 23 | if (wallet.publicKey) { 24 | console.log(wallet.publicKey.toBase58()) 25 | getUserSOLBalance(wallet.publicKey, connection) 26 | } 27 | }, [wallet.publicKey, connection, getUserSOLBalance]) 28 | 29 | return ( 30 | 31 |
32 |
33 |
34 |

35 | NFT Minter 36 |

37 |
38 |

39 |

Upload a pic and mint it to an NFT!

40 |

41 | { wallet.connected ?
42 |
43 | 44 |
45 |
46 | 47 |

48 | {wallet && 49 |
50 |
51 | {(balance || 0).toLocaleString()} 52 |
53 |
54 | SOL 55 |
56 |
57 | } 58 |

59 |
60 |
61 | : 62 |
63 |

64 | Connect Your Wallet! 65 |

66 |
67 | } 68 |
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /workshops/nft-minter/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "jit", 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | darkMode: "media", 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [ 9 | require('daisyui'), 10 | require("@tailwindcss/typography") 11 | ], 12 | daisyui: { 13 | styled: true, 14 | // TODO: Theme needs works 15 | themes: [ 16 | { 17 | 'solana': { 18 | fontFamily: { 19 | display: ['PT Mono, monospace'], 20 | body: ['Inter, sans-serif'], 21 | }, 22 | 'primary': '#000000', /* Primary color */ 23 | 'primary-focus': '#9945FF', /* Primary color - focused */ 24 | 'primary-content': '#ffffff', /* Foreground content color to use on primary color */ 25 | 26 | 'secondary': '#808080', /* Secondary color */ 27 | 'secondary-focus': '#f3cc30', /* Secondary color - focused */ 28 | 'secondary-content': '#ffffff', /* Foreground content color to use on secondary color */ 29 | 30 | 'accent': '#33a382', /* Accent color */ 31 | 'accent-focus': '#2aa79b', /* Accent color - focused */ 32 | 'accent-content': '#ffffff', /* Foreground content color to use on accent color */ 33 | 34 | 'neutral': '#2b2b2b', /* Neutral color */ 35 | 'neutral-focus': '#2a2e37', /* Neutral color - focused */ 36 | 'neutral-content': '#ffffff', /* Foreground content color to use on neutral color */ 37 | 38 | 'base-100': '#000000', /* Base color of page, used for blank backgrounds */ 39 | 'base-200': '#35363a', /* Base color, a little darker */ 40 | 'base-300': '#222222', /* Base color, even more darker */ 41 | 'base-content': '#f9fafb', /* Foreground content color to use on base color */ 42 | 43 | 'info': '#2094f3', /* Info */ 44 | 'success': '#009485', /* Success */ 45 | 'warning': '#ff9900', /* Warning */ 46 | 'error': '#ff5724', /* Error */ 47 | }, 48 | }, 49 | // backup themes: 50 | // 'dark', 51 | // 'synthwave' 52 | ], 53 | base: true, 54 | utils: true, 55 | logs: true, 56 | rtl: false, 57 | }, 58 | } -------------------------------------------------------------------------------- /workshops/nft-minter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "target": "es6", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "strictNullChecks": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules", ".next",], 22 | "ts-node": { 23 | "require": ["tsconfig-paths/register"], 24 | "compilerOptions": { 25 | "module": "commonjs" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/* 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/README.md: -------------------------------------------------------------------------------- 1 | # xnft-quickstart 2 | 3 | Quickstart repo for building your own xNFT. 4 | 5 | ## Developing 6 | 7 | Once you've installed Backpack, get started building your xNFT with these steps. Note that the packages here will always use the latest, which correspond to the latest tagged build of Backpack. If you have unexepected issues, make sure your package versions match the app version. 8 | 9 | Further documentation: https://docs.xnfts.dev/getting-started/getting-started 10 | 11 | ### Install 12 | 13 | First, install dependencies. 14 | 15 | ``` 16 | yarn 17 | ``` 18 | 19 | ### Run the dev server 20 | 21 | Then, run the dev server with hot reloading 22 | 23 | ``` 24 | yarn dev 25 | ``` 26 | 27 | ### Open the Simulator in Backpack 28 | 29 | Now that you have your xNFT dev server running, open it in the Backpack simulator to see it run. 30 | 31 | That's it! 32 | 33 | 34 | ## Build & Publish 35 | 36 | Once you're done and ready to publish, build your xNFT: 37 | 38 | ``` 39 | yarn build 40 | ``` 41 | 42 | Test the newly created build in `dist/index.html` in the simulator: 43 | 44 | ``` 45 | yarn start 46 | ``` 47 | 48 | Once everything looks good head over to [xnft.gg](https://www.xnft.gg) to publish your xNFT! 49 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-xnft", 4 | "slug": "react-native-xnft", 5 | "entryPoint": "./src/App" 6 | } 7 | } -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/ship-an-xnft/solution/assets/icon.png -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "npx xnft native start", 5 | "build": "expo export:web", 6 | "dev": "expo start --web & npx xnft dev --iframe http://localhost:19006" 7 | }, 8 | "dependencies": { 9 | "@coral-xyz/common-public": "^0.2.0-latest.1931", 10 | "@expo-google-fonts/dev": "*", 11 | "@expo/vector-icons": "^13.0.0", 12 | "@react-native-async-storage/async-storage": "^1.17.11", 13 | "@react-navigation/bottom-tabs": "6.3.1", 14 | "@react-navigation/native": "6.0.10", 15 | "@react-navigation/native-stack": "6.6.1", 16 | "@react-navigation/stack": "6.2.1", 17 | "@solana/web3.js": "^1.73.0", 18 | "expo": "~47.0.8", 19 | "expo-linking": "~3.3.0", 20 | "react": "18.1.0", 21 | "react-dom": "18.1.0", 22 | "react-native": "0.70.5", 23 | "react-native-gesture-handler": "~2.8.0", 24 | "react-native-safe-area-context": "4.4.1", 25 | "react-native-screens": "~3.18.0", 26 | "react-native-web": "~0.18.9", 27 | "recoil": "*", 28 | "twrnc": "*" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.20.12", 32 | "@expo/webpack-config": "^0.17.2", 33 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 34 | "@types/react": "~18.0.26", 35 | "@types/react-native": "~0.71.0", 36 | "react-refresh": "^0.14.0", 37 | "typescript": "^4.9.4", 38 | "webpack-hot-middleware": "^2.25.3", 39 | "xnft": "latest" 40 | }, 41 | "resolutions": { 42 | "react-error-overlay": "6.0.9" 43 | }, 44 | "private": true, 45 | "engines": { 46 | "node": "<17" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | import React from 'react'; 3 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 4 | import { NavigationContainer } from '@react-navigation/native'; 5 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 6 | 7 | import HomeScreen from './screens/FriendsScreen'; 8 | import FriendsScreen from './screens/HomeScreen'; 9 | 10 | const Tab = createBottomTabNavigator(); 11 | 12 | declare global { 13 | interface Window { 14 | xnft: any; 15 | } 16 | } 17 | 18 | function App() { 19 | function TabNavigator() { 20 | return ( 21 | 27 | ( 34 | 39 | ), 40 | }} 41 | /> 42 | ( 49 | 50 | ), 51 | }} 52 | /> 53 | 54 | ); 55 | } 56 | 57 | return ( 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default registerRootComponent(App); 65 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/src/screens/FriendsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, TextInput, Button } from 'react-native'; 2 | import tw from 'twrnc'; 3 | import { useEffect, useState } from 'react'; 4 | import AsyncStorage from '@react-native-async-storage/async-storage'; 5 | 6 | function FriendsScreen() { 7 | const [addFriend, setAddFriend] = useState({ name: '', pubkey: '' }); 8 | const [friends, setFriends] = useState([]); 9 | 10 | useEffect(() => { 11 | const getFriends = async () => { 12 | const friends = await AsyncStorage.getItem('friends'); 13 | console.log('friends', friends); 14 | if (friends) { 15 | setFriends(JSON.parse(friends)); 16 | } 17 | }; 18 | getFriends(); 19 | }, []); 20 | 21 | const addFriendToLS = () => { 22 | AsyncStorage.setItem('friends', JSON.stringify([...friends, addFriend])); 23 | setAddFriend({ name: '', pubkey: '' }); 24 | }; 25 | 26 | const removeAllFriends = () => { 27 | AsyncStorage.removeItem('friends'); 28 | setFriends([]); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | Friends 35 | 36 | setAddFriend({ ...addFriend, pubkey: e })} 38 | style={tw`text-white text-xl p-2 bg-[#386FA4] rounded-lg`} 39 | placeholder={'Friends Pubkey'} 40 | value={addFriend.pubkey} 41 | /> 42 | setAddFriend({ ...addFriend, name: e })} 44 | style={tw`text-white text-xl p-2 bg-[#386FA4] rounded-lg mt-3`} 45 | placeholder={'Friends Name'} 46 | value={addFriend.name} 47 | /> 48 | 49 | 54 |

55 |
56 | 57 | 58 |
59 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/solution/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createExpoWebpackConfigAsync = require("@expo/webpack-config"); 2 | const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); 3 | 4 | const fs = require("fs"); 5 | 6 | module.exports = async function (env, argv) { 7 | const config = await createExpoWebpackConfigAsync(env, argv); 8 | 9 | if (env.mode === "development") { 10 | config.plugins.push(new ReactRefreshWebpackPlugin()); 11 | // keep everything else the same for expo start 12 | return config; 13 | } 14 | 15 | config.output = { 16 | globalObject: "this", 17 | path: __dirname + "/dist/.artifacts/", 18 | filename: "index.js", 19 | }; 20 | 21 | config.optimization.splitChunks = { 22 | cacheGroups: { 23 | default: false, 24 | }, 25 | }; 26 | config.optimization.runtimeChunk = false; 27 | 28 | config.plugins = config.plugins.filter((plugin) => 29 | ["DefinePlugin", "CleanWebpackPlugin"].includes(plugin.constructor.name) 30 | ); 31 | 32 | config.plugins.push( 33 | new InlineJSPlugin({ 34 | template: "template.html", 35 | filename: "index.html", 36 | }) 37 | ); 38 | 39 | // this is brittle but works for now. 40 | const loaders = config.module.rules.find( 41 | (rule) => typeof rule.oneOf !== "undefined" 42 | ); 43 | const urlLoader = loaders.oneOf.find( 44 | (loader) => 45 | typeof loader.use === "object" && 46 | loader.use.loader && 47 | loader.use.loader.includes("url-loader") 48 | ); 49 | 50 | urlLoader.use.options.limit = true; 51 | urlLoader.test = /\.(gif|jpe?g|png|svg|css|woff2?|eot|ttf|otf)$/; 52 | 53 | return config; 54 | }; 55 | 56 | // const logger = console.log.bind(console); 57 | 58 | class InlineJSPlugin { 59 | constructor({ template, filename }) { 60 | this.options = { 61 | template, 62 | filename, 63 | }; 64 | } 65 | apply(compiler) { 66 | compiler.hooks.done.tap("InlineJSPlugin", (stats) => { 67 | const filename = stats.compilation.outputOptions.filename; 68 | const path = stats.compilation.outputOptions.path; 69 | const asset = stats.compilation.assets[filename]; 70 | const JSBundle = asset.children[0]._value; 71 | const template = fs 72 | .readFileSync(this.options.template) 73 | .toString() 74 | .split("####JS####"); 75 | fs.writeFileSync( 76 | path + "/../" + this.options.filename, 77 | template[0] + JSBundle + template[1] 78 | ); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/* 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | yarn.lock 13 | yarn-error.log 14 | 15 | # macOS 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/README.md: -------------------------------------------------------------------------------- 1 | # xnft-quickstart 2 | 3 | Quickstart repo for building your own xNFT. 4 | 5 | ## Developing 6 | 7 | Once you've installed Backpack, get started building your xNFT with these steps. Note that the packages here will always use the latest, which correspond to the latest tagged build of Backpack. If you have unexepected issues, make sure your package versions match the app version. 8 | 9 | Further documentation: https://docs.xnfts.dev/getting-started/getting-started 10 | 11 | ### Install 12 | 13 | First, install dependencies. 14 | 15 | ``` 16 | yarn 17 | ``` 18 | 19 | ### Run the dev server 20 | 21 | Then, run the dev server with hot reloading 22 | 23 | ``` 24 | yarn dev 25 | ``` 26 | 27 | ### Open the Simulator in Backpack 28 | 29 | Now that you have your xNFT dev server running, open it in the Backpack simulator to see it run. 30 | 31 | That's it! 32 | 33 | 34 | ## Build & Publish 35 | 36 | Once you're done and ready to publish, build your xNFT: 37 | 38 | ``` 39 | yarn build 40 | ``` 41 | 42 | Test the newly created build in `dist/index.html` in the simulator: 43 | 44 | ``` 45 | yarn start 46 | ``` 47 | 48 | Once everything looks good head over to [xnft.gg](https://www.xnft.gg) to publish your xNFT! 49 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-xnft", 4 | "slug": "react-native-xnft", 5 | "entryPoint": "./src/App" 6 | } 7 | } -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/workshops/fecde56a6a59cc6d8e627fad15bf05b2cf35dc87/workshops/ship-an-xnft/starter/assets/icon.png -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "npx xnft native start", 5 | "build": "expo export:web", 6 | "dev": "expo start --web & npx xnft dev --iframe http://localhost:19006" 7 | }, 8 | "dependencies": { 9 | "@coral-xyz/common-public": "^0.2.0-latest.1931", 10 | "@expo-google-fonts/dev": "*", 11 | "@expo/vector-icons": "^13.0.0", 12 | "@react-navigation/bottom-tabs": "6.3.1", 13 | "@react-navigation/native": "6.0.10", 14 | "@react-navigation/native-stack": "6.6.1", 15 | "@react-navigation/stack": "6.2.1", 16 | "@solana/web3.js": "^1.73.0", 17 | "expo": "~47.0.8", 18 | "expo-linking": "~3.3.0", 19 | "react": "18.1.0", 20 | "react-dom": "18.1.0", 21 | "react-native": "0.70.5", 22 | "react-native-gesture-handler": "~2.8.0", 23 | "react-native-safe-area-context": "4.4.1", 24 | "react-native-screens": "~3.18.0", 25 | "react-native-web": "~0.18.9", 26 | "recoil": "*", 27 | "twrnc": "*" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.20.12", 31 | "@expo/webpack-config": "^0.17.2", 32 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 33 | "@types/react": "~18.0.26", 34 | "@types/react-native": "~0.71.0", 35 | "react-refresh": "^0.14.0", 36 | "typescript": "^4.9.4", 37 | "webpack-hot-middleware": "^2.25.3", 38 | "xnft": "latest" 39 | }, 40 | "resolutions": { 41 | "react-error-overlay": "6.0.9" 42 | }, 43 | "private": true, 44 | "engines": { 45 | "node": "<17" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { registerRootComponent } from 'expo'; 3 | import { View, Text } from 'react-native'; 4 | 5 | function App() { 6 | return ( 7 | 8 | Hello world! 9 | 10 | ); 11 | } 12 | 13 | export default registerRootComponent(App); 14 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | xNFT 10 | 11 | 46 | 47 | 48 | 58 |
59 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workshops/ship-an-xnft/starter/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createExpoWebpackConfigAsync = require("@expo/webpack-config"); 2 | const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); 3 | 4 | const fs = require("fs"); 5 | 6 | module.exports = async function (env, argv) { 7 | const config = await createExpoWebpackConfigAsync(env, argv); 8 | 9 | if (env.mode === "development") { 10 | config.plugins.push(new ReactRefreshWebpackPlugin()); 11 | // keep everything else the same for expo start 12 | return config; 13 | } 14 | 15 | config.output = { 16 | globalObject: "this", 17 | path: __dirname + "/dist/.artifacts/", 18 | filename: "index.js", 19 | }; 20 | 21 | config.optimization.splitChunks = { 22 | cacheGroups: { 23 | default: false, 24 | }, 25 | }; 26 | config.optimization.runtimeChunk = false; 27 | 28 | config.plugins = config.plugins.filter((plugin) => 29 | ["DefinePlugin", "CleanWebpackPlugin"].includes(plugin.constructor.name) 30 | ); 31 | 32 | config.plugins.push( 33 | new InlineJSPlugin({ 34 | template: "template.html", 35 | filename: "index.html", 36 | }) 37 | ); 38 | 39 | // this is brittle but works for now. 40 | const loaders = config.module.rules.find( 41 | (rule) => typeof rule.oneOf !== "undefined" 42 | ); 43 | const urlLoader = loaders.oneOf.find( 44 | (loader) => 45 | typeof loader.use === "object" && 46 | loader.use.loader && 47 | loader.use.loader.includes("url-loader") 48 | ); 49 | 50 | urlLoader.use.options.limit = true; 51 | urlLoader.test = /\.(gif|jpe?g|png|svg|css|woff2?|eot|ttf|otf)$/; 52 | 53 | return config; 54 | }; 55 | 56 | // const logger = console.log.bind(console); 57 | 58 | class InlineJSPlugin { 59 | constructor({ template, filename }) { 60 | this.options = { 61 | template, 62 | filename, 63 | }; 64 | } 65 | apply(compiler) { 66 | compiler.hooks.done.tap("InlineJSPlugin", (stats) => { 67 | const filename = stats.compilation.outputOptions.filename; 68 | const path = stats.compilation.outputOptions.path; 69 | const asset = stats.compilation.assets[filename]; 70 | const JSBundle = asset.children[0]._value; 71 | const template = fs 72 | .readFileSync(this.options.template) 73 | .toString() 74 | .split("####JS####"); 75 | fs.writeFileSync( 76 | path + "/../" + this.options.filename, 77 | template[0] + JSBundle + template[1] 78 | ); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /workshops/solana-journal/README.md: -------------------------------------------------------------------------------- 1 | # Solana Journal 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | Coming soon! | Coming soon! | 7 | 8 | ## 📗 Learn -------------------------------------------------------------------------------- /workshops/solana-twitter/README.md: -------------------------------------------------------------------------------- 1 | # Solana Twitter 2 | 3 | ## 🎬 Recorded Sessions 4 | | Link | Instructor | Event | 5 | | ---- | ---------- | ----- | 6 | | [youtube](https://github.com/solana-developers) | Coming soon! | Coming soon! | 7 | 8 | ## 📗 Learn -------------------------------------------------------------------------------- /workshops/storefront-solanapay/.gitignore: -------------------------------------------------------------------------------- 1 | /*/**/node_modules 2 | /*/**/target -------------------------------------------------------------------------------- /workshops/storefront-solanapay/CONDUCTING.md: -------------------------------------------------------------------------------- 1 | # How to Conduct This Workshop 2 | 3 | Participants do not need to have Solana installed to participate in this workshop. They need only Node JS. 4 | 5 | ### **Step 1** (`starter`): Add a Solana Pay QR for transferring SOL 6 | 7 | We start with the shell of the UI - a Pizza storefront where you can add ingredients to your pizza. 8 | 9 | The section for the QR code is rendered, but the QR code doesn't come with it yet. 10 | 11 | We go from having no Solana Pay imports/code whatsoever to having a working SOL transfer QR code. 12 | 13 | ### **Step 2** (`step-2`): Implement other SPL tokens to the transaction 14 | 15 | Now that we have a working QR code that can do a transfer of SOL, we can start to build on our transaction. 16 | 17 | We'll go ahead and add support for two SPL tokens, whose mint addresses are hard-coded in the source, and below: 18 | * Simulated USDC: `EvLepoDXhscvLxbTQ7byj3NE6n6gSNJP3DeZx5k49uLm` 19 | * Simulated BONK: `EvLepoDXhscvLxbTQ7byj3NE6n6gSNJP3DeZx5k49uLm` 20 | 21 | ### **Step 3** (`step-3`): Implement a custom program instruction to the transaction 22 | 23 | Now that we've got multiple tokens and a new token transfer instruction included in our transaction, we can now add custom program instructions as well! 24 | 25 | We'll do exactly that in this step: Add the custom instruction to write the Pizza Order to the chain. 26 | 27 | ### **Step 4** (`solution`): You made it! -------------------------------------------------------------------------------- /workshops/storefront-solanapay/README.md: -------------------------------------------------------------------------------- 1 | # A straightforward guide to Solana Pay Transaction Requests 📲 2 | # 🍕 Build a Storefront 3 | 4 | ## 🎬 Recorded Sessions 5 | 6 | | Link | Instructor | Type | 7 | | -------------------------------------------------------------------------------------------------------------------------------------- | --------------- | --------------- | 8 | | [youtube Video](https://www.youtube.com/watch?v=FQYmWWw5l04) | Joe Caufield | Hacker House Recording | 9 | 10 | ## 😍 Why I like Solana Pay, and why you should too. 11 | 12 | Solana Pay is freakin amazing. Have you ever scanned a QR Code on PayPal, Cashapp or a Wallet to send some money to your friends and thought "Oh damn this is pretty smooth". Well Solana Pay is gonna blow your mind. But there's two different "features" in [Solana Pay](https://solanapay.com/), let's go into them: 13 | First of all what you should know that Solana Pay is actually a URL that is read by wallets with an end goal of it's user signing a transaction on the Solana network. This URL can be read by a wallet by you scanning a QR Code for example. Let's look at the differences between these URLs. 14 | - Transfer Requests: A transfer request is pretty easy to understand: it's a transfer request. Let's look at one of these URLs: ` 15 | solana:?amount=&spl-token=&reference=&label=