diff --git a/.github/workflows/templates.json b/.github/workflows/templates.json index c749383..38c3737 100644 --- a/.github/workflows/templates.json +++ b/.github/workflows/templates.json @@ -9,6 +9,7 @@ "mobile/kotlin-compose-minimal", "mobile/react-kit-shadcn", "mobile/sample-expo-kit-cause-pots", + "mobile/sample-expo-kit-farming-idle", "mobile/sample-expo-kit-privy", "mobile/sample-react-kit-privy", "mobile/typescript-kit-minimal" diff --git a/TEMPLATES.md b/TEMPLATES.md index 38569ad..c3425d8 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -82,6 +82,14 @@ Solana Mobile Templates `anchor` `expo` `mobile-wallet-adapter` `react-native` `sample` `solana-kit` `tailwind` `uniwind` +### [sample-expo-kit-farming-idle](mobile/sample-expo-kit-farming-idle) + +`gh:solana-mobile/templates/mobile/sample-expo-kit-farming-idle` + +> A sample idle farming game with a burner player wallet, on-chain upgrades, and a global leaderboard, built with Expo, React Native, Solana Kit, Anchor, and Uniwind. + +`anchor` `expo` `mobile-wallet-adapter` `react-native` `sample` `solana-kit` `tailwind` `uniwind` + ### [sample-expo-kit-privy](mobile/sample-expo-kit-privy) `gh:solana-mobile/templates/mobile/sample-expo-kit-privy` diff --git a/mobile/sample-expo-kit-farming-idle/.gitignore b/mobile/sample-expo-kit-farming-idle/.gitignore new file mode 100644 index 0000000..dbced89 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/.gitignore @@ -0,0 +1,60 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local +.env + +# typescript +*.tsbuildinfo + +app-example + +# generated native folders +/ios +/android + +# Anchor +/anchor/.anchor +/anchor/.surfpool +/anchor/target/.rustc_info.json +/anchor/target/CACHEDIR.TAG +/anchor/target/debug +/anchor/target/deploy +/anchor/target/flycheck* +/anchor/target/package +/anchor/target/release +/anchor/target/sbf-solana-solana +/anchor/target/sbpf-solana-solana +/anchor/target/test-ledger +/anchor/target/tmp +**/*.rs.bk +test-ledger diff --git a/mobile/sample-expo-kit-farming-idle/.prettierignore b/mobile/sample-expo-kit-farming-idle/.prettierignore new file mode 100644 index 0000000..1cdece6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/.prettierignore @@ -0,0 +1,14 @@ +# Add files here to ignore them from prettier formatting +/.expo +/android +/coverage +/dist +/ios +/tmp +/anchor/.anchor +/anchor/src/client/js/generated +/anchor/target +pnpm-lock.yaml +.agents +agent +src/uniwind-types.d.ts diff --git a/mobile/sample-expo-kit-farming-idle/.prettierrc b/mobile/sample-expo-kit-farming-idle/.prettierrc new file mode 100644 index 0000000..d6c3437 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/.prettierrc @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "printWidth": 120, + "semi": false, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/mobile/sample-expo-kit-farming-idle/LICENSE b/mobile/sample-expo-kit-farming-idle/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mobile/sample-expo-kit-farming-idle/README.md b/mobile/sample-expo-kit-farming-idle/README.md new file mode 100644 index 0000000..8c505d5 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/README.md @@ -0,0 +1,132 @@ +# sample-expo-kit-farming-idle + +Farming Idle is a **sample**, not a template: a complete reference app to read, run, and copy the parts you need. Templates in this repository are starting points; this app shows what a finished feature set looks like on the [expo-kit-anchor](../expo-kit-anchor) stack — an idle clicker game on Solana where every harvest is a real transaction, rebuilt from the classic [FarmingIdleGame tutorial app](https://github.com/solana-mobile/tutorial-apps/tree/main/FarmingIdleGame). + +The pattern worth copying is the **two-wallet split**: the connected wallet (the _owner_) holds the funds and approves the few transactions that matter, while a burner keypair generated on the device (the _player_) signs the high-frequency gameplay transactions with no approval prompts. That is what makes a tap-to-harvest game playable when every tap hits the chain. + +## Features + +- Tap to harvest: every harvest is an on-chain transaction signed silently by the player wallet +- Eight crop upgrades that keep farming per second between harvests, priced with compounding costs +- A global on-chain top-5 leaderboard; submitting a score resets the farm for the next run +- A wallet screen that moves gas money between the owner and player wallets and can rotate the burner keypair +- No backend: the farm, the leaderboard, and the math all live in the Anchor program + +## Technologies + +- [Anchor](https://www.anchor-lang.com/) (Solana program framework) +- [Expo](https://expo.dev) +- [Uniwind](https://uniwind.dev/) (Tailwind CSS for React Native) +- [@solana/kit](https://www.solanakit.com/) +- [@wallet-ui/react-native-kit](https://wallet-ui.dev/) + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + +This step builds the dependencies for the development client. + +```bash +npm run android +``` + +The app connects to devnet by default, where the program is already deployed — you don't need the Rust toolchain to run it. Your wallet needs a small amount of devnet SOL for the farm account and the player wallet's gas deposit; get some at [faucet.solana.com](https://faucet.solana.com). + +## The Anchor program + +The `anchor` directory contains the `farming_idle` program, deployed on devnet at [`Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7`](https://explorer.solana.com/address/Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7?cluster=devnet). Five instructions cover the whole game: + +| Instruction | Behavior | +| ------------------------ | -------------------------------------------------------------------------------------------------------------- | +| `initialize_farm` | Creates the farm PDA and tops up the player wallet with 0.01 SOL of gas money; owner and player both sign | +| `harvest` | Banks 1 point plus everything the crops produced since the last harvest; player signs alone | +| `upgrade_farm` | Spends banked points on crop plots, with each plot 15% more expensive than the last; player signs alone | +| `initialize_leaderboard` | Creates the single global leaderboard account; anyone can pay for it once per cluster | +| `submit_farm` | Puts the run's score on the leaderboard if it beats the lowest seat, then resets the farm; owner + player sign | + +PDA seeds: + +- Farm: `["farm", player, owner]` +- Leaderboard: `["leaderboard"]` + +Rules enforced on-chain: + +- Every farm instruction re-derives the PDA from the farm's own `player` and `owner` fields, so a farm from another pair can't be passed in +- Harvest yields derive from `Clock::get()`, never from a client-supplied duration +- Upgrade costs use integer math (15% compounding, floored per step) that the TypeScript client mirrors exactly — see `anchor/src/client/js/farming-idle.ts` +- `submit_farm` requires the owner's signature too: the leaderboard records the owner wallet, so the burner key alone can't speak in its name + +Working on the program requires the [Rust](https://www.rust-lang.org/tools/install), [Solana](https://solana.com/docs/intro/installation), and [Anchor](https://www.anchor-lang.com/docs/installation) toolchains. + +1. Generate a program keypair and sync the program ID + + This generates your own keypair, syncs it into `Anchor.toml` and `declare_id!`, rebuilds the program so the IDL carries the new address, and regenerates the TypeScript client from it: + + ```bash + npm run anchor:setup + ``` + +2. Build and test the program + + ```bash + npm run anchor:test + ``` + + This builds the program, spins up a local validator, deploys, and runs the [Vitest](https://vitest.dev/) tests in `anchor/tests`, including the rejection paths: harvesting with the wrong player and buying upgrades the farm can't afford. + +3. Deploy your own instance to devnet + + ```bash + npm run anchor:deploy:devnet + ``` + +### The generated TypeScript client + +The app talks to the program through a fully typed client generated by [Codama](https://github.com/codama-idl/codama) from the program's IDL. It lives in `anchor/src/client/js/generated` and only depends on `@solana/kit`. Import it anywhere in the app via the `@project/anchor` alias: + +```ts +import { fetchMaybeFarm, findFarmPda, getHarvestInstruction } from '@project/anchor' +``` + +After changing the program, rebuild the IDL and regenerate the client: + +```bash +npm run anchor:build +npm run codama:js +``` + +Add hand-written wrappers around the generated code in `anchor/src/client/js/index.ts` — the `generated` directory is overwritten on every run. This sample keeps the shared game math there (`farming-idle.ts`), so the app and the tests compute the same costs and yields as the program. + +## How the app is put together + +- **Player wallet** (`src/features/player`) is the heart of the sample: a keypair created from 32 random bytes, kept in the device keystore with `expo-secure-store`, one per owner wallet, and turned into a `KeyPairSigner` with `createKeyPairSignerFromPrivateKeyBytes`. Resetting it deletes the stored key and generates a new one, which makes the existing farm and any SOL left in the player wallet unreachable — the app asks you to confirm, but withdraw first. +- **Transactions** (`src/features/transactions`) has the two signing paths side by side: `use-send-player-instructions.ts` signs with the local keypair and submits straight to the RPC (no prompts), while `use-send-owner-instructions.ts` goes through Mobile Wallet Adapter — with the player keypair co-signing as a partial signer when an instruction needs both, as `initialize_farm` and `submit_farm` do. +- **Farm** (`src/features/farm`) reads the farm account with the generated client (`use-farm-query.ts`), estimates the pending harvest client-side on a 250 ms ticker (`use-available-harvest.ts`), and wires all five instructions into mutations (`use-farm-program.ts`). +- **Leaderboard** (`src/features/leaderboard`) fetches the global account and offers to create it on clusters where it doesn't exist yet, such as your own localnet. +- **Wallet** (`src/features/wallet`) moves gas money: deposits go owner → player through the wallet app, withdrawals send the player's balance back without a prompt, since the player wallet signs for itself. + +## Develop against localnet + +For a faster loop you can run everything against a local validator, forwarded to your device over adb: + +1. Start the localnet and keep it running: `npx solana-mobile localnet` +2. Deploy the program to it: `npm run anchor:deploy:localnet` +3. Switch the network to Localnet with the selector in the app +4. Connect your wallet and tap Request Airdrop to fund it + +Two things to know: localnet needs a debug build — `npm run android` makes one, and Android debug builds allow the cleartext `http://localhost` endpoint while release builds only talk HTTPS. And the regular Seeker wallet only signs for public clusters, so signing on localnet requires a localnet-capable wallet such as [fakewallet](https://github.com/solana-mobile/mobile-wallet-adapter/tree/main/android/fakewallet). + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Uniwind documentation](https://uniwind.dev/): Learn how to style your app with Tailwind CSS. +- [Anchor documentation](https://www.anchor-lang.com/docs): Learn how to build Solana programs with Anchor. +- [Solana documentation](https://solana.com/docs): Learn how to build on Solana. diff --git a/mobile/sample-expo-kit-farming-idle/anchor/.prettierignore b/mobile/sample-expo-kit-farming-idle/anchor/.prettierignore new file mode 100644 index 0000000..4142583 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/.prettierignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/mobile/sample-expo-kit-farming-idle/anchor/Anchor.toml b/mobile/sample-expo-kit-farming-idle/anchor/Anchor.toml new file mode 100644 index 0000000..9076848 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/Anchor.toml @@ -0,0 +1,21 @@ +[toolchain] +package_manager = "pnpm" +solana_version = "3.1.10" + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +farming_idle = "Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +# The default 5s timeout is enough for Surfpool, but not for the slower validator +# that `anchor test --validator legacy` starts, which warms up on first invoke. +test = "npx vitest run --testTimeout=60000" + +[hooks] diff --git a/mobile/sample-expo-kit-farming-idle/anchor/Cargo.lock b/mobile/sample-expo-kit-farming-idle/anchor/Cargo.lock new file mode 100644 index 0000000..e81ba9f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/Cargo.lock @@ -0,0 +1,1632 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc4b6b3c3f3e37a0b7d537e03a36bfb0415ee11b4443fa4190437cf26a874b6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-attribute-account" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e8b1468add67e6a69732883e58d23a18be51538994adaae69c9b3fe967e6be" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-attribute-constant" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e1e3ade39ba05716ddc3b792859d838cccf43f5f4913ae8a163de7a2ed7f7e" +dependencies = [ + "anchor-syn", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-attribute-error" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e026f0ff09d740fd1821bbf97d9ac649c123ff92f04b9197b002494af4acf1" +dependencies = [ + "anchor-syn", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-attribute-event" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aafc8e26eb16a0c4129661f6857c42f301859fc2ee10f4e5287d571f0ea07f5" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-attribute-program" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844e657ffe049b073389efb843bb281d08579b8a47ef294365f729b35ed3dca" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-derive-accounts" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c87a6769d7cf2deed8d3351d6d1d8aa05a3a07751f1418ab6b34aaa307bd34b" +dependencies = [ + "anchor-syn", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-derive-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f014d99530ae48a81f710f150688c5c542fdeee3af8998010e147a64d7a697c" +dependencies = [ + "anchor-syn", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-derive-space" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ba30c2d844e7440c491ad5f3e25f93115c6c9319df480eda460ce96030832b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "anchor-lang" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6708f356398752b8d7a08fb701c9c17395e103786fb1b33615a7267a6f4774" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-error", + "anchor-lang-idl", + "base64 0.21.7", + "bincode", + "borsh", + "bytemuck", + "const-crypto", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall 3.0.0", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-stake-interface", + "solana-system-interface 2.0.0", + "solana-sysvar", + "solana-sysvar-id", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang-error" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20760a8d5b7ddd1aea7a347c43c702ea4e962ccb7b9908c952e4b372b2d9e1f7" +dependencies = [ + "anchor-attribute-error", + "borsh", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47914b4290ae2bdf4ec203aa821e6eba86d7c78ef497918938038dcc6919f953" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f6c61ef5b47db60700087bb6e419284b3461ba8f59040e1f6827aa9e9a5bc4" +dependencies = [ + "anyhow", + "bs58", + "cargo_toml", + "heck", + "proc-macro2", + "quote", + "serde", + "sha2 0.11.0", + "syn 2.0.119", + "thiserror 1.0.69", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "const-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" +dependencies = [ + "keccak-const", + "sha2-const-stable", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "farming_idle" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solana-account-info" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" +dependencies = [ + "solana-address 2.7.0", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.7.0", +] + +[[package]] +name = "solana-address" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01332a01c0a3098404d55a724c8d9a92aed4a50fe40a7dd0c7a51e29274c14de" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "serde", + "serde_derive", + "sha2-const-stable", + "solana-atomic-u64", + "solana-define-syscall 5.2.0", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", + "wincode", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085db4906d89324cef2a30840d59eaecf3d4231c560ec7c9f6614a93c652f501" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7708bdb262fd9c257c3a56d4289297b10a3e821b8cba3f7cf42b49c40201ab9" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction", + "solana-program-error", + "solana-pubkey 4.3.0", + "solana-stable-layout", +] + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-define-syscall" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8209ece2bd9f1450e672858ffc0e5c8c786ff6916d2a862b126dd0128f380f" + +[[package]] +name = "solana-epoch-rewards" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0788d74ee15778deecaa15ed1a1e37727ba954f86cbc35225450a1f2b5012969" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1633cfd10cde127f2caf8f12021b4f8e9a425e7e4eea4326e428c422376d6fd" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-program-error", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731" +dependencies = [ + "solana-program-error", + "solana-pubkey 4.3.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4bf4f4afb4704212ef1d994ee65bef7293441721639dfd16f0e60996f37be51" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address 2.7.0", + "solana-define-syscall 5.2.0", + "solana-program-error", +] + +[[package]] +name = "solana-hash" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df9b01495ed31100aca97a7f5862d5e19ab1636d60d1a9f02391408dd9dec84" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-instruction" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70cf6ece1070a66d25e68274f338f29664794669c175223940a298645ef3495" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-define-syscall 5.2.0", + "solana-instruction-error", + "solana-pubkey 4.3.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e7db78c122d02189619490b1fef04a2a042c389662920617c0e6381fdf8fdd" +dependencies = [ + "num-traits", + "solana-program-error", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0732294560e88ecdb2bbc656e67383e9f88c78ec09469cef172f0d28cd1bcd" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-invoke" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4065031f5c7dd29ef5f5003c1a353011eeabbafa6c5a5033da0cedbfca824b94" +dependencies = [ + "solana-account-info", + "solana-define-syscall 3.0.0", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5099e736c3c06c451b307a60637d69eb65b3144143ebeed899e2fd1134f4fc35" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey 4.3.0", + "solana-sdk-ids", + "solana-system-interface 3.3.0", +] + +[[package]] +name = "solana-msg" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008" +dependencies = [ + "solana-define-syscall 5.2.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.3.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-program-option" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a88006a9b8594088cec9027ab77caaaa258a2aaa2083d3f086c44b42e50aeab" + +[[package]] +name = "solana-program-pack" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f71b7a414de2ced1071f015834e9be581592d013432adbd06d02e4af11eba9" +dependencies = [ + "solana-address 2.7.0", +] + +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.7.0", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "solana-serialize-utils" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" +dependencies = [ + "solana-instruction-error", + "solana-pubkey 4.3.0", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall 4.0.1", + "solana-hash", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d6bc909599eea325c9d09f7ff16ef6166c134876ff47a6a122d693d058dad" +dependencies = [ + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4560fde841a6f2001aedc33b74fe99840ee3d56a71fe9b98ee332303b8597900" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-get-sysvar", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa" +dependencies = [ + "solana-instruction", + "solana-pubkey 4.3.0", +] + +[[package]] +name = "solana-stake-interface" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bc26191b533f9a6e5a14cca05174119819ced680a80febff2f5051a713f0db" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-system-interface 2.0.0", + "solana-sysvar", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-system-interface" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2d5311d0584af231c6c2b0503ba1c3aa50118ead4f503ab215ccbdd1ffac57" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address 2.7.0", + "solana-instruction", + "solana-msg", + "solana-program-error", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64 0.22.1", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.3.0", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.7.0", + "solana-sdk-ids", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wincode" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc6339f1ba427bf7ad7c42403b28e524832ba2ddb5eef1bb2cc3b85db6b7b75" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror 2.0.20", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d582d9fc9d813264407a9e16bfa16846ccf15ef032f9bbcd700741bdc00255" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/mobile/sample-expo-kit-farming-idle/anchor/Cargo.toml b/mobile/sample-expo-kit-farming-idle/anchor/Cargo.toml new file mode 100644 index 0000000..0999ea4 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[workspace.package] +edition = "2021" +rust-version = "1.89.0" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/mobile/sample-expo-kit-farming-idle/anchor/codama.mjs b/mobile/sample-expo-kit-farming-idle/anchor/codama.mjs new file mode 100644 index 0000000..6a7fb48 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/codama.mjs @@ -0,0 +1,12 @@ +export default { + idl: 'target/idl/farming_idle.json', + scripts: { + js: { + from: '@codama/renderers-js', + args: [ + 'anchor/src/client/js', + { generatedFolder: 'generated', kitImportStrategy: 'rootOnly', syncPackageJson: false }, + ], + }, + }, +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/Cargo.toml b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/Cargo.toml new file mode 100644 index 0000000..41a4cf3 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "farming_idle" +version = "0.1.0" +description = "Created with Anchor" +edition.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] +name = "farming_idle" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] +anchor-debug = [] +custom-heap = [] +custom-panic = [] + + +[dependencies] +anchor-lang = "1.1.2" + + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/constants.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/constants.rs new file mode 100644 index 0000000..665ed53 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/constants.rs @@ -0,0 +1,26 @@ +use anchor_lang::prelude::*; + +#[constant] +pub const FARM_SEED: &[u8] = b"farm"; + +#[constant] +pub const LEADERBOARD_SEED: &[u8] = b"leaderboard"; + +/// Harvest points each plot of a crop yields per second. +pub const CROP_POINTS: [u64; CROP_COUNT] = [1, 3, 8, 47, 260, 1_400, 7_800, 44_000]; + +/// Cost in harvest points of the first plot of each crop. +pub const CROP_COSTS: [u64; CROP_COUNT] = [15, 100, 1_100, 12_000, 130_000, 1_400_000, 20_000_000, 330_000_000]; + +/// Each additional plot of the same crop costs 15% more than the previous one. +/// Integer math so the TypeScript client can reproduce costs exactly. +pub const COST_GROWTH_NUMERATOR: u128 = 115; +pub const COST_GROWTH_DENOMINATOR: u128 = 100; + +pub const CROP_COUNT: usize = 8; + +pub const LEADERBOARD_ENTRIES: usize = 5; + +/// Lamports moved from the owner to the player wallet when a farm is created, +/// so the player wallet can pay gameplay transaction fees on its own. +pub const PLAYER_TOP_UP_LAMPORTS: u64 = 10_000_000; diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/error.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/error.rs new file mode 100644 index 0000000..5e00332 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/error.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ErrorCode { + #[msg("Not enough harvest points to fund the upgrade")] + InsufficientHarvestPoints, + #[msg("Amount must be greater than zero")] + InvalidAmount, + #[msg("Not a valid crop index")] + InvalidCrop, + #[msg("Overflow")] + Overflow, +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions.rs new file mode 100644 index 0000000..e6f029e --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions.rs @@ -0,0 +1,11 @@ +pub mod harvest; +pub mod initialize_farm; +pub mod initialize_leaderboard; +pub mod submit_farm; +pub mod upgrade_farm; + +pub use harvest::*; +pub use initialize_farm::*; +pub use initialize_leaderboard::*; +pub use submit_farm::*; +pub use upgrade_farm::*; diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/harvest.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/harvest.rs new file mode 100644 index 0000000..ffd8f53 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/harvest.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{constants::*, state::*}; + +#[derive(Accounts)] +pub struct Harvest<'info> { + // Re-deriving the PDA proves this farm was created for this exact + // player/owner pair and not passed in from another game. + #[account( + mut, + has_one = player, + seeds = [FARM_SEED, farm.player.as_ref(), farm.owner.as_ref()], + bump = farm.bump + )] + pub farm: Account<'info, Farm>, + pub player: Signer<'info>, +} + +pub fn handle_harvest(ctx: Context) -> Result<()> { + let farm = &mut ctx.accounts.farm; + let now = Clock::get()?.unix_timestamp; + + let points = farm.harvest_yield(now); + farm.last_harvested = now; + farm.harvest_points = farm.harvest_points.saturating_add(points); + + Ok(()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_farm.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_farm.rs new file mode 100644 index 0000000..9d8ac2c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_farm.rs @@ -0,0 +1,53 @@ +use anchor_lang::prelude::*; + +use crate::{constants::*, state::*}; + +#[derive(Accounts)] +pub struct InitializeFarm<'info> { + #[account( + init, + payer = owner, + space = 8 + Farm::INIT_SPACE, + seeds = [FARM_SEED, player.key().as_ref(), owner.key().as_ref()], + bump + )] + pub farm: Account<'info, Farm>, + #[account(mut)] + pub owner: Signer<'info>, + #[account(mut)] + pub player: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn handle_initialize_farm(ctx: Context) -> Result<()> { + // Top up the player wallet so it can pay gameplay transaction fees without + // the owner wallet having to approve every harvest and upgrade. + if ctx.accounts.player.lamports() < PLAYER_TOP_UP_LAMPORTS { + let ix = anchor_lang::solana_program::system_instruction::transfer( + &ctx.accounts.owner.key(), + &ctx.accounts.player.key(), + PLAYER_TOP_UP_LAMPORTS, + ); + anchor_lang::solana_program::program::invoke( + &ix, + &[ + ctx.accounts.owner.to_account_info(), + ctx.accounts.player.to_account_info(), + ], + )?; + } + + let now = Clock::get()?.unix_timestamp; + ctx.accounts.farm.set_inner(Farm { + owner: ctx.accounts.owner.key(), + player: ctx.accounts.player.key(), + bump: ctx.bumps.farm, + created_at: now, + last_harvested: now, + harvest_points: 0, + crops: [0; CROP_COUNT], + high_score: 0, + }); + + Ok(()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_leaderboard.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_leaderboard.rs new file mode 100644 index 0000000..8a5985e --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/initialize_leaderboard.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{constants::*, state::*}; + +#[derive(Accounts)] +pub struct InitializeLeaderboard<'info> { + // One global leaderboard per program deployment; anyone can pay to create it. + #[account( + init, + payer = payer, + space = 8 + Leaderboard::INIT_SPACE, + seeds = [LEADERBOARD_SEED], + bump + )] + pub leaderboard: Account<'info, Leaderboard>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn handle_initialize_leaderboard(ctx: Context) -> Result<()> { + ctx.accounts.leaderboard.entries = [LeaderboardEntry { + owner: Pubkey::default(), + points: 0, + }; LEADERBOARD_ENTRIES]; + + Ok(()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/submit_farm.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/submit_farm.rs new file mode 100644 index 0000000..08b790c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/submit_farm.rs @@ -0,0 +1,48 @@ +use anchor_lang::prelude::*; + +use crate::{constants::*, state::*}; + +#[derive(Accounts)] +pub struct SubmitFarm<'info> { + #[account( + mut, + has_one = owner, + has_one = player, + seeds = [FARM_SEED, farm.player.as_ref(), farm.owner.as_ref()], + bump = farm.bump + )] + pub farm: Account<'info, Farm>, + #[account(mut, seeds = [LEADERBOARD_SEED], bump)] + pub leaderboard: Account<'info, Leaderboard>, + // The owner signs too: the leaderboard records the owner wallet, so a + // burner player key alone must not be able to submit in the owner's name. + pub owner: Signer<'info>, + pub player: Signer<'info>, +} + +pub fn handle_submit_farm(ctx: Context) -> Result<()> { + let farm = &mut ctx.accounts.farm; + let score = farm.harvest_points; + + if score > farm.high_score { + farm.high_score = score; + } + + // Claim the seat of the lowest-scoring entry if this run beats it. + let leaderboard = &mut ctx.accounts.leaderboard; + if let Some(lowest) = leaderboard.entries.iter_mut().min_by_key(|entry| entry.points) { + if score > lowest.points { + *lowest = LeaderboardEntry { + owner: ctx.accounts.owner.key(), + points: score, + }; + } + } + + // Submitting ends the run: the farm starts over. + farm.last_harvested = Clock::get()?.unix_timestamp; + farm.harvest_points = 0; + farm.crops = [0; CROP_COUNT]; + + Ok(()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/upgrade_farm.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/upgrade_farm.rs new file mode 100644 index 0000000..01bd0b6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/instructions/upgrade_farm.rs @@ -0,0 +1,31 @@ +use anchor_lang::prelude::*; + +use crate::{constants::*, error::ErrorCode, state::*}; + +#[derive(Accounts)] +pub struct UpgradeFarm<'info> { + #[account( + mut, + has_one = player, + seeds = [FARM_SEED, farm.player.as_ref(), farm.owner.as_ref()], + bump = farm.bump + )] + pub farm: Account<'info, Farm>, + pub player: Signer<'info>, +} + +pub fn handle_upgrade_farm(ctx: Context, crop_index: u8, amount: u8) -> Result<()> { + require!(amount > 0, ErrorCode::InvalidAmount); + let index = crop_index as usize; + require!(index < CROP_COUNT, ErrorCode::InvalidCrop); + + let farm = &mut ctx.accounts.farm; + let owned = farm.crops[index]; + let cost = Farm::upgrade_cost(CROP_COSTS[index], owned, amount); + require!(farm.harvest_points >= cost, ErrorCode::InsufficientHarvestPoints); + + farm.harvest_points -= cost; + farm.crops[index] = owned.checked_add(amount as u16).ok_or(ErrorCode::Overflow)?; + + Ok(()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/lib.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/lib.rs new file mode 100644 index 0000000..904148e --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/lib.rs @@ -0,0 +1,37 @@ +pub mod constants; +pub mod error; +pub mod instructions; +pub mod state; + +use anchor_lang::prelude::*; + +pub use constants::*; +pub use instructions::*; +pub use state::*; + +declare_id!("Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7"); + +#[program] +pub mod farming_idle { + use super::*; + + pub fn harvest(ctx: Context) -> Result<()> { + crate::instructions::harvest::handle_harvest(ctx) + } + + pub fn initialize_farm(ctx: Context) -> Result<()> { + crate::instructions::initialize_farm::handle_initialize_farm(ctx) + } + + pub fn initialize_leaderboard(ctx: Context) -> Result<()> { + crate::instructions::initialize_leaderboard::handle_initialize_leaderboard(ctx) + } + + pub fn submit_farm(ctx: Context) -> Result<()> { + crate::instructions::submit_farm::handle_submit_farm(ctx) + } + + pub fn upgrade_farm(ctx: Context, crop_index: u8, amount: u8) -> Result<()> { + crate::instructions::upgrade_farm::handle_upgrade_farm(ctx, crop_index, amount) + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/state.rs b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/state.rs new file mode 100644 index 0000000..a9a4dd2 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/programs/farming_idle/src/state.rs @@ -0,0 +1,64 @@ +use anchor_lang::prelude::*; + +use crate::constants::*; + +#[account] +#[derive(InitSpace)] +pub struct Farm { + /// The wallet that created the farm and holds the funds (signs via MWA). + pub owner: Pubkey, + /// The local burner wallet that signs gameplay transactions without approval prompts. + pub player: Pubkey, + pub bump: u8, + pub created_at: i64, + pub last_harvested: i64, + pub harvest_points: u64, + /// Plots owned per crop, indexed like CROP_POINTS and CROP_COSTS. + pub crops: [u16; CROP_COUNT], + pub high_score: u64, +} + +impl Farm { + /// Points a harvest yields: 1 for the tap itself, plus what every plot + /// produced per second since the last harvest. + pub fn harvest_yield(&self, now: i64) -> u64 { + let elapsed = now.saturating_sub(self.last_harvested).max(0) as u64; + let mut points: u64 = 1; + for (index, plots) in self.crops.iter().enumerate() { + points = points.saturating_add((*plots as u64).saturating_mul(CROP_POINTS[index]).saturating_mul(elapsed)); + } + points + } + + /// Total cost of the next `amount` plots of a crop, starting from `owned` plots. + pub fn upgrade_cost(base_cost: u64, owned: u16, amount: u8) -> u64 { + let mut total: u64 = 0; + for n in 0..amount as u16 { + total = total.saturating_add(Self::nth_plot_cost(base_cost, owned.saturating_add(n))); + } + total + } + + /// Cost of a crop's `n`th plot: the base cost compounded by 15% per plot + /// already owned, in integer math (each step floors). + pub fn nth_plot_cost(base_cost: u64, n: u16) -> u64 { + let mut cost = base_cost as u128; + for _ in 0..n { + cost = cost.saturating_mul(COST_GROWTH_NUMERATOR) / COST_GROWTH_DENOMINATOR; + } + cost.min(u64::MAX as u128) as u64 + } +} + +#[account] +#[derive(InitSpace)] +pub struct Leaderboard { + pub entries: [LeaderboardEntry; LEADERBOARD_ENTRIES], +} + +#[derive(AnchorDeserialize, AnchorSerialize, Clone, Copy, Debug, Eq, InitSpace, PartialEq)] +pub struct LeaderboardEntry { + /// The owner wallet behind the farm, not the burner player wallet. + pub owner: Pubkey, + pub points: u64, +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/rust-toolchain.toml b/mobile/sample-expo-kit-farming-idle/anchor/rust-toolchain.toml new file mode 100644 index 0000000..cb684c0 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.89.0" +components = ["rustfmt","clippy"] +profile = "minimal" diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/farming-idle.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/farming-idle.ts new file mode 100644 index 0000000..2654a6b --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/farming-idle.ts @@ -0,0 +1,59 @@ +import type { Farm } from './generated' + +/** + * Game math, mirroring the on-chain integer math in + * `programs/farming_idle/src/state.rs` so costs and yields shown in the app + * match what the program computes exactly. + */ + +/** Harvest points each plot of a crop yields per second. */ +export const CROP_POINTS: readonly bigint[] = [1n, 3n, 8n, 47n, 260n, 1_400n, 7_800n, 44_000n] + +/** Cost in harvest points of the first plot of each crop. */ +export const CROP_COSTS: readonly bigint[] = [ + 15n, + 100n, + 1_100n, + 12_000n, + 130_000n, + 1_400_000n, + 20_000_000n, + 330_000_000n, +] + +export const CROP_COUNT = 8 + +const COST_GROWTH_NUMERATOR = 115n +const COST_GROWTH_DENOMINATOR = 100n + +// The program saturates costs at u64::MAX; mirror that here. +const U64_MAX = 2n ** 64n - 1n + +/** Cost of a crop's `n`th plot: 15% compounding per plot already owned, each step floored. */ +export function nthPlotCost(baseCost: bigint, n: number): bigint { + let cost = baseCost + for (let i = 0; i < n; i++) { + cost = (cost * COST_GROWTH_NUMERATOR) / COST_GROWTH_DENOMINATOR + } + return cost > U64_MAX ? U64_MAX : cost +} + +/** Total cost of the next `amount` plots of a crop, starting from `owned` plots. */ +export function upgradeCost(cropIndex: number, owned: number, amount: number): bigint { + let total = 0n + for (let n = 0; n < amount; n++) { + total += nthPlotCost(CROP_COSTS[cropIndex], owned + n) + } + return total > U64_MAX ? U64_MAX : total +} + +/** Harvest points all plots on the farm produce per second. */ +export function pointsPerSecond(crops: readonly number[]): bigint { + return crops.reduce((total, plots, index) => total + BigInt(plots) * CROP_POINTS[index], 0n) +} + +/** Points a harvest would yield right now: 1 for the tap itself, plus per-second production since the last harvest. */ +export function harvestYield(farm: Farm, nowSeconds: number): bigint { + const elapsed = BigInt(Math.max(0, Math.floor(nowSeconds) - Number(farm.lastHarvested))) + return 1n + pointsPerSecond(farm.crops) * elapsed +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/farm.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/farm.ts new file mode 100644 index 0000000..687aa42 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/farm.ts @@ -0,0 +1,173 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + assertAccountExists, + assertAccountsExist, + combineCodec, + decodeAccount, + fetchEncodedAccount, + fetchEncodedAccounts, + fixDecoderSize, + fixEncoderSize, + getAddressDecoder, + getAddressEncoder, + getArrayDecoder, + getArrayEncoder, + getBytesDecoder, + getBytesEncoder, + getI64Decoder, + getI64Encoder, + getStructDecoder, + getStructEncoder, + getU16Decoder, + getU16Encoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Account, + type Address, + type EncodedAccount, + type FetchAccountConfig, + type FetchAccountsConfig, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type MaybeAccount, + type MaybeEncodedAccount, + type ReadonlyUint8Array, +} from '@solana/kit' + +export const FARM_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([161, 156, 211, 253, 250, 64, 53, 250]) + +export function getFarmDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(FARM_DISCRIMINATOR) +} + +export type Farm = { + discriminator: ReadonlyUint8Array + /** The wallet that created the farm and holds the funds (signs via MWA). */ + owner: Address + /** The local burner wallet that signs gameplay transactions without approval prompts. */ + player: Address + bump: number + createdAt: bigint + lastHarvested: bigint + harvestPoints: bigint + /** Plots owned per crop, indexed like CROP_POINTS and CROP_COSTS. */ + crops: Array + highScore: bigint +} + +export type FarmArgs = { + /** The wallet that created the farm and holds the funds (signs via MWA). */ + owner: Address + /** The local burner wallet that signs gameplay transactions without approval prompts. */ + player: Address + bump: number + createdAt: number | bigint + lastHarvested: number | bigint + harvestPoints: number | bigint + /** Plots owned per crop, indexed like CROP_POINTS and CROP_COSTS. */ + crops: Array + highScore: number | bigint +} + +/** Gets the encoder for {@link FarmArgs} account data. */ +export function getFarmEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', fixEncoderSize(getBytesEncoder(), 8)], + ['owner', getAddressEncoder()], + ['player', getAddressEncoder()], + ['bump', getU8Encoder()], + ['createdAt', getI64Encoder()], + ['lastHarvested', getI64Encoder()], + ['harvestPoints', getU64Encoder()], + ['crops', getArrayEncoder(getU16Encoder(), { size: 8 })], + ['highScore', getU64Encoder()], + ]), + (value) => ({ ...value, discriminator: FARM_DISCRIMINATOR }), + ) +} + +/** Gets the decoder for {@link Farm} account data. */ +export function getFarmDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', fixDecoderSize(getBytesDecoder(), 8)], + ['owner', getAddressDecoder()], + ['player', getAddressDecoder()], + ['bump', getU8Decoder()], + ['createdAt', getI64Decoder()], + ['lastHarvested', getI64Decoder()], + ['harvestPoints', getU64Decoder()], + ['crops', getArrayDecoder(getU16Decoder(), { size: 8 })], + ['highScore', getU64Decoder()], + ]) +} + +/** Gets the codec for {@link Farm} account data. */ +export function getFarmCodec(): FixedSizeCodec { + return combineCodec(getFarmEncoder(), getFarmDecoder()) +} + +export function decodeFarm( + encodedAccount: EncodedAccount, +): Account +export function decodeFarm( + encodedAccount: MaybeEncodedAccount, +): MaybeAccount +export function decodeFarm( + encodedAccount: EncodedAccount | MaybeEncodedAccount, +): Account | MaybeAccount { + return decodeAccount(encodedAccount as MaybeEncodedAccount, getFarmDecoder()) +} + +export async function fetchFarm( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig, +): Promise> { + const maybeAccount = await fetchMaybeFarm(rpc, address, config) + assertAccountExists(maybeAccount) + return maybeAccount +} + +export async function fetchMaybeFarm( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig, +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config) + return decodeFarm(maybeAccount) +} + +export async function fetchAllFarm( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig, +): Promise[]> { + const maybeAccounts = await fetchAllMaybeFarm(rpc, addresses, config) + assertAccountsExist(maybeAccounts) + return maybeAccounts +} + +export async function fetchAllMaybeFarm( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig, +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config) + return maybeAccounts.map((maybeAccount) => decodeFarm(maybeAccount)) +} + +export function getFarmSize(): number { + return 121 +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/index.ts new file mode 100644 index 0000000..c23e757 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/index.ts @@ -0,0 +1,10 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './farm' +export * from './leaderboard' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/leaderboard.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/leaderboard.ts new file mode 100644 index 0000000..41503f7 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/accounts/leaderboard.ts @@ -0,0 +1,130 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + assertAccountExists, + assertAccountsExist, + combineCodec, + decodeAccount, + fetchEncodedAccount, + fetchEncodedAccounts, + fixDecoderSize, + fixEncoderSize, + getArrayDecoder, + getArrayEncoder, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + transformEncoder, + type Account, + type Address, + type EncodedAccount, + type FetchAccountConfig, + type FetchAccountsConfig, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type MaybeAccount, + type MaybeEncodedAccount, + type ReadonlyUint8Array, +} from '@solana/kit' +import { + getLeaderboardEntryDecoder, + getLeaderboardEntryEncoder, + type LeaderboardEntry, + type LeaderboardEntryArgs, +} from '../types' + +export const LEADERBOARD_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([247, 186, 238, 243, 194, 30, 9, 36]) + +export function getLeaderboardDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(LEADERBOARD_DISCRIMINATOR) +} + +export type Leaderboard = { discriminator: ReadonlyUint8Array; entries: Array } + +export type LeaderboardArgs = { entries: Array } + +/** Gets the encoder for {@link LeaderboardArgs} account data. */ +export function getLeaderboardEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', fixEncoderSize(getBytesEncoder(), 8)], + ['entries', getArrayEncoder(getLeaderboardEntryEncoder(), { size: 5 })], + ]), + (value) => ({ ...value, discriminator: LEADERBOARD_DISCRIMINATOR }), + ) +} + +/** Gets the decoder for {@link Leaderboard} account data. */ +export function getLeaderboardDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', fixDecoderSize(getBytesDecoder(), 8)], + ['entries', getArrayDecoder(getLeaderboardEntryDecoder(), { size: 5 })], + ]) +} + +/** Gets the codec for {@link Leaderboard} account data. */ +export function getLeaderboardCodec(): FixedSizeCodec { + return combineCodec(getLeaderboardEncoder(), getLeaderboardDecoder()) +} + +export function decodeLeaderboard( + encodedAccount: EncodedAccount, +): Account +export function decodeLeaderboard( + encodedAccount: MaybeEncodedAccount, +): MaybeAccount +export function decodeLeaderboard( + encodedAccount: EncodedAccount | MaybeEncodedAccount, +): Account | MaybeAccount { + return decodeAccount(encodedAccount as MaybeEncodedAccount, getLeaderboardDecoder()) +} + +export async function fetchLeaderboard( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig, +): Promise> { + const maybeAccount = await fetchMaybeLeaderboard(rpc, address, config) + assertAccountExists(maybeAccount) + return maybeAccount +} + +export async function fetchMaybeLeaderboard( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig, +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config) + return decodeLeaderboard(maybeAccount) +} + +export async function fetchAllLeaderboard( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig, +): Promise[]> { + const maybeAccounts = await fetchAllMaybeLeaderboard(rpc, addresses, config) + assertAccountsExist(maybeAccounts) + return maybeAccounts +} + +export async function fetchAllMaybeLeaderboard( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig, +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config) + return maybeAccounts.map((maybeAccount) => decodeLeaderboard(maybeAccount)) +} + +export function getLeaderboardSize(): number { + return 208 +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/farmingIdle.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/farmingIdle.ts new file mode 100644 index 0000000..f821167 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/farmingIdle.ts @@ -0,0 +1,13 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import type { ReadonlyUint8Array } from '@solana/kit' + +export const FARM_SEED: ReadonlyUint8Array = new Uint8Array([102, 97, 114, 109]) + +export const LEADERBOARD_SEED: ReadonlyUint8Array = new Uint8Array([108, 101, 97, 100, 101, 114, 98, 111, 97, 114, 100]) diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/index.ts new file mode 100644 index 0000000..eff3ae6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/constants/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './farmingIdle' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/farmingIdle.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/farmingIdle.ts new file mode 100644 index 0000000..4fa8d4c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/farmingIdle.ts @@ -0,0 +1,57 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + isProgramError, + type Address, + type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, + type SolanaError, +} from '@solana/kit' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +/** InsufficientHarvestPoints: Not enough harvest points to fund the upgrade */ +export const FARMING_IDLE_ERROR__INSUFFICIENT_HARVEST_POINTS = 0x1770 // 6000 +/** InvalidAmount: Amount must be greater than zero */ +export const FARMING_IDLE_ERROR__INVALID_AMOUNT = 0x1771 // 6001 +/** InvalidCrop: Not a valid crop index */ +export const FARMING_IDLE_ERROR__INVALID_CROP = 0x1772 // 6002 +/** Overflow: Overflow */ +export const FARMING_IDLE_ERROR__OVERFLOW = 0x1773 // 6003 + +export type FarmingIdleError = + | typeof FARMING_IDLE_ERROR__INSUFFICIENT_HARVEST_POINTS + | typeof FARMING_IDLE_ERROR__INVALID_AMOUNT + | typeof FARMING_IDLE_ERROR__INVALID_CROP + | typeof FARMING_IDLE_ERROR__OVERFLOW + +let farmingIdleErrorMessages: Record | undefined +if (process.env['NODE_ENV'] !== 'production') { + farmingIdleErrorMessages = { + [FARMING_IDLE_ERROR__INSUFFICIENT_HARVEST_POINTS]: `Not enough harvest points to fund the upgrade`, + [FARMING_IDLE_ERROR__INVALID_AMOUNT]: `Amount must be greater than zero`, + [FARMING_IDLE_ERROR__INVALID_CROP]: `Not a valid crop index`, + [FARMING_IDLE_ERROR__OVERFLOW]: `Overflow`, + } +} + +export function getFarmingIdleErrorMessage(code: FarmingIdleError): string { + if (process.env['NODE_ENV'] !== 'production') { + return (farmingIdleErrorMessages as Record)[code] + } + + return 'Error message not available in production bundles.' +} + +export function isFarmingIdleError( + error: unknown, + transactionMessage: { instructions: Record }, + code?: TProgramErrorCode, +): error is SolanaError & + Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> { + return isProgramError(error, transactionMessage, FARMING_IDLE_PROGRAM_ADDRESS, code) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/index.ts new file mode 100644 index 0000000..eff3ae6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/errors/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './farmingIdle' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/index.ts new file mode 100644 index 0000000..718ae93 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/index.ts @@ -0,0 +1,15 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './accounts' +export * from './constants' +export * from './errors' +export * from './instructions' +export * from './pdas' +export * from './programs' +export * from './types' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/harvest.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/harvest.ts new file mode 100644 index 0000000..c7bf72c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/harvest.ts @@ -0,0 +1,142 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, + SolanaError, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit' +import { getAccountMetaFactory, type ResolvedInstructionAccount } from '@solana/kit/program-client-core' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +export const HARVEST_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([228, 241, 31, 182, 53, 169, 59, 199]) + +export function getHarvestDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(HARVEST_DISCRIMINATOR) +} + +export type HarvestInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountFarm extends string | AccountMeta = string, + TAccountPlayer extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountFarm extends string ? WritableAccount : TAccountFarm, + TAccountPlayer extends string + ? ReadonlySignerAccount & AccountSignerMeta + : TAccountPlayer, + ...TRemainingAccounts, + ] + > + +export type HarvestInstructionData = { discriminator: ReadonlyUint8Array } + +export type HarvestInstructionDataArgs = {} + +export function getHarvestInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder(getStructEncoder([['discriminator', fixEncoderSize(getBytesEncoder(), 8)]]), (value) => ({ + ...value, + discriminator: HARVEST_DISCRIMINATOR, + })) +} + +export function getHarvestInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', fixDecoderSize(getBytesDecoder(), 8)]]) +} + +export function getHarvestInstructionDataCodec(): FixedSizeCodec { + return combineCodec(getHarvestInstructionDataEncoder(), getHarvestInstructionDataDecoder()) +} + +export type HarvestInput = { + farm: Address + player: TransactionSigner +} + +export function getHarvestInstruction< + TAccountFarm extends string, + TAccountPlayer extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: HarvestInput, + config?: { programAddress?: TProgramAddress }, +): HarvestInstruction { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + player: { value: input.player ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [getAccountMeta('farm', accounts.farm), getAccountMeta('player', accounts.player)], + data: getHarvestInstructionDataEncoder().encode({}), + programAddress, + } as HarvestInstruction) +} + +export type ParsedHarvestInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address + accounts: { + farm: TAccountMetas[0] + player: TAccountMetas[1] + } + data: HarvestInstructionData +} + +export function parseHarvestInstruction( + instruction: Instruction & InstructionWithAccounts & InstructionWithData, +): ParsedHarvestInstruction { + if (instruction.accounts.length < 2) { + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, { + actualAccountMetas: instruction.accounts.length, + expectedAccountMetas: 2, + }) + } + let accountIndex = 0 + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]! + accountIndex += 1 + return accountMeta + } + return { + programAddress: instruction.programAddress, + accounts: { farm: getNextAccount(), player: getNextAccount() }, + data: getHarvestInstructionDataDecoder().decode(instruction.data), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/index.ts new file mode 100644 index 0000000..d4aed5d --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/index.ts @@ -0,0 +1,13 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './harvest' +export * from './initializeFarm' +export * from './initializeLeaderboard' +export * from './submitFarm' +export * from './upgradeFarm' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeFarm.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeFarm.ts new file mode 100644 index 0000000..7c336be --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeFarm.ts @@ -0,0 +1,248 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, + SolanaError, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/kit' +import { + getAccountMetaFactory, + getAddressFromResolvedInstructionAccount, + type ResolvedInstructionAccount, +} from '@solana/kit/program-client-core' +import { findFarmPda } from '../pdas' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +export const INITIALIZE_FARM_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([252, 28, 185, 172, 244, 74, 117, 165]) + +export function getInitializeFarmDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(INITIALIZE_FARM_DISCRIMINATOR) +} + +export type InitializeFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountFarm extends string | AccountMeta = string, + TAccountOwner extends string | AccountMeta = string, + TAccountPlayer extends string | AccountMeta = string, + TAccountSystemProgram extends string | AccountMeta = '11111111111111111111111111111111', + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountFarm extends string ? WritableAccount : TAccountFarm, + TAccountOwner extends string + ? WritableSignerAccount & AccountSignerMeta + : TAccountOwner, + TAccountPlayer extends string + ? WritableSignerAccount & AccountSignerMeta + : TAccountPlayer, + TAccountSystemProgram extends string ? ReadonlyAccount : TAccountSystemProgram, + ...TRemainingAccounts, + ] + > + +export type InitializeFarmInstructionData = { discriminator: ReadonlyUint8Array } + +export type InitializeFarmInstructionDataArgs = {} + +export function getInitializeFarmInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder(getStructEncoder([['discriminator', fixEncoderSize(getBytesEncoder(), 8)]]), (value) => ({ + ...value, + discriminator: INITIALIZE_FARM_DISCRIMINATOR, + })) +} + +export function getInitializeFarmInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', fixDecoderSize(getBytesDecoder(), 8)]]) +} + +export function getInitializeFarmInstructionDataCodec(): FixedSizeCodec< + InitializeFarmInstructionDataArgs, + InitializeFarmInstructionData +> { + return combineCodec(getInitializeFarmInstructionDataEncoder(), getInitializeFarmInstructionDataDecoder()) +} + +export type InitializeFarmAsyncInput< + TAccountFarm extends string = string, + TAccountOwner extends string = string, + TAccountPlayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + farm?: Address + owner: TransactionSigner + player: TransactionSigner + systemProgram?: Address +} + +export async function getInitializeFarmInstructionAsync< + TAccountFarm extends string, + TAccountOwner extends string, + TAccountPlayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: InitializeFarmAsyncInput, + config?: { programAddress?: TProgramAddress }, +): Promise< + InitializeFarmInstruction +> { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + owner: { value: input.owner ?? null, isWritable: true }, + player: { value: input.player ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Resolve default values. + if (!accounts.farm.value) { + accounts.farm.value = await findFarmPda( + { + player: getAddressFromResolvedInstructionAccount('player', accounts.player.value), + owner: getAddressFromResolvedInstructionAccount('owner', accounts.owner.value), + }, + { programAddress }, + ) + } + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'> + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('farm', accounts.farm), + getAccountMeta('owner', accounts.owner), + getAccountMeta('player', accounts.player), + getAccountMeta('systemProgram', accounts.systemProgram), + ], + data: getInitializeFarmInstructionDataEncoder().encode({}), + programAddress, + } as InitializeFarmInstruction) +} + +export type InitializeFarmInput< + TAccountFarm extends string = string, + TAccountOwner extends string = string, + TAccountPlayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + farm: Address + owner: TransactionSigner + player: TransactionSigner + systemProgram?: Address +} + +export function getInitializeFarmInstruction< + TAccountFarm extends string, + TAccountOwner extends string, + TAccountPlayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: InitializeFarmInput, + config?: { programAddress?: TProgramAddress }, +): InitializeFarmInstruction { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + owner: { value: input.owner ?? null, isWritable: true }, + player: { value: input.player ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Resolve default values. + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'> + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('farm', accounts.farm), + getAccountMeta('owner', accounts.owner), + getAccountMeta('player', accounts.player), + getAccountMeta('systemProgram', accounts.systemProgram), + ], + data: getInitializeFarmInstructionDataEncoder().encode({}), + programAddress, + } as InitializeFarmInstruction) +} + +export type ParsedInitializeFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address + accounts: { + farm: TAccountMetas[0] + owner: TAccountMetas[1] + player: TAccountMetas[2] + systemProgram: TAccountMetas[3] + } + data: InitializeFarmInstructionData +} + +export function parseInitializeFarmInstruction( + instruction: Instruction & InstructionWithAccounts & InstructionWithData, +): ParsedInitializeFarmInstruction { + if (instruction.accounts.length < 4) { + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, { + actualAccountMetas: instruction.accounts.length, + expectedAccountMetas: 4, + }) + } + let accountIndex = 0 + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]! + accountIndex += 1 + return accountMeta + } + return { + programAddress: instruction.programAddress, + accounts: { + farm: getNextAccount(), + owner: getNextAccount(), + player: getNextAccount(), + systemProgram: getNextAccount(), + }, + data: getInitializeFarmInstructionDataDecoder().decode(instruction.data), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeLeaderboard.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeLeaderboard.ts new file mode 100644 index 0000000..2145ef6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/initializeLeaderboard.ts @@ -0,0 +1,226 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, + SolanaError, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/kit' +import { getAccountMetaFactory, type ResolvedInstructionAccount } from '@solana/kit/program-client-core' +import { findLeaderboardPda } from '../pdas' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +export const INITIALIZE_LEADERBOARD_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([ + 47, 23, 34, 39, 46, 108, 91, 176, +]) + +export function getInitializeLeaderboardDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(INITIALIZE_LEADERBOARD_DISCRIMINATOR) +} + +export type InitializeLeaderboardInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountLeaderboard extends string | AccountMeta = string, + TAccountPayer extends string | AccountMeta = string, + TAccountSystemProgram extends string | AccountMeta = '11111111111111111111111111111111', + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountLeaderboard extends string ? WritableAccount : TAccountLeaderboard, + TAccountPayer extends string + ? WritableSignerAccount & AccountSignerMeta + : TAccountPayer, + TAccountSystemProgram extends string ? ReadonlyAccount : TAccountSystemProgram, + ...TRemainingAccounts, + ] + > + +export type InitializeLeaderboardInstructionData = { discriminator: ReadonlyUint8Array } + +export type InitializeLeaderboardInstructionDataArgs = {} + +export function getInitializeLeaderboardInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder(getStructEncoder([['discriminator', fixEncoderSize(getBytesEncoder(), 8)]]), (value) => ({ + ...value, + discriminator: INITIALIZE_LEADERBOARD_DISCRIMINATOR, + })) +} + +export function getInitializeLeaderboardInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', fixDecoderSize(getBytesDecoder(), 8)]]) +} + +export function getInitializeLeaderboardInstructionDataCodec(): FixedSizeCodec< + InitializeLeaderboardInstructionDataArgs, + InitializeLeaderboardInstructionData +> { + return combineCodec( + getInitializeLeaderboardInstructionDataEncoder(), + getInitializeLeaderboardInstructionDataDecoder(), + ) +} + +export type InitializeLeaderboardAsyncInput< + TAccountLeaderboard extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + leaderboard?: Address + payer: TransactionSigner + systemProgram?: Address +} + +export async function getInitializeLeaderboardInstructionAsync< + TAccountLeaderboard extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: InitializeLeaderboardAsyncInput, + config?: { programAddress?: TProgramAddress }, +): Promise< + InitializeLeaderboardInstruction +> { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + leaderboard: { value: input.leaderboard ?? null, isWritable: true }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Resolve default values. + if (!accounts.leaderboard.value) { + accounts.leaderboard.value = await findLeaderboardPda({ programAddress }) + } + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'> + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('leaderboard', accounts.leaderboard), + getAccountMeta('payer', accounts.payer), + getAccountMeta('systemProgram', accounts.systemProgram), + ], + data: getInitializeLeaderboardInstructionDataEncoder().encode({}), + programAddress, + } as InitializeLeaderboardInstruction) +} + +export type InitializeLeaderboardInput< + TAccountLeaderboard extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + leaderboard: Address + payer: TransactionSigner + systemProgram?: Address +} + +export function getInitializeLeaderboardInstruction< + TAccountLeaderboard extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: InitializeLeaderboardInput, + config?: { programAddress?: TProgramAddress }, +): InitializeLeaderboardInstruction { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + leaderboard: { value: input.leaderboard ?? null, isWritable: true }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Resolve default values. + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'> + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('leaderboard', accounts.leaderboard), + getAccountMeta('payer', accounts.payer), + getAccountMeta('systemProgram', accounts.systemProgram), + ], + data: getInitializeLeaderboardInstructionDataEncoder().encode({}), + programAddress, + } as InitializeLeaderboardInstruction) +} + +export type ParsedInitializeLeaderboardInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address + accounts: { + leaderboard: TAccountMetas[0] + payer: TAccountMetas[1] + systemProgram: TAccountMetas[2] + } + data: InitializeLeaderboardInstructionData +} + +export function parseInitializeLeaderboardInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & InstructionWithAccounts & InstructionWithData, +): ParsedInitializeLeaderboardInstruction { + if (instruction.accounts.length < 3) { + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, { + actualAccountMetas: instruction.accounts.length, + expectedAccountMetas: 3, + }) + } + let accountIndex = 0 + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]! + accountIndex += 1 + return accountMeta + } + return { + programAddress: instruction.programAddress, + accounts: { leaderboard: getNextAccount(), payer: getNextAccount(), systemProgram: getNextAccount() }, + data: getInitializeLeaderboardInstructionDataDecoder().decode(instruction.data), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/submitFarm.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/submitFarm.ts new file mode 100644 index 0000000..e4cc9b4 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/submitFarm.ts @@ -0,0 +1,227 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, + SolanaError, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit' +import { getAccountMetaFactory, type ResolvedInstructionAccount } from '@solana/kit/program-client-core' +import { findLeaderboardPda } from '../pdas' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +export const SUBMIT_FARM_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([8, 195, 198, 253, 255, 190, 96, 102]) + +export function getSubmitFarmDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(SUBMIT_FARM_DISCRIMINATOR) +} + +export type SubmitFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountFarm extends string | AccountMeta = string, + TAccountLeaderboard extends string | AccountMeta = string, + TAccountOwner extends string | AccountMeta = string, + TAccountPlayer extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountFarm extends string ? WritableAccount : TAccountFarm, + TAccountLeaderboard extends string ? WritableAccount : TAccountLeaderboard, + TAccountOwner extends string + ? ReadonlySignerAccount & AccountSignerMeta + : TAccountOwner, + TAccountPlayer extends string + ? ReadonlySignerAccount & AccountSignerMeta + : TAccountPlayer, + ...TRemainingAccounts, + ] + > + +export type SubmitFarmInstructionData = { discriminator: ReadonlyUint8Array } + +export type SubmitFarmInstructionDataArgs = {} + +export function getSubmitFarmInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder(getStructEncoder([['discriminator', fixEncoderSize(getBytesEncoder(), 8)]]), (value) => ({ + ...value, + discriminator: SUBMIT_FARM_DISCRIMINATOR, + })) +} + +export function getSubmitFarmInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', fixDecoderSize(getBytesDecoder(), 8)]]) +} + +export function getSubmitFarmInstructionDataCodec(): FixedSizeCodec< + SubmitFarmInstructionDataArgs, + SubmitFarmInstructionData +> { + return combineCodec(getSubmitFarmInstructionDataEncoder(), getSubmitFarmInstructionDataDecoder()) +} + +export type SubmitFarmAsyncInput< + TAccountFarm extends string = string, + TAccountLeaderboard extends string = string, + TAccountOwner extends string = string, + TAccountPlayer extends string = string, +> = { + farm: Address + leaderboard?: Address + owner: TransactionSigner + player: TransactionSigner +} + +export async function getSubmitFarmInstructionAsync< + TAccountFarm extends string, + TAccountLeaderboard extends string, + TAccountOwner extends string, + TAccountPlayer extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: SubmitFarmAsyncInput, + config?: { programAddress?: TProgramAddress }, +): Promise> { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + leaderboard: { value: input.leaderboard ?? null, isWritable: true }, + owner: { value: input.owner ?? null, isWritable: false }, + player: { value: input.player ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Resolve default values. + if (!accounts.leaderboard.value) { + accounts.leaderboard.value = await findLeaderboardPda({ programAddress }) + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('farm', accounts.farm), + getAccountMeta('leaderboard', accounts.leaderboard), + getAccountMeta('owner', accounts.owner), + getAccountMeta('player', accounts.player), + ], + data: getSubmitFarmInstructionDataEncoder().encode({}), + programAddress, + } as SubmitFarmInstruction) +} + +export type SubmitFarmInput< + TAccountFarm extends string = string, + TAccountLeaderboard extends string = string, + TAccountOwner extends string = string, + TAccountPlayer extends string = string, +> = { + farm: Address + leaderboard: Address + owner: TransactionSigner + player: TransactionSigner +} + +export function getSubmitFarmInstruction< + TAccountFarm extends string, + TAccountLeaderboard extends string, + TAccountOwner extends string, + TAccountPlayer extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: SubmitFarmInput, + config?: { programAddress?: TProgramAddress }, +): SubmitFarmInstruction { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + leaderboard: { value: input.leaderboard ?? null, isWritable: true }, + owner: { value: input.owner ?? null, isWritable: false }, + player: { value: input.player ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [ + getAccountMeta('farm', accounts.farm), + getAccountMeta('leaderboard', accounts.leaderboard), + getAccountMeta('owner', accounts.owner), + getAccountMeta('player', accounts.player), + ], + data: getSubmitFarmInstructionDataEncoder().encode({}), + programAddress, + } as SubmitFarmInstruction) +} + +export type ParsedSubmitFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address + accounts: { + farm: TAccountMetas[0] + leaderboard: TAccountMetas[1] + owner: TAccountMetas[2] + player: TAccountMetas[3] + } + data: SubmitFarmInstructionData +} + +export function parseSubmitFarmInstruction( + instruction: Instruction & InstructionWithAccounts & InstructionWithData, +): ParsedSubmitFarmInstruction { + if (instruction.accounts.length < 4) { + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, { + actualAccountMetas: instruction.accounts.length, + expectedAccountMetas: 4, + }) + } + let accountIndex = 0 + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]! + accountIndex += 1 + return accountMeta + } + return { + programAddress: instruction.programAddress, + accounts: { + farm: getNextAccount(), + leaderboard: getNextAccount(), + owner: getNextAccount(), + player: getNextAccount(), + }, + data: getSubmitFarmInstructionDataDecoder().decode(instruction.data), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/upgradeFarm.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/upgradeFarm.ts new file mode 100644 index 0000000..06614a3 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/instructions/upgradeFarm.ts @@ -0,0 +1,160 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, + SolanaError, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit' +import { getAccountMetaFactory, type ResolvedInstructionAccount } from '@solana/kit/program-client-core' +import { FARMING_IDLE_PROGRAM_ADDRESS } from '../programs' + +export const UPGRADE_FARM_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([110, 239, 193, 1, 165, 101, 54, 200]) + +export function getUpgradeFarmDiscriminatorBytes(): ReadonlyUint8Array { + return fixEncoderSize(getBytesEncoder(), 8).encode(UPGRADE_FARM_DISCRIMINATOR) +} + +export type UpgradeFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountFarm extends string | AccountMeta = string, + TAccountPlayer extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountFarm extends string ? WritableAccount : TAccountFarm, + TAccountPlayer extends string + ? ReadonlySignerAccount & AccountSignerMeta + : TAccountPlayer, + ...TRemainingAccounts, + ] + > + +export type UpgradeFarmInstructionData = { discriminator: ReadonlyUint8Array; cropIndex: number; amount: number } + +export type UpgradeFarmInstructionDataArgs = { cropIndex: number; amount: number } + +export function getUpgradeFarmInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', fixEncoderSize(getBytesEncoder(), 8)], + ['cropIndex', getU8Encoder()], + ['amount', getU8Encoder()], + ]), + (value) => ({ ...value, discriminator: UPGRADE_FARM_DISCRIMINATOR }), + ) +} + +export function getUpgradeFarmInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', fixDecoderSize(getBytesDecoder(), 8)], + ['cropIndex', getU8Decoder()], + ['amount', getU8Decoder()], + ]) +} + +export function getUpgradeFarmInstructionDataCodec(): FixedSizeCodec< + UpgradeFarmInstructionDataArgs, + UpgradeFarmInstructionData +> { + return combineCodec(getUpgradeFarmInstructionDataEncoder(), getUpgradeFarmInstructionDataDecoder()) +} + +export type UpgradeFarmInput = { + farm: Address + player: TransactionSigner + cropIndex: UpgradeFarmInstructionDataArgs['cropIndex'] + amount: UpgradeFarmInstructionDataArgs['amount'] +} + +export function getUpgradeFarmInstruction< + TAccountFarm extends string, + TAccountPlayer extends string, + TProgramAddress extends Address = typeof FARMING_IDLE_PROGRAM_ADDRESS, +>( + input: UpgradeFarmInput, + config?: { programAddress?: TProgramAddress }, +): UpgradeFarmInstruction { + // Program address. + const programAddress = config?.programAddress ?? FARMING_IDLE_PROGRAM_ADDRESS + + // Original accounts. + const originalAccounts = { + farm: { value: input.farm ?? null, isWritable: true }, + player: { value: input.player ?? null, isWritable: false }, + } + const accounts = originalAccounts as Record + + // Original args. + const args = { ...input } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId') + return Object.freeze({ + accounts: [getAccountMeta('farm', accounts.farm), getAccountMeta('player', accounts.player)], + data: getUpgradeFarmInstructionDataEncoder().encode(args as UpgradeFarmInstructionDataArgs), + programAddress, + } as UpgradeFarmInstruction) +} + +export type ParsedUpgradeFarmInstruction< + TProgram extends string = typeof FARMING_IDLE_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address + accounts: { + farm: TAccountMetas[0] + player: TAccountMetas[1] + } + data: UpgradeFarmInstructionData +} + +export function parseUpgradeFarmInstruction( + instruction: Instruction & InstructionWithAccounts & InstructionWithData, +): ParsedUpgradeFarmInstruction { + if (instruction.accounts.length < 2) { + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, { + actualAccountMetas: instruction.accounts.length, + expectedAccountMetas: 2, + }) + } + let accountIndex = 0 + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]! + accountIndex += 1 + return accountMeta + } + return { + programAddress: instruction.programAddress, + accounts: { farm: getNextAccount(), player: getNextAccount() }, + data: getUpgradeFarmInstructionDataDecoder().decode(instruction.data), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/farm.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/farm.ts new file mode 100644 index 0000000..4e0c036 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/farm.ts @@ -0,0 +1,37 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + getAddressEncoder, + getBytesEncoder, + getProgramDerivedAddress, + type Address, + type ProgramDerivedAddress, +} from '@solana/kit' + +export type FarmSeeds = { + player: Address + owner: Address +} + +export async function findFarmPda( + seeds: FarmSeeds, + config: { programAddress?: Address | undefined } = {}, +): Promise { + const { + programAddress = 'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7' as Address<'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7'>, + } = config + return await getProgramDerivedAddress({ + programAddress, + seeds: [ + getBytesEncoder().encode(new Uint8Array([102, 97, 114, 109])), + getAddressEncoder().encode(seeds.player), + getAddressEncoder().encode(seeds.owner), + ], + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/index.ts new file mode 100644 index 0000000..c23e757 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/index.ts @@ -0,0 +1,10 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './farm' +export * from './leaderboard' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/leaderboard.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/leaderboard.ts new file mode 100644 index 0000000..1d7037f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/pdas/leaderboard.ts @@ -0,0 +1,21 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { getBytesEncoder, getProgramDerivedAddress, type Address, type ProgramDerivedAddress } from '@solana/kit' + +export async function findLeaderboardPda( + config: { programAddress?: Address | undefined } = {}, +): Promise { + const { + programAddress = 'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7' as Address<'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7'>, + } = config + return await getProgramDerivedAddress({ + programAddress, + seeds: [getBytesEncoder().encode(new Uint8Array([108, 101, 97, 100, 101, 114, 98, 111, 97, 114, 100]))], + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/farmingIdle.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/farmingIdle.ts new file mode 100644 index 0000000..d63c218 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/farmingIdle.ts @@ -0,0 +1,278 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + assertIsInstructionWithAccounts, + containsBytes, + extendClient, + fixEncoderSize, + getBytesEncoder, + SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_ACCOUNT, + SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_INSTRUCTION, + SOLANA_ERROR__PROGRAM_CLIENTS__UNRECOGNIZED_INSTRUCTION_TYPE, + SolanaError, + type Address, + type ClientWithPayer, + type ClientWithRpc, + type ClientWithTransactionPlanning, + type ClientWithTransactionSending, + type ExtendedClient, + type GetAccountInfoApi, + type GetMultipleAccountsApi, + type Instruction, + type InstructionWithData, + type ReadonlyUint8Array, +} from '@solana/kit' +import { + addSelfFetchFunctions, + addSelfPlanAndSendFunctions, + type SelfFetchFunctions, + type SelfPlanAndSendFunctions, +} from '@solana/kit/program-client-core' +import { + getFarmCodec, + getLeaderboardCodec, + type Farm, + type FarmArgs, + type Leaderboard, + type LeaderboardArgs, +} from '../accounts' +import { + getHarvestInstruction, + getInitializeFarmInstructionAsync, + getInitializeLeaderboardInstructionAsync, + getSubmitFarmInstructionAsync, + getUpgradeFarmInstruction, + parseHarvestInstruction, + parseInitializeFarmInstruction, + parseInitializeLeaderboardInstruction, + parseSubmitFarmInstruction, + parseUpgradeFarmInstruction, + type HarvestInput, + type InitializeFarmAsyncInput, + type InitializeLeaderboardAsyncInput, + type ParsedHarvestInstruction, + type ParsedInitializeFarmInstruction, + type ParsedInitializeLeaderboardInstruction, + type ParsedSubmitFarmInstruction, + type ParsedUpgradeFarmInstruction, + type SubmitFarmAsyncInput, + type UpgradeFarmInput, +} from '../instructions' +import { findFarmPda, findLeaderboardPda } from '../pdas' + +export const FARMING_IDLE_PROGRAM_ADDRESS = + 'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7' as Address<'Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7'> + +export enum FarmingIdleAccount { + Farm, + Leaderboard, +} + +export function identifyFarmingIdleAccount( + account: { data: ReadonlyUint8Array } | ReadonlyUint8Array, +): FarmingIdleAccount { + const data = 'data' in account ? account.data : account + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([161, 156, 211, 253, 250, 64, 53, 250])), + 0, + ) + ) { + return FarmingIdleAccount.Farm + } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([247, 186, 238, 243, 194, 30, 9, 36])), + 0, + ) + ) { + return FarmingIdleAccount.Leaderboard + } + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_ACCOUNT, { + accountData: data, + programName: 'farmingIdle', + }) +} + +export enum FarmingIdleInstruction { + Harvest, + InitializeFarm, + InitializeLeaderboard, + SubmitFarm, + UpgradeFarm, +} + +export function identifyFarmingIdleInstruction( + instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array, +): FarmingIdleInstruction { + const data = 'data' in instruction ? instruction.data : instruction + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([228, 241, 31, 182, 53, 169, 59, 199])), + 0, + ) + ) { + return FarmingIdleInstruction.Harvest + } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([252, 28, 185, 172, 244, 74, 117, 165])), + 0, + ) + ) { + return FarmingIdleInstruction.InitializeFarm + } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([47, 23, 34, 39, 46, 108, 91, 176])), + 0, + ) + ) { + return FarmingIdleInstruction.InitializeLeaderboard + } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([8, 195, 198, 253, 255, 190, 96, 102])), + 0, + ) + ) { + return FarmingIdleInstruction.SubmitFarm + } + if ( + containsBytes( + data, + fixEncoderSize(getBytesEncoder(), 8).encode(new Uint8Array([110, 239, 193, 1, 165, 101, 54, 200])), + 0, + ) + ) { + return FarmingIdleInstruction.UpgradeFarm + } + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_INSTRUCTION, { + instructionData: data, + programName: 'farmingIdle', + }) +} + +export type ParsedFarmingIdleInstruction = + | ({ instructionType: FarmingIdleInstruction.Harvest } & ParsedHarvestInstruction) + | ({ instructionType: FarmingIdleInstruction.InitializeFarm } & ParsedInitializeFarmInstruction) + | ({ + instructionType: FarmingIdleInstruction.InitializeLeaderboard + } & ParsedInitializeLeaderboardInstruction) + | ({ instructionType: FarmingIdleInstruction.SubmitFarm } & ParsedSubmitFarmInstruction) + | ({ instructionType: FarmingIdleInstruction.UpgradeFarm } & ParsedUpgradeFarmInstruction) + +export function parseFarmingIdleInstruction( + instruction: Instruction & InstructionWithData, +): ParsedFarmingIdleInstruction { + const instructionType = identifyFarmingIdleInstruction(instruction) + switch (instructionType) { + case FarmingIdleInstruction.Harvest: { + assertIsInstructionWithAccounts(instruction) + return { instructionType: FarmingIdleInstruction.Harvest, ...parseHarvestInstruction(instruction) } + } + case FarmingIdleInstruction.InitializeFarm: { + assertIsInstructionWithAccounts(instruction) + return { instructionType: FarmingIdleInstruction.InitializeFarm, ...parseInitializeFarmInstruction(instruction) } + } + case FarmingIdleInstruction.InitializeLeaderboard: { + assertIsInstructionWithAccounts(instruction) + return { + instructionType: FarmingIdleInstruction.InitializeLeaderboard, + ...parseInitializeLeaderboardInstruction(instruction), + } + } + case FarmingIdleInstruction.SubmitFarm: { + assertIsInstructionWithAccounts(instruction) + return { instructionType: FarmingIdleInstruction.SubmitFarm, ...parseSubmitFarmInstruction(instruction) } + } + case FarmingIdleInstruction.UpgradeFarm: { + assertIsInstructionWithAccounts(instruction) + return { instructionType: FarmingIdleInstruction.UpgradeFarm, ...parseUpgradeFarmInstruction(instruction) } + } + default: + throw new SolanaError(SOLANA_ERROR__PROGRAM_CLIENTS__UNRECOGNIZED_INSTRUCTION_TYPE, { + instructionType: instructionType as string, + programName: 'farmingIdle', + }) + } +} + +export type FarmingIdlePlugin = { + accounts: FarmingIdlePluginAccounts + instructions: FarmingIdlePluginInstructions + pdas: FarmingIdlePluginPdas + identifyAccount: typeof identifyFarmingIdleAccount + identifyInstruction: typeof identifyFarmingIdleInstruction + parseInstruction: typeof parseFarmingIdleInstruction +} + +export type FarmingIdlePluginAccounts = { + farm: ReturnType & SelfFetchFunctions + leaderboard: ReturnType & SelfFetchFunctions +} + +export type FarmingIdlePluginInstructions = { + harvest: (input: HarvestInput) => ReturnType & SelfPlanAndSendFunctions + initializeFarm: ( + input: InitializeFarmAsyncInput, + ) => ReturnType & SelfPlanAndSendFunctions + initializeLeaderboard: ( + input: MakeOptional, + ) => ReturnType & SelfPlanAndSendFunctions + submitFarm: ( + input: SubmitFarmAsyncInput, + ) => ReturnType & SelfPlanAndSendFunctions + upgradeFarm: (input: UpgradeFarmInput) => ReturnType & SelfPlanAndSendFunctions +} + +export type FarmingIdlePluginPdas = { farm: typeof findFarmPda; leaderboard: typeof findLeaderboardPda } + +export type FarmingIdlePluginRequirements = ClientWithRpc & + ClientWithPayer & + ClientWithTransactionPlanning & + ClientWithTransactionSending + +export function farmingIdleProgram() { + return ( + client: T, + ): ExtendedClient => { + return extendClient(client, { + farmingIdle: { + accounts: { + farm: addSelfFetchFunctions(client, getFarmCodec()), + leaderboard: addSelfFetchFunctions(client, getLeaderboardCodec()), + }, + instructions: { + harvest: (input) => addSelfPlanAndSendFunctions(client, getHarvestInstruction(input)), + initializeFarm: (input) => addSelfPlanAndSendFunctions(client, getInitializeFarmInstructionAsync(input)), + initializeLeaderboard: (input) => + addSelfPlanAndSendFunctions( + client, + getInitializeLeaderboardInstructionAsync({ ...input, payer: input.payer ?? client.payer }), + ), + submitFarm: (input) => addSelfPlanAndSendFunctions(client, getSubmitFarmInstructionAsync(input)), + upgradeFarm: (input) => addSelfPlanAndSendFunctions(client, getUpgradeFarmInstruction(input)), + }, + pdas: { farm: findFarmPda, leaderboard: findLeaderboardPda }, + identifyAccount: identifyFarmingIdleAccount, + identifyInstruction: identifyFarmingIdleInstruction, + parseInstruction: parseFarmingIdleInstruction, + }, + }) + } +} + +type MakeOptional = Omit & Partial> diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/index.ts new file mode 100644 index 0000000..eff3ae6 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/programs/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './farmingIdle' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/index.ts new file mode 100644 index 0000000..47e5e01 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './leaderboardEntry' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/leaderboardEntry.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/leaderboardEntry.ts new file mode 100644 index 0000000..cca9e5f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/generated/types/leaderboardEntry.ts @@ -0,0 +1,51 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getAddressDecoder, + getAddressEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, +} from '@solana/kit' + +export type LeaderboardEntry = { + /** The owner wallet behind the farm, not the burner player wallet. */ + owner: Address + points: bigint +} + +export type LeaderboardEntryArgs = { + /** The owner wallet behind the farm, not the burner player wallet. */ + owner: Address + points: number | bigint +} + +export function getLeaderboardEntryEncoder(): FixedSizeEncoder { + return getStructEncoder([ + ['owner', getAddressEncoder()], + ['points', getU64Encoder()], + ]) +} + +export function getLeaderboardEntryDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['owner', getAddressDecoder()], + ['points', getU64Decoder()], + ]) +} + +export function getLeaderboardEntryCodec(): FixedSizeCodec { + return combineCodec(getLeaderboardEntryEncoder(), getLeaderboardEntryDecoder()) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/index.ts new file mode 100644 index 0000000..822afdf --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/client/js/index.ts @@ -0,0 +1,5 @@ +/** + * Build your wrappers around the generated client here. + */ +export * from './farming-idle' +export * from './generated' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/src/index.ts b/mobile/sample-expo-kit-farming-idle/anchor/src/index.ts new file mode 100644 index 0000000..a7404a0 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/src/index.ts @@ -0,0 +1,7 @@ +// Here we export some useful types and functions for interacting with the Anchor program. +import FarmingIdleIDL from '../target/idl/farming_idle.json' + +// Re-export the generated IDL and type +export { FarmingIdleIDL } + +export * from './client/js' diff --git a/mobile/sample-expo-kit-farming-idle/anchor/target/idl/farming_idle.json b/mobile/sample-expo-kit-farming-idle/anchor/target/idl/farming_idle.json new file mode 100644 index 0000000..4efd031 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/target/idl/farming_idle.json @@ -0,0 +1,463 @@ +{ + "address": "Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7", + "metadata": { + "name": "farming_idle", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "harvest", + "discriminator": [ + 228, + 241, + 31, + 182, + 53, + 169, + 59, + 199 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "Farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "Farm" + } + ] + } + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [] + }, + { + "name": "initialize_farm", + "discriminator": [ + 252, + 28, + 185, + 172, + 244, + 74, + 117, + 165 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "player" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "player", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "initialize_leaderboard", + "discriminator": [ + 47, + 23, + 34, + 39, + 46, + 108, + 91, + 176 + ], + "accounts": [ + { + "name": "leaderboard", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 101, + 97, + 100, + 101, + 114, + 98, + 111, + 97, + 114, + 100 + ] + } + ] + } + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "submit_farm", + "discriminator": [ + 8, + 195, + 198, + 253, + 255, + 190, + 96, + 102 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "Farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "Farm" + } + ] + } + }, + { + "name": "leaderboard", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 101, + 97, + 100, + 101, + 114, + 98, + 111, + 97, + 114, + 100 + ] + } + ] + } + }, + { + "name": "owner", + "signer": true, + "relations": [ + "farm" + ] + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [] + }, + { + "name": "upgrade_farm", + "discriminator": [ + 110, + 239, + 193, + 1, + 165, + 101, + 54, + 200 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "Farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "Farm" + } + ] + } + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [ + { + "name": "crop_index", + "type": "u8" + }, + { + "name": "amount", + "type": "u8" + } + ] + } + ], + "accounts": [ + { + "name": "Farm", + "discriminator": [ + 161, + 156, + 211, + 253, + 250, + 64, + 53, + 250 + ] + }, + { + "name": "Leaderboard", + "discriminator": [ + 247, + 186, + 238, + 243, + 194, + 30, + 9, + 36 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InsufficientHarvestPoints", + "msg": "Not enough harvest points to fund the upgrade" + }, + { + "code": 6001, + "name": "InvalidAmount", + "msg": "Amount must be greater than zero" + }, + { + "code": 6002, + "name": "InvalidCrop", + "msg": "Not a valid crop index" + }, + { + "code": 6003, + "name": "Overflow", + "msg": "Overflow" + } + ], + "types": [ + { + "name": "Farm", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "docs": [ + "The wallet that created the farm and holds the funds (signs via MWA)." + ], + "type": "pubkey" + }, + { + "name": "player", + "docs": [ + "The local burner wallet that signs gameplay transactions without approval prompts." + ], + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "created_at", + "type": "i64" + }, + { + "name": "last_harvested", + "type": "i64" + }, + { + "name": "harvest_points", + "type": "u64" + }, + { + "name": "crops", + "docs": [ + "Plots owned per crop, indexed like CROP_POINTS and CROP_COSTS." + ], + "type": { + "array": [ + "u16", + 8 + ] + } + }, + { + "name": "high_score", + "type": "u64" + } + ] + } + }, + { + "name": "Leaderboard", + "type": { + "kind": "struct", + "fields": [ + { + "name": "entries", + "type": { + "array": [ + { + "defined": { + "name": "LeaderboardEntry" + } + }, + 5 + ] + } + } + ] + } + }, + { + "name": "LeaderboardEntry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "docs": [ + "The owner wallet behind the farm, not the burner player wallet." + ], + "type": "pubkey" + }, + { + "name": "points", + "type": "u64" + } + ] + } + } + ], + "constants": [ + { + "name": "FARM_SEED", + "type": "bytes", + "value": "[102, 97, 114, 109]" + }, + { + "name": "LEADERBOARD_SEED", + "type": "bytes", + "value": "[108, 101, 97, 100, 101, 114, 98, 111, 97, 114, 100]" + } + ] +} \ No newline at end of file diff --git a/mobile/sample-expo-kit-farming-idle/anchor/target/types/farming_idle.ts b/mobile/sample-expo-kit-farming-idle/anchor/target/types/farming_idle.ts new file mode 100644 index 0000000..527f463 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/target/types/farming_idle.ts @@ -0,0 +1,469 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/farming_idle.json`. + */ +export type FarmingIdle = { + "address": "Hyfitmh1dAkw852R3DrXtWmLcmVoVkiR4H6D9DAMytH7", + "metadata": { + "name": "farmingIdle", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "harvest", + "discriminator": [ + 228, + 241, + 31, + 182, + 53, + 169, + 59, + 199 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "farm" + } + ] + } + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [] + }, + { + "name": "initializeFarm", + "discriminator": [ + 252, + 28, + 185, + 172, + 244, + 74, + 117, + 165 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "player" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "player", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "initializeLeaderboard", + "discriminator": [ + 47, + 23, + 34, + 39, + 46, + 108, + 91, + 176 + ], + "accounts": [ + { + "name": "leaderboard", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 101, + 97, + 100, + 101, + 114, + 98, + 111, + 97, + 114, + 100 + ] + } + ] + } + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "submitFarm", + "discriminator": [ + 8, + 195, + 198, + 253, + 255, + 190, + 96, + 102 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "farm" + } + ] + } + }, + { + "name": "leaderboard", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 101, + 97, + 100, + 101, + 114, + 98, + 111, + 97, + 114, + 100 + ] + } + ] + } + }, + { + "name": "owner", + "signer": true, + "relations": [ + "farm" + ] + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [] + }, + { + "name": "upgradeFarm", + "discriminator": [ + 110, + 239, + 193, + 1, + 165, + 101, + 54, + 200 + ], + "accounts": [ + { + "name": "farm", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 97, + 114, + 109 + ] + }, + { + "kind": "account", + "path": "farm.player", + "account": "farm" + }, + { + "kind": "account", + "path": "farm.owner", + "account": "farm" + } + ] + } + }, + { + "name": "player", + "signer": true, + "relations": [ + "farm" + ] + } + ], + "args": [ + { + "name": "cropIndex", + "type": "u8" + }, + { + "name": "amount", + "type": "u8" + } + ] + } + ], + "accounts": [ + { + "name": "farm", + "discriminator": [ + 161, + 156, + 211, + 253, + 250, + 64, + 53, + 250 + ] + }, + { + "name": "leaderboard", + "discriminator": [ + 247, + 186, + 238, + 243, + 194, + 30, + 9, + 36 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "insufficientHarvestPoints", + "msg": "Not enough harvest points to fund the upgrade" + }, + { + "code": 6001, + "name": "invalidAmount", + "msg": "Amount must be greater than zero" + }, + { + "code": 6002, + "name": "invalidCrop", + "msg": "Not a valid crop index" + }, + { + "code": 6003, + "name": "overflow", + "msg": "Overflow" + } + ], + "types": [ + { + "name": "farm", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "docs": [ + "The wallet that created the farm and holds the funds (signs via MWA)." + ], + "type": "pubkey" + }, + { + "name": "player", + "docs": [ + "The local burner wallet that signs gameplay transactions without approval prompts." + ], + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "createdAt", + "type": "i64" + }, + { + "name": "lastHarvested", + "type": "i64" + }, + { + "name": "harvestPoints", + "type": "u64" + }, + { + "name": "crops", + "docs": [ + "Plots owned per crop, indexed like CROP_POINTS and CROP_COSTS." + ], + "type": { + "array": [ + "u16", + 8 + ] + } + }, + { + "name": "highScore", + "type": "u64" + } + ] + } + }, + { + "name": "leaderboard", + "type": { + "kind": "struct", + "fields": [ + { + "name": "entries", + "type": { + "array": [ + { + "defined": { + "name": "leaderboardEntry" + } + }, + 5 + ] + } + } + ] + } + }, + { + "name": "leaderboardEntry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "docs": [ + "The owner wallet behind the farm, not the burner player wallet." + ], + "type": "pubkey" + }, + { + "name": "points", + "type": "u64" + } + ] + } + } + ], + "constants": [ + { + "name": "farmSeed", + "type": "bytes", + "value": "[102, 97, 114, 109]" + }, + { + "name": "leaderboardSeed", + "type": "bytes", + "value": "[108, 101, 97, 100, 101, 114, 98, 111, 97, 114, 100]" + } + ] +}; diff --git a/mobile/sample-expo-kit-farming-idle/anchor/tests/create-funded-signer.ts b/mobile/sample-expo-kit-farming-idle/anchor/tests/create-funded-signer.ts new file mode 100644 index 0000000..5a6291f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/tests/create-funded-signer.ts @@ -0,0 +1,37 @@ +import { + airdropFactory, + generateKeyPairSigner, + lamports, + type createSolanaRpc, + type createSolanaRpcSubscriptions, + type GetSignatureStatusesApi, + type KeyPairSigner, + type Lamports, + type RequestAirdropApi, + type Rpc, + type RpcSubscriptions, + type SignatureNotificationsApi, +} from '@solana/kit' + +// Generates a fresh keypair signer and funds it with an airdrop. Using a new +// signer per run keeps the tests independent of earlier state, so they pass +// against any validator with a faucet — not just the throwaway one that +// `anchor test` starts. +export async function createFundedSigner({ + amount = lamports(5_000_000_000n), + rpc, + rpcSubscriptions, +}: { + amount?: Lamports + rpc: ReturnType + rpcSubscriptions: ReturnType +}): Promise { + const signer = await generateKeyPairSigner() + // Airdrops only exist on test clusters, which the rpc type can't know statically. + const airdrop = airdropFactory({ + rpc: rpc as unknown as Rpc, + rpcSubscriptions: rpcSubscriptions as unknown as RpcSubscriptions, + }) + await airdrop({ commitment: 'confirmed', lamports: amount, recipientAddress: signer.address }) + return signer +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/tests/farming_idle.test.ts b/mobile/sample-expo-kit-farming-idle/anchor/tests/farming_idle.test.ts new file mode 100644 index 0000000..721d418 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/tests/farming_idle.test.ts @@ -0,0 +1,187 @@ +import { + appendTransactionMessageInstruction, + assertIsTransactionWithBlockhashLifetime, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + generateKeyPairSigner, + getSignatureFromTransaction, + pipe, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, + type Address, + type Instruction, + type KeyPairSigner, +} from '@solana/kit' +import { beforeAll, describe, expect, it } from 'vitest' +import { + fetchFarm, + fetchLeaderboard, + fetchMaybeLeaderboard, + findFarmPda, + findLeaderboardPda, + getHarvestInstruction, + getInitializeFarmInstructionAsync, + getInitializeLeaderboardInstructionAsync, + getSubmitFarmInstruction, + getUpgradeFarmInstruction, + upgradeCost, +} from '../src' +import { createFundedSigner } from './create-funded-signer' + +// The `anchor test` command starts a local validator and provides this variable. +const rpcUrl = process.env.ANCHOR_PROVIDER_URL! +const rpc = createSolanaRpc(rpcUrl) +const rpcSubscriptions = createSolanaRpcSubscriptions(rpcUrl.replace('http', 'ws').replace('8899', '8900')) +const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }) + +describe('farming_idle', () => { + // The owner wallet holds the funds; the player is the burner wallet that + // signs gameplay transactions. In the app the owner signs through Mobile + // Wallet Adapter, but the program only sees two ordinary signers. + let owner: KeyPairSigner + let player: KeyPairSigner + let farm: Address + + beforeAll(async () => { + owner = await createFundedSigner({ rpc, rpcSubscriptions }) + player = await createFundedSigner({ rpc, rpcSubscriptions }) + farm = (await findFarmPda({ player: player.address, owner: owner.address }))[0] + }) + + it('initializes a farm', async () => { + // ARRANGE + const instruction = await getInitializeFarmInstructionAsync({ owner, player }) + + // ACT + await sendInstruction({ instruction, feePayer: owner }) + + // ASSERT + const { data } = await fetchFarm(rpc, farm) + expect(data.owner).toEqual(owner.address) + expect(data.player).toEqual(player.address) + expect(data.harvestPoints).toEqual(0n) + expect(data.crops).toEqual(new Array(8).fill(0)) + expect(data.highScore).toEqual(0n) + expect(data.lastHarvested).toEqual(data.createdAt) + }) + + it('tops up an empty player wallet on initialize', async () => { + // ARRANGE: a brand-new burner wallet with no lamports at all. + const emptyPlayer = await generateKeyPairSigner() + const instruction = await getInitializeFarmInstructionAsync({ owner, player: emptyPlayer }) + + // ACT: the owner pays, so the empty player can still co-sign. + await sendInstruction({ instruction, feePayer: owner }) + + // ASSERT: the program moved gas money from the owner to the player. + const { value: balance } = await rpc.getBalance(emptyPlayer.address).send() + expect(balance).toEqual(10_000_000n) + }) + + it('rejects an upgrade the farm cannot afford', async () => { + // ARRANGE + const instruction = getUpgradeFarmInstruction({ farm, player, cropIndex: 0, amount: 1 }) + + // ACT + ASSERT + await expect(sendInstruction({ instruction, feePayer: player })).rejects.toThrow() + }) + + it('rejects a harvest signed by a different player', async () => { + // ARRANGE + const intruder = await createFundedSigner({ rpc, rpcSubscriptions }) + const instruction = getHarvestInstruction({ farm, player: intruder }) + + // ACT + ASSERT + await expect(sendInstruction({ instruction, feePayer: intruder })).rejects.toThrow() + }) + + it('harvests points with only the player signing', async () => { + // ARRANGE + const instruction = getHarvestInstruction({ farm, player }) + + // ACT + await sendInstruction({ instruction, feePayer: player }) + + // ASSERT: a tap on a farm without crops yields exactly one point. + const { data } = await fetchFarm(rpc, farm) + expect(data.harvestPoints).toEqual(1n) + }) + + it('buys a crop plot once the farm can afford it', async () => { + // ARRANGE: tap until the farm can pay for the first plot of the first crop. + const cost = upgradeCost(0, 0, 1) + let points = 1n + while (points < cost) { + await sendInstruction({ instruction: getHarvestInstruction({ farm, player }), feePayer: player }) + points = (await fetchFarm(rpc, farm)).data.harvestPoints + } + + // ACT + await sendInstruction({ + instruction: getUpgradeFarmInstruction({ farm, player, cropIndex: 0, amount: 1 }), + feePayer: player, + }) + + // ASSERT + const { data } = await fetchFarm(rpc, farm) + expect(data.crops[0]).toEqual(1) + expect(data.harvestPoints).toEqual(points - cost) + }) + + it('initializes the leaderboard once', async () => { + // ARRANGE + const [leaderboard] = await findLeaderboardPda() + const existing = await fetchMaybeLeaderboard(rpc, leaderboard) + if (existing.exists) { + // Another run against the same validator already created the singleton. + return + } + const instruction = await getInitializeLeaderboardInstructionAsync({ payer: owner }) + + // ACT + await sendInstruction({ instruction, feePayer: owner }) + + // ASSERT + const { data } = await fetchLeaderboard(rpc, leaderboard) + expect(data.entries).toHaveLength(5) + expect(data.entries.every((entry) => entry.points === 0n)).toBe(true) + }) + + it('submits the score and resets the farm', async () => { + // ARRANGE: harvest once more so the run has a score to submit. + await sendInstruction({ instruction: getHarvestInstruction({ farm, player }), feePayer: player }) + const [leaderboard] = await findLeaderboardPda() + const scoreBefore = (await fetchFarm(rpc, farm)).data.harvestPoints + expect(scoreBefore > 0n).toBe(true) + const instruction = getSubmitFarmInstruction({ farm, leaderboard, owner, player }) + + // ACT + await sendInstruction({ instruction, feePayer: owner }) + + // ASSERT: the owner wallet is on the leaderboard and the run started over. + const { data } = await fetchLeaderboard(rpc, leaderboard) + expect(data.entries.some((entry) => entry.owner === owner.address && entry.points >= scoreBefore)).toBe(true) + const { data: farmData } = await fetchFarm(rpc, farm) + expect(farmData.harvestPoints).toEqual(0n) + expect(farmData.crops).toEqual(new Array(8).fill(0)) + expect(farmData.highScore >= scoreBefore).toBe(true) + }) +}) + +// Helper function to keep the tests DRY +async function sendInstruction({ feePayer, instruction }: { feePayer: KeyPairSigner; instruction: Instruction }) { + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send() + const transaction = await pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayerSigner(feePayer, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + (tx) => appendTransactionMessageInstruction(instruction, tx), + (tx) => signTransactionMessageWithSigners(tx), + ) + assertIsTransactionWithBlockhashLifetime(transaction) + await sendAndConfirm(transaction, { commitment: 'confirmed' }) + return getSignatureFromTransaction(transaction) +} diff --git a/mobile/sample-expo-kit-farming-idle/anchor/tsconfig.json b/mobile/sample-expo-kit-farming-idle/anchor/tsconfig.json new file mode 100644 index 0000000..99b6352 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/anchor/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["node_modules"] +} diff --git a/mobile/sample-expo-kit-farming-idle/app.json b/mobile/sample-expo-kit-farming-idle/app.json new file mode 100644 index 0000000..23b3227 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/app.json @@ -0,0 +1,26 @@ +{ + "expo": { + "scheme": "sample-expo-kit-farming-idle", + "userInterfaceStyle": "automatic", + "orientation": "default", + "web": { + "output": "static" + }, + "name": "sample-expo-kit-farming-idle", + "slug": "sample-expo-kit-farming-idle", + "android": { + "package": "com.anonymous.sample_expo_kit_farming_idle" + }, + "plugins": [ + [ + "expo-dev-client", + { + "showMenuAtLaunch": false, + "skipOnboarding": true + } + ], + "expo-router", + "expo-secure-store" + ] + } +} diff --git a/mobile/sample-expo-kit-farming-idle/eslint.config.js b/mobile/sample-expo-kit-farming-idle/eslint.config.js new file mode 100644 index 0000000..f311114 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/eslint.config.js @@ -0,0 +1,10 @@ +// https://docs.expo.dev/guides/using-eslint/ +const { defineConfig } = require('eslint/config') +const expoConfig = require('eslint-config-expo/flat') + +module.exports = defineConfig([ + expoConfig, + { + ignores: ['anchor/src/client/js/generated/*', 'anchor/target/*', 'dist/*'], + }, +]) diff --git a/mobile/sample-expo-kit-farming-idle/index.js b/mobile/sample-expo-kit-farming-idle/index.js new file mode 100644 index 0000000..f6f5291 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/index.js @@ -0,0 +1,3 @@ +// index.js +import './polyfill' +import 'expo-router/entry' diff --git a/mobile/sample-expo-kit-farming-idle/metro.config.js b/mobile/sample-expo-kit-farming-idle/metro.config.js new file mode 100644 index 0000000..53e25cb --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/metro.config.js @@ -0,0 +1,21 @@ +const { getDefaultConfig } = require('expo/metro-config') +const { withUniwindConfig } = require('uniwind/metro') // make sure this import exists +const path = require('path') + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname) + +// Apply uniwind modifications before exporting +const uniwindConfig = withUniwindConfig(config, { + // relative path to your global.css file + cssEntryFile: './src/global.css', + // optional: path to typings + dtsFile: './src/uniwind-types.d.ts', +}) + +// Cache transforms per project; the machine-wide Metro cache can serve stale transforms from other projects. +uniwindConfig.cacheStores = ({ FileStore }) => [ + new FileStore({ root: path.join(__dirname, 'node_modules', '.cache', 'metro') }), +] + +module.exports = uniwindConfig diff --git a/mobile/sample-expo-kit-farming-idle/og-image.png b/mobile/sample-expo-kit-farming-idle/og-image.png new file mode 100644 index 0000000..d8205a2 Binary files /dev/null and b/mobile/sample-expo-kit-farming-idle/og-image.png differ diff --git a/mobile/sample-expo-kit-farming-idle/package.json b/mobile/sample-expo-kit-farming-idle/package.json new file mode 100644 index 0000000..b7e7b0f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/package.json @@ -0,0 +1,118 @@ +{ + "name": "sample-expo-kit-farming-idle", + "displayName": "Farming Idle sample", + "description": "A sample idle farming game with a burner player wallet, on-chain upgrades, and a global leaderboard, built with Expo, React Native, Solana Kit, Anchor, and Uniwind.", + "usecase": "Mobile", + "keywords": [ + "anchor", + "expo", + "mobile-wallet-adapter", + "react-native", + "sample", + "solana-kit", + "tailwind", + "uniwind" + ], + "create-solana-dapp": { + "instructions": [ + "Verify your Android development environment:", + "+npx -y solana-mobile@latest doctor", + "Build the Android app locally:", + "+{pm} run android", + "To configure the Anchor deployment keypair and generate the program client (requires the Rust, Solana, and Anchor toolchains):", + "+{pm} run anchor:setup", + "Run Anchor commands:", + "+{pm} run anchor:build | anchor:test | anchor:localnet", + "Deploy the program to a cluster:", + "+{pm} run anchor:deploy:localnet | anchor:deploy:devnet" + ], + "rename": { + "Farming Idle": { + "in": [ + "src/app" + ], + "to": "{{name}}" + } + }, + "skills": [ + "https://github.com/solana-foundation/solana-dev-skill", + "https://github.com/solana-mobile/solana-mobile-dev-skill" + ], + "versions": { + "adb": "33.0.0", + "anchor": "1.1.2", + "solana": "3.1.10" + } + }, + "main": "./index.js", + "version": "1.0.0", + "scripts": { + "anchor": "cd anchor && anchor", + "anchor:build": "cd anchor && anchor build", + "anchor:deploy:devnet": "cd anchor && anchor build && anchor program deploy --provider.cluster devnet", + "anchor:deploy:localnet": "cd anchor && anchor build && anchor program deploy --provider.cluster localnet", + "anchor:localnet": "cd anchor && anchor localnet", + "anchor:setup": "cd anchor && anchor keys sync && anchor build && cd .. && npm run codama:js", + "anchor:test": "cd anchor && anchor test", + "android": "expo run:android", + "android:build": "expo prebuild -p android", + "build": "tsc --noEmit && npm run android:build", + "ci": "tsc --noEmit && npm run lint:check && npm run format:check && npm run android:build", + "codama:js": "codama run js -c ./anchor/codama.mjs", + "dev": "expo start --clear --dev-client --reset-cache", + "doctor": "npx -y expo-doctor@latest", + "format": "prettier --write .", + "format:check": "prettier --check .", + "ios": "expo run:ios", + "lint": "expo lint --fix", + "lint:check": "expo lint", + "start": "expo start", + "web": "expo start --web" + }, + "dependencies": { + "@expo/metro-runtime": "~57.0.5", + "@solana-program/system": "^0.13.0", + "@solana/kit": "^7.0.0", + "@tanstack/react-query": "^5.90.7", + "@wallet-ui/react-native-kit": "^4.3.0", + "expo": "~57.0.6", + "expo-clipboard": "~57.0.1", + "expo-constants": "~57.0.5", + "expo-dev-client": "^57.0.6", + "expo-linking": "~57.0.3", + "expo-router": "~57.0.6", + "expo-secure-store": "~57.0.1", + "expo-status-bar": "~57.0.1", + "expo-system-ui": "~57.0.1", + "react": "~19.2.3", + "react-dom": "19.2.3", + "react-native": "0.86.2", + "react-native-nitro-modules": "^0.36.5", + "react-native-quick-base64": "^3.0.1", + "react-native-quick-crypto": "^1.1.6", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.2", + "tailwindcss": "~4.1.16", + "uniwind": "~1.0.0" + }, + "devDependencies": { + "@codama/cli": "^1.6.0", + "@codama/nodes-from-anchor": "^1.5.3", + "@codama/renderers-js": "^2.3.1", + "@types/react": "~19.2.14", + "eslint": "^9.25.0", + "eslint-config-expo": "~57.0.0", + "prettier": "^3.9.6", + "typescript": "~6.0.3", + "vitest": "^4.1.10" + }, + "overrides": { + "react-native-reanimated": "4.5.4", + "react-native-worklets": "0.10.4" + }, + "resolutions": { + "react-native-reanimated": "4.5.4", + "react-native-worklets": "0.10.4" + }, + "private": true +} diff --git a/mobile/sample-expo-kit-farming-idle/pnpm-workspace.yaml b/mobile/sample-expo-kit-farming-idle/pnpm-workspace.yaml new file mode 100644 index 0000000..41f20db --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +# pnpm 10 and later no longer read the `pnpm` field from package.json, so the +# overrides that npm, bun, and yarn take from `overrides`/`resolutions` there +# have to be repeated here. Keep the three lists in step. +overrides: + react-native-reanimated: 4.5.4 + react-native-worklets: 0.10.4 diff --git a/mobile/sample-expo-kit-farming-idle/polyfill.js b/mobile/sample-expo-kit-farming-idle/polyfill.js new file mode 100644 index 0000000..5454e3c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/polyfill.js @@ -0,0 +1,4 @@ +// polyfill.js +import { install } from 'react-native-quick-crypto' + +install() diff --git a/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/_layout.tsx b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..d37dbbc --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/_layout.tsx @@ -0,0 +1,39 @@ +import { Redirect, Tabs } from 'expo-router' +import { Text } from 'react-native' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { useNavColors } from '../../features/theme/use-nav-colors' + +function tabIcon(emoji: string) { + return function TabIcon({ focused }: { focused: boolean }) { + return {emoji} + } +} + +export default function TabsLayout() { + const { account } = useMobileWallet() + const colors = useNavColors() + + // Everything behind the tabs needs a connected wallet. + if (!account) { + return + } + + return ( + + + + + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/crops.tsx b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/crops.tsx new file mode 100644 index 0000000..f7b7cc5 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/crops.tsx @@ -0,0 +1,5 @@ +import { CropsFeature } from '../../features/farm/crops-feature' + +export default function CropsScreen() { + return +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/farm.tsx b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/farm.tsx new file mode 100644 index 0000000..e4325ce --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/farm.tsx @@ -0,0 +1,5 @@ +import { FarmFeature } from '../../features/farm/farm-feature' + +export default function FarmScreen() { + return +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/leaderboard.tsx b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/leaderboard.tsx new file mode 100644 index 0000000..b456cd1 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/leaderboard.tsx @@ -0,0 +1,5 @@ +import { LeaderboardFeature } from '../../features/leaderboard/leaderboard-feature' + +export default function LeaderboardScreen() { + return +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/wallet.tsx b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/wallet.tsx new file mode 100644 index 0000000..230734e --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/(tabs)/wallet.tsx @@ -0,0 +1,5 @@ +import { WalletFeature } from '../../features/wallet/wallet-feature' + +export default function WalletScreen() { + return +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/_layout.tsx b/mobile/sample-expo-kit-farming-idle/src/app/_layout.tsx new file mode 100644 index 0000000..b0a022c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/_layout.tsx @@ -0,0 +1,31 @@ +import '../global.css' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { Slot } from 'expo-router' +import { + AppIdentity, + createSolanaDevnet, + createSolanaLocalnet, + MobileWalletProvider, +} from '@wallet-ui/react-native-kit' +import { NetworkProvider } from '../features/network/network-provider' + +// Localnet: run `npx solana-mobile localnet` to start it and forward it to the device. +const networks = [createSolanaDevnet(), createSolanaLocalnet({ url: 'http://localhost:8899' })] +const identity: AppIdentity = { name: 'Farming Idle' } +const queryClient = new QueryClient() + +export default function Layout() { + return ( + + ( + + + + )} + /> + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/app/index.tsx b/mobile/sample-expo-kit-farming-idle/src/app/index.tsx new file mode 100644 index 0000000..3dfbfeb --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/app/index.tsx @@ -0,0 +1,64 @@ +import { Redirect } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import { useState } from 'react' +import { Pressable, Text, View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { NetworkUiSelect } from '../features/network/network-ui-select' +import { formatError } from '../utils/format-error' + +// The connect screen. Everything else lives behind the tabs, which redirect +// back here when the wallet disconnects. +export default function App() { + const insets = useSafeAreaInsets() + const { account, connect } = useMobileWallet() + const [error, setError] = useState(null) + const [isBusy, setIsBusy] = useState(false) + + if (account) { + return + } + + // Wallet actions can be declined or fail. Run them here so a rejection never + // escapes as an unhandled promise and the reason ends up on screen. + async function run(action: () => Promise) { + if (isBusy) { + return + } + setIsBusy(true) + setError(null) + try { + await action() + } catch (e) { + setError(formatError(e)) + } finally { + setIsBusy(false) + } + } + + return ( + + 🌾 + Farming Idle + + Tap to harvest, buy crops that farm for you, and put your best run on the on-chain leaderboard. A local player + wallet signs the gameplay, so only funding and submitting need your approval. + + void run(connect)} + className={`bg-blue-600 px-6 py-3 rounded-xl active:bg-blue-700 ${isBusy ? 'opacity-50' : ''}`} + > + {isBusy ? 'Working...' : 'Connect Wallet'} + + {error ? {error} : null} + + + + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/components/app-address-link.tsx b/mobile/sample-expo-kit-farming-idle/src/components/app-address-link.tsx new file mode 100644 index 0000000..46ffe27 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/components/app-address-link.tsx @@ -0,0 +1,35 @@ +import * as Clipboard from 'expo-clipboard' +import { openURL } from 'expo-linking' +import { useState } from 'react' +import { Pressable, Text, View } from 'react-native' +import { useNetwork } from '../features/network/use-network' +import { ellipsify } from '../utils/ellipsify' + +// A labeled address with an explorer link and a copy button. Tapping the +// address also copies it. +export function AppAddressLink({ address, label }: { address: string; label: string }) { + const { getExplorerUrl } = useNetwork() + const [copied, setCopied] = useState(false) + + async function copy() { + await Clipboard.setStringAsync(address) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( + + void copy()}> + + {label}: {ellipsify(address)} + + + void openURL(getExplorerUrl(`address/${address}`))} hitSlop={8}> + 🌐 + + void copy()} hitSlop={8}> + {copied ? '✅' : '📋'} + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/components/app-button.tsx b/mobile/sample-expo-kit-farming-idle/src/components/app-button.tsx new file mode 100644 index 0000000..e7177e7 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/components/app-button.tsx @@ -0,0 +1,29 @@ +import { Pressable, Text } from 'react-native' + +const variants = { + danger: 'bg-red-500 active:bg-red-600', + primary: 'bg-blue-600 active:bg-blue-700', + success: 'bg-green-600 active:bg-green-700', +} + +export function AppButton({ + disabled = false, + label, + onPress, + variant = 'primary', +}: { + disabled?: boolean + label: string + onPress: () => void + variant?: keyof typeof variants +}) { + return ( + + {label} + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-airdrop-mutation.ts b/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-airdrop-mutation.ts new file mode 100644 index 0000000..0a57852 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-airdrop-mutation.ts @@ -0,0 +1,25 @@ +import { lamports, type RequestAirdropApi, type Rpc } from '@solana/kit' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { waitForConfirmation } from '../../../utils/wait-for-confirmation' + +// Requests an airdrop for the connected wallet. Only works on clusters with a +// faucet, like localnet. +export function useAirdropMutation() { + const { account, chain, client } = useMobileWallet() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + if (!account) { + throw new Error('Wallet not connected') + } + // requestAirdrop only exists on test clusters, which the client type can't know statically. + const rpc = client.rpc as unknown as Rpc + const signature = await rpc.requestAirdrop(account.address, lamports(1_000_000_000n)).send() + await waitForConfirmation(client.rpc, signature) + return signature + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: ['balance', chain, account?.address] }), + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-balance-query.ts b/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-balance-query.ts new file mode 100644 index 0000000..246d9a1 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/account/data-access/use-balance-query.ts @@ -0,0 +1,21 @@ +import type { Address } from '@solana/kit' +import { useQuery } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' + +// A wallet's balance in lamports. Takes the address as a parameter so the +// wallet screen can watch both the owner and the player wallet. +export function useBalanceQuery({ address }: { address: Address | undefined }) { + const { chain, client } = useMobileWallet() + + return useQuery({ + enabled: !!address, + queryKey: ['balance', chain, address], + queryFn: async () => { + if (!address) { + return null + } + const { value } = await client.rpc.getBalance(address).send() + return value + }, + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/crops-feature.tsx b/mobile/sample-expo-kit-farming-idle/src/features/farm/crops-feature.tsx new file mode 100644 index 0000000..1d35b94 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/crops-feature.tsx @@ -0,0 +1,68 @@ +import { CROP_POINTS, upgradeCost } from '@project/anchor' +import { useState } from 'react' +import { FlatList, Text, View } from 'react-native' +import { formatError } from '../../utils/format-error' +import { formatPoints } from '../../utils/format-points' +import { CROPS } from './crops' +import { useFarmProgram } from './data-access/use-farm-program' +import { CropUiCard } from './ui/crop-ui-card' + +export function CropsFeature() { + const { farmQuery, upgradeFarmMutation } = useFarmProgram() + const [pendingIndex, setPendingIndex] = useState(null) + + const farm = farmQuery.data?.farm + + async function onBuy(cropIndex: number) { + setPendingIndex(cropIndex) + try { + await upgradeFarmMutation.mutateAsync({ cropIndex }) + } catch { + // The failure reason renders through upgradeFarmMutation.error. + } finally { + setPendingIndex(null) + } + } + + return ( + + crop.name} + contentContainerClassName="px-4 py-4 gap-3" + ListHeaderComponent={ + + + {farm ? formatPoints(farm.harvestPoints) : '0'} 🌾 to spend + + + Crops cost banked 🌾 — harvest first. Each plot costs 15% more than the last. + + {!farm ? ( + Create your farm first. + ) : null} + {upgradeFarmMutation.error ? ( + {formatError(upgradeFarmMutation.error)} + ) : null} + + } + renderItem={({ index, item: crop }) => { + const owned = farm?.crops[index] ?? 0 + const cost = upgradeCost(index, owned, 1) + return ( + = cost} + isPending={pendingIndex === index} + name={crop.name} + onBuy={() => void onBuy(index)} + owned={owned} + pointsPerPlot={CROP_POINTS[index]} + /> + ) + }} + /> + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/crops.ts b/mobile/sample-expo-kit-farming-idle/src/features/farm/crops.ts new file mode 100644 index 0000000..759036a --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/crops.ts @@ -0,0 +1,12 @@ +// Display metadata for the crops, indexed like the program's CROP_COSTS and +// CROP_POINTS tables. Costs and yields live on-chain — see anchor/src/client/js. +export const CROPS = [ + { emoji: '🥬', name: 'Cabbage Patch' }, + { emoji: '🍅', name: 'Tomato Garden' }, + { emoji: '🍓', name: 'Strawberry Patch' }, + { emoji: '🌽', name: 'Corn Field' }, + { emoji: '🍎', name: 'Apple Orchard' }, + { emoji: '🥭', name: 'Exotic Fruit Farm' }, + { emoji: '🍄', name: 'Mushroom Grove' }, + { emoji: '🌾', name: 'Golden Greenhouse' }, +] diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-available-harvest.ts b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-available-harvest.ts new file mode 100644 index 0000000..0509c61 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-available-harvest.ts @@ -0,0 +1,19 @@ +import { harvestYield, pointsPerSecond, type Farm } from '@project/anchor' +import { useEffect, useState } from 'react' + +// Live estimate of what a harvest would yield right now, ticking a few times a +// second. The program computes the real number from the cluster clock when the +// harvest lands. +export function useAvailableHarvest(farm: Farm | null | undefined) { + const [now, setNow] = useState(() => Date.now() / 1000) + + useEffect(() => { + const interval = setInterval(() => setNow(Date.now() / 1000), 250) + return () => clearInterval(interval) + }, []) + + if (!farm) { + return { availableHarvest: 0n, perSecond: 0n } + } + return { availableHarvest: harvestYield(farm, now), perSecond: pointsPerSecond(farm.crops) } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-program.ts b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-program.ts new file mode 100644 index 0000000..fc6803c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-program.ts @@ -0,0 +1,91 @@ +import { + findLeaderboardPda, + getHarvestInstruction, + getInitializeFarmInstructionAsync, + getInitializeLeaderboardInstructionAsync, + getSubmitFarmInstruction, + getUpgradeFarmInstruction, + harvestYield, +} from '@project/anchor' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { usePlayerSigner } from '../../player/data-access/use-player-signer' +import { useSendOwnerInstructions } from '../../transactions/data-access/use-send-owner-instructions' +import { useSendPlayerInstructions } from '../../transactions/data-access/use-send-player-instructions' +import { useFarmQuery } from './use-farm-query' + +// The gameplay mutations. Harvest and upgrade sign with the player wallet only +// (no approval prompt); creating a farm, submitting a score, and creating the +// leaderboard move funds or speak for the owner, so they go through the wallet. +export function useFarmProgram() { + const { chain } = useMobileWallet() + const { data: player } = usePlayerSigner() + const farmQuery = useFarmQuery() + const sendOwnerInstructions = useSendOwnerInstructions() + const sendPlayerInstructions = useSendPlayerInstructions() + const queryClient = useQueryClient() + + function requirePlayer() { + if (!player) { + throw new Error('Player wallet not ready') + } + return player + } + + function requireFarm() { + if (!farmQuery.data?.address) { + throw new Error('Farm not found') + } + return farmQuery.data.address + } + + // Refresh even when a mutation fails: the wallet may have submitted the + // transaction after the app stopped waiting for it. + const onSettled = () => + Promise.all([ + queryClient.invalidateQueries({ queryKey: ['balance', chain] }), + queryClient.invalidateQueries({ queryKey: ['farm', chain] }), + queryClient.invalidateQueries({ queryKey: ['leaderboard', chain] }), + ]) + + return { + farmQuery, + harvestMutation: useMutation({ + mutationFn: async () => { + const farm = requireFarm() + // Estimate the yield for the "+N" toast; the program computes the real number. + const gained = farmQuery.data?.farm ? harvestYield(farmQuery.data.farm, Date.now() / 1000) : 1n + await sendPlayerInstructions([getHarvestInstruction({ farm, player: requirePlayer() })]) + return gained + }, + onSettled, + }), + initializeFarmMutation: useMutation({ + mutationFn: () => + sendOwnerInstructions(async (owner) => [ + await getInitializeFarmInstructionAsync({ owner, player: requirePlayer() }), + ]), + onSettled, + }), + initializeLeaderboardMutation: useMutation({ + mutationFn: () => + sendOwnerInstructions(async (payer) => [await getInitializeLeaderboardInstructionAsync({ payer })]), + onSettled, + }), + submitFarmMutation: useMutation({ + mutationFn: () => + sendOwnerInstructions(async (owner) => { + const [leaderboard] = await findLeaderboardPda() + return [getSubmitFarmInstruction({ farm: requireFarm(), leaderboard, owner, player: requirePlayer() })] + }), + onSettled, + }), + upgradeFarmMutation: useMutation({ + mutationFn: ({ amount = 1, cropIndex }: { amount?: number; cropIndex: number }) => + sendPlayerInstructions([ + getUpgradeFarmInstruction({ amount, cropIndex, farm: requireFarm(), player: requirePlayer() }), + ]), + onSettled, + }), + } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-query.ts b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-query.ts new file mode 100644 index 0000000..1002510 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/data-access/use-farm-query.ts @@ -0,0 +1,24 @@ +import { fetchMaybeFarm, findFarmPda } from '@project/anchor' +import { useQuery } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { usePlayerSigner } from '../../player/data-access/use-player-signer' + +// The farm PDA for the connected owner and the current player wallet. The +// address always resolves; `farm` is null until initializeFarm creates it. +export function useFarmQuery() { + const { account, chain, client } = useMobileWallet() + const { data: player } = usePlayerSigner() + + return useQuery({ + enabled: !!account && !!player, + queryKey: ['farm', chain, account?.address, player?.address], + queryFn: async () => { + if (!account || !player) { + return null + } + const [address] = await findFarmPda({ owner: account.address, player: player.address }) + const maybeFarm = await fetchMaybeFarm(client.rpc, address) + return { address, farm: maybeFarm.exists ? maybeFarm.data : null } + }, + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/farm-feature.tsx b/mobile/sample-expo-kit-farming-idle/src/features/farm/farm-feature.tsx new file mode 100644 index 0000000..53fe653 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/farm-feature.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react' +import { ActivityIndicator, Text, View } from 'react-native' +import { AppButton } from '../../components/app-button' +import { formatError } from '../../utils/format-error' +import { useFarmProgram } from './data-access/use-farm-program' +import { FarmUiEmpty } from './ui/farm-ui-empty' +import { FarmUiGame } from './ui/farm-ui-game' + +export function FarmFeature() { + const { farmQuery, harvestMutation, initializeFarmMutation } = useFarmProgram() + const [gained, setGained] = useState(null) + const toastTimer = useRef | null>(null) + + useEffect(() => { + return () => { + if (toastTimer.current) { + clearTimeout(toastTimer.current) + } + } + }, []) + + async function onHarvest() { + try { + const points = await harvestMutation.mutateAsync() + setGained(points) + if (toastTimer.current) { + clearTimeout(toastTimer.current) + } + toastTimer.current = setTimeout(() => setGained(null), 2_500) + } catch { + // The failure reason renders through harvestMutation.error. + } + } + + if (farmQuery.isLoading) { + return ( + + + + ) + } + + // A failed fetch must not fall through to the empty state: the farm may + // exist, and offering to create it again would just fail on-chain. + if (farmQuery.isError) { + return ( + + {formatError(farmQuery.error)} + void farmQuery.refetch()} /> + + ) + } + + const farm = farmQuery.data?.farm + if (!farm) { + return ( + initializeFarmMutation.mutate()} + /> + ) + } + + return ( + void onHarvest()} + /> + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/crop-ui-card.tsx b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/crop-ui-card.tsx new file mode 100644 index 0000000..8531324 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/crop-ui-card.tsx @@ -0,0 +1,42 @@ +import { Text, View } from 'react-native' +import { AppButton } from '../../../components/app-button' +import { formatPoints } from '../../../utils/format-points' + +export function CropUiCard({ + cost, + emoji, + isAffordable, + isPending, + name, + onBuy, + owned, + pointsPerPlot, +}: { + cost: bigint + emoji: string + isAffordable: boolean + isPending: boolean + name: string + onBuy: () => void + owned: number + pointsPerPlot: bigint +}) { + return ( + + {emoji} + + {name} + + {owned} owned · +{formatPoints(pointsPerPlot)} 🌾/s each + + Next: {formatPoints(cost)} 🌾 + + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-empty.tsx b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-empty.tsx new file mode 100644 index 0000000..888dbc5 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-empty.tsx @@ -0,0 +1,34 @@ +import { ScrollView, Text } from 'react-native' +import { AppButton } from '../../../components/app-button' +import { formatError } from '../../../utils/format-error' + +// The farm has not been created yet: explain the one wallet approval the game +// needs and offer to create it. +export function FarmUiEmpty({ + error, + isPending, + onCreate, +}: { + error: Error | null + isPending: boolean + onCreate: () => void +}) { + return ( + + 🌱 + Start your farm + + Creating the farm is the one transaction your wallet approves. It creates the farm account and moves 0.01 SOL of + gas money to a player wallet generated on this device — from then on, harvesting and buying crops sign locally + without any prompts. + + + {error ? {formatError(error)} : null} + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-game.tsx b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-game.tsx new file mode 100644 index 0000000..f990a8f --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/farm/ui/farm-ui-game.tsx @@ -0,0 +1,68 @@ +import type { Farm } from '@project/anchor' +import { Pressable, ScrollView, Text, View } from 'react-native' +import { formatError } from '../../../utils/format-error' +import { formatPoints } from '../../../utils/format-points' +import { useAvailableHarvest } from '../data-access/use-available-harvest' + +function Stat({ label, value }: { label: string; value: string }) { + return ( + + {value} + {label} + + ) +} + +export function FarmUiGame({ + error, + farm, + gained, + isHarvesting, + onHarvest, +}: { + error: Error | null + farm: Farm + gained: bigint | null + isHarvesting: boolean + onHarvest: () => void +}) { + const { availableHarvest, perSecond } = useAvailableHarvest(farm) + + return ( + + + + + + + + + + Tap to harvest ~{formatPoints(availableHarvest)} 🌾 + + + 🚜 + + + +{gained === null ? '0' : formatPoints(gained)} 🌾 + + {error ? {formatError(error)} : null} + + + + How it works + + Every tap sends a real transaction, signed by the player wallet on this device — no approval prompt. It banks + 1 🌾 plus everything your crops grew since the last harvest. + + + Spend 🌾 on crops to raise your per-second yield, then submit your run to the on-chain leaderboard. + + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/data-access/use-leaderboard-query.ts b/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/data-access/use-leaderboard-query.ts new file mode 100644 index 0000000..59d9fc7 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/data-access/use-leaderboard-query.ts @@ -0,0 +1,27 @@ +import { fetchMaybeLeaderboard, findLeaderboardPda } from '@project/anchor' +import { useQuery } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' + +// Empty leaderboard seats hold the default (all-zero) public key. +const EMPTY_SEAT = '11111111111111111111111111111111' + +// The global top-5 leaderboard, sorted best first. `leaderboard` is null until +// someone runs initializeLeaderboard on this cluster. +export function useLeaderboardQuery() { + const { chain, client } = useMobileWallet() + + return useQuery({ + queryKey: ['leaderboard', chain], + queryFn: async () => { + const [address] = await findLeaderboardPda() + const maybeLeaderboard = await fetchMaybeLeaderboard(client.rpc, address) + if (!maybeLeaderboard.exists) { + return { address, entries: null } + } + const entries = maybeLeaderboard.data.entries + .filter((entry) => entry.owner !== EMPTY_SEAT) + .sort((a, b) => (a.points > b.points ? -1 : a.points < b.points ? 1 : 0)) + return { address, entries } + }, + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/leaderboard-feature.tsx b/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/leaderboard-feature.tsx new file mode 100644 index 0000000..e1b110c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/leaderboard/leaderboard-feature.tsx @@ -0,0 +1,90 @@ +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { Alert, ScrollView, Text, View } from 'react-native' +import { AppButton } from '../../components/app-button' +import { ellipsify } from '../../utils/ellipsify' +import { formatError } from '../../utils/format-error' +import { formatPoints } from '../../utils/format-points' +import { useFarmProgram } from '../farm/data-access/use-farm-program' +import { useLeaderboardQuery } from './data-access/use-leaderboard-query' + +const MEDALS = ['🥇', '🥈', '🥉', '4.', '5.'] + +export function LeaderboardFeature() { + const { account } = useMobileWallet() + const { farmQuery, initializeLeaderboardMutation, submitFarmMutation } = useFarmProgram() + const leaderboardQuery = useLeaderboardQuery() + + const farm = farmQuery.data?.farm + const entries = leaderboardQuery.data?.entries + + function onSubmit() { + Alert.alert( + 'Submit your score?', + 'Your harvested 🌾 goes on the leaderboard and the farm starts over: points and crops reset. Your wallet approves this transaction.', + [ + { style: 'cancel', text: 'Cancel' }, + { onPress: () => submitFarmMutation.mutate(), style: 'destructive', text: 'Submit' }, + ], + ) + } + + return ( + + + Global top 5 + {leaderboardQuery.isLoading ? Loading... : null} + {entries && entries.length === 0 ? ( + No scores yet. Be the first! + ) : null} + {entries?.map((entry, index) => { + const isOwn = entry.owner === account?.address + return ( + + + {MEDALS[index]} {ellipsify(entry.owner)} + {isOwn ? ' (you)' : ''} + + {formatPoints(entry.points)} 🌾 + + ) + })} + {leaderboardQuery.data && !entries ? ( + <> + + The leaderboard account does not exist on this cluster yet. Anyone can create it once. + + initializeLeaderboardMutation.mutate()} + /> + {initializeLeaderboardMutation.error ? ( + {formatError(initializeLeaderboardMutation.error)} + ) : null} + + ) : null} + + + + Your run + + {farm ? formatPoints(farm.harvestPoints) : '0'} 🌾 + + + High score: {farm ? formatPoints(farm.highScore) : '0'} 🌾 + + + {submitFarmMutation.error ? ( + {formatError(submitFarmMutation.error)} + ) : null} + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/network/network-provider.tsx b/mobile/sample-expo-kit-farming-idle/src/features/network/network-provider.tsx new file mode 100644 index 0000000..fc73a8c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/network/network-provider.tsx @@ -0,0 +1,63 @@ +import { SolanaCluster, SolanaClusterId } from '@wallet-ui/react-native-kit' +import { createContext, ReactNode, useMemo, useState } from 'react' + +export interface NetworkProviderContextValue { + chain: SolanaClusterId + endpoint: string + getExplorerUrl(path: string): string + getFaucetUrl(address?: string): string + networks: SolanaCluster[] + selectedNetwork: SolanaCluster + setSelectedNetwork: (network: SolanaCluster) => void +} + +export const NetworkProviderContext = createContext({} as NetworkProviderContextValue) + +export function NetworkProvider({ + networks, + render, +}: { + networks: SolanaCluster[] + render: (value: NetworkProviderContextValue) => ReactNode +}) { + const [selectedNetwork, setSelectedNetwork] = useState(networks[0]) + const value: NetworkProviderContextValue = useMemo( + () => ({ + chain: selectedNetwork.id, + endpoint: selectedNetwork.url, + getExplorerUrl: (path: string) => `https://explorer.solana.com/${path}${getExplorerUrlParam(selectedNetwork)}`, + getFaucetUrl: (address?: string) => `https://faucet.solana.com/${getFaucetUrlParams(selectedNetwork, address)}`, + networks: [...networks].sort((a, b) => a.label.localeCompare(b.label)), + selectedNetwork, + setSelectedNetwork: (network: SolanaCluster) => setSelectedNetwork(network), + }), + [networks, selectedNetwork, setSelectedNetwork], + ) + return {render(value)} +} + +function getExplorerUrlParam(network: SolanaCluster): string { + switch (network.id) { + case 'solana:devnet': + return `?cluster=devnet` + case 'solana:testnet': + return `?cluster=testnet` + case 'solana:localnet': + return `?cluster=custom&customUrl=${encodeURIComponent(network.url)}` + default: + return '' + } +} + +/** The faucet pre-fills its form from these params. It only funds devnet and testnet. */ +function getFaucetUrlParams(network: SolanaCluster, address?: string): string { + const walletAddressParam = address ? `&walletAddress=${address}` : '' + switch (network.id) { + case 'solana:devnet': + return `?cluster=devnet${walletAddressParam}` + case 'solana:testnet': + return `?cluster=testnet${walletAddressParam}` + default: + return address ? `?walletAddress=${address}` : '' + } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/network/network-ui-select.tsx b/mobile/sample-expo-kit-farming-idle/src/features/network/network-ui-select.tsx new file mode 100644 index 0000000..b7a58bf --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/network/network-ui-select.tsx @@ -0,0 +1,28 @@ +import { Pressable, Text, View } from 'react-native' +import { useNetwork } from './use-network' + +// A segmented control for switching between the configured networks. +export function NetworkUiSelect() { + const { networks, selectedNetwork, setSelectedNetwork } = useNetwork() + + return ( + + {networks.map((network) => { + const isSelected = network.id === selectedNetwork.id + return ( + setSelectedNetwork(network)} + className={`px-5 py-2 rounded-full ${isSelected ? 'bg-white dark:bg-black' : ''}`} + > + + {network.label} + + + ) + })} + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/network/use-network.tsx b/mobile/sample-expo-kit-farming-idle/src/features/network/use-network.tsx new file mode 100644 index 0000000..1b922d0 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/network/use-network.tsx @@ -0,0 +1,6 @@ +import { useContext } from 'react' +import { NetworkProviderContext } from './network-provider' + +export function useNetwork() { + return useContext(NetworkProviderContext) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-player-signer.ts b/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-player-signer.ts new file mode 100644 index 0000000..e729eca --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-player-signer.ts @@ -0,0 +1,43 @@ +import { createKeyPairSignerFromPrivateKeyBytes, getBase64Decoder, getBase64Encoder } from '@solana/kit' +import { useQuery } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import * as SecureStore from 'expo-secure-store' + +// One player wallet per owner wallet, so switching accounts switches farms. +// Base58 addresses only use characters SecureStore accepts in keys. +export function playerStoreKey(owner: string) { + return `farming-idle-player-${owner}` +} + +async function loadOrCreatePlayerKey(storeKey: string): Promise { + const stored = await SecureStore.getItemAsync(storeKey) + if (stored) { + return new Uint8Array(getBase64Encoder().encode(stored)) + } + const privateKey = crypto.getRandomValues(new Uint8Array(32)) + await SecureStore.setItemAsync(storeKey, getBase64Decoder().decode(privateKey)) + return privateKey +} + +// The player wallet: a burner keypair generated on the device and kept in the +// keystore (expo-secure-store), so gameplay transactions can sign without +// opening the wallet app. It only ever holds the small gas deposit — the +// owner wallet remains the source of funds. +export function usePlayerSigner() { + const { account } = useMobileWallet() + + return useQuery({ + enabled: !!account, + // The signer is derived from the keystore, not the cluster: cache it for the session. + gcTime: Infinity, + staleTime: Infinity, + queryKey: ['player-signer', account?.address], + queryFn: async () => { + if (!account) { + return null + } + const privateKey = await loadOrCreatePlayerKey(playerStoreKey(account.address)) + return await createKeyPairSignerFromPrivateKeyBytes(privateKey) + }, + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-reset-player-mutation.ts b/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-reset-player-mutation.ts new file mode 100644 index 0000000..189f866 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/player/data-access/use-reset-player-mutation.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import * as SecureStore from 'expo-secure-store' +import { playerStoreKey } from './use-player-signer' + +// Deletes the stored player keypair; the next usePlayerSigner read generates a +// fresh one. The old player's farm PDA and any lamports it still holds become +// unreachable, so the UI asks the user to withdraw first. +export function useResetPlayerMutation() { + const { account } = useMobileWallet() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + if (!account) { + throw new Error('Wallet not connected') + } + await SecureStore.deleteItemAsync(playerStoreKey(account.address)) + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ['player-signer'] }) + await queryClient.invalidateQueries({ queryKey: ['farm'] }) + await queryClient.invalidateQueries({ queryKey: ['balance'] }) + }, + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/theme/use-nav-colors.ts b/mobile/sample-expo-kit-farming-idle/src/features/theme/use-nav-colors.ts new file mode 100644 index 0000000..bbab514 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/theme/use-nav-colors.ts @@ -0,0 +1,15 @@ +import { useColorScheme } from 'react-native' + +// Colors for the react-navigation chrome (headers, tab bar), matching the +// Tailwind palette the screens use. Navigation components take style objects +// instead of classNames, so the color scheme is applied here by hand. +export function useNavColors() { + const isDark = useColorScheme() === 'dark' + return { + background: isDark ? '#000000' : '#ffffff', + border: isDark ? '#1f2937' : '#e5e7eb', + inactive: isDark ? '#9ca3af' : '#6b7280', + text: isDark ? '#ffffff' : '#1f2937', + tint: isDark ? '#3b82f6' : '#2563eb', + } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-owner-instructions.ts b/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-owner-instructions.ts new file mode 100644 index 0000000..7cded61 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-owner-instructions.ts @@ -0,0 +1,55 @@ +import { + appendTransactionMessageInstructions, + assertIsTransactionMessageWithSingleSendingSigner, + createTransactionMessage, + getBase58Decoder, + pipe, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signAndSendTransactionMessageWithSigners, + signature, + type Instruction, + type TransactionSigner, +} from '@solana/kit' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { waitForConfirmation } from '../../../utils/wait-for-confirmation' + +// Sends instructions through the owner wallet with Mobile Wallet Adapter: this +// is the path that opens the wallet app for approval. The wallet's sending +// signer must be the only *sending* signer in the transaction, so the same +// instance pays the fee and is passed to the instruction builders — but the +// player keypair can still co-sign, since it signs as a partial signer before +// the wallet submits. +export function useSendOwnerInstructions() { + const { account, client, getTransactionSigner } = useMobileWallet() + + return async function sendOwnerInstructions( + createInstructions: (signer: TransactionSigner) => Promise, + ) { + if (!account) { + throw new Error('Wallet not connected') + } + const { + context: { slot: minContextSlot }, + value: latestBlockhash, + } = await client.rpc.getLatestBlockhash().send() + + const signer = getTransactionSigner(account.address, minContextSlot) + const instructions = await createInstructions(signer) + + const message = pipe( + createTransactionMessage({ version: 0 }), + (message) => setTransactionMessageFeePayerSigner(signer, message), + (message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message), + (message) => appendTransactionMessageInstructions(instructions, message), + ) + assertIsTransactionMessageWithSingleSendingSigner(message) + + const signatureBytes = await signAndSendTransactionMessageWithSigners(message) + const transactionSignature = signature(getBase58Decoder().decode(signatureBytes)) + + // The wallet submits the transaction, so poll until the cluster confirms it. + await waitForConfirmation(client.rpc, transactionSignature) + return transactionSignature + } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-player-instructions.ts b/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-player-instructions.ts new file mode 100644 index 0000000..7bbe3a8 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/transactions/data-access/use-send-player-instructions.ts @@ -0,0 +1,46 @@ +import { + appendTransactionMessageInstructions, + createTransactionMessage, + getBase64EncodedWireTransaction, + getSignatureFromTransaction, + pipe, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, + type Instruction, +} from '@solana/kit' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { usePlayerSigner } from '../../player/data-access/use-player-signer' +import { waitForConfirmation } from '../../../utils/wait-for-confirmation' + +// Sends instructions signed only by the local player keypair: no wallet app, +// no approval prompt. This is what keeps the harvest-upgrade loop instant. +export function useSendPlayerInstructions() { + const { client } = useMobileWallet() + const { data: player } = usePlayerSigner() + + return async function sendPlayerInstructions(instructions: Instruction[]) { + if (!player) { + throw new Error('Player wallet not ready') + } + const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send() + + const transaction = await pipe( + createTransactionMessage({ version: 0 }), + (message) => setTransactionMessageFeePayerSigner(player, message), + (message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message), + (message) => appendTransactionMessageInstructions(instructions, message), + (message) => signTransactionMessageWithSigners(message), + ) + + const transactionSignature = getSignatureFromTransaction(transaction) + await client.rpc + .sendTransaction(getBase64EncodedWireTransaction(transaction), { + encoding: 'base64', + preflightCommitment: 'confirmed', + }) + .send() + await waitForConfirmation(client.rpc, transactionSignature) + return transactionSignature + } +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-deposit-mutation.ts b/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-deposit-mutation.ts new file mode 100644 index 0000000..21ca2c7 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-deposit-mutation.ts @@ -0,0 +1,29 @@ +import { getTransferSolInstruction } from '@solana-program/system' +import { lamports } from '@solana/kit' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { usePlayerSigner } from '../../player/data-access/use-player-signer' +import { useSendOwnerInstructions } from '../../transactions/data-access/use-send-owner-instructions' + +export const DEPOSIT_LAMPORTS = lamports(1_000_000n) // 0.001 SOL + +// Moves a little more gas money from the owner wallet to the player wallet, +// for when the initial top-up runs out. Signed by the owner through the wallet. +export function useDepositMutation() { + const { chain } = useMobileWallet() + const { data: player } = usePlayerSigner() + const sendOwnerInstructions = useSendOwnerInstructions() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + if (!player) { + throw new Error('Player wallet not ready') + } + return await sendOwnerInstructions(async (owner) => [ + getTransferSolInstruction({ amount: DEPOSIT_LAMPORTS, destination: player.address, source: owner }), + ]) + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: ['balance', chain] }), + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-withdraw-mutation.ts b/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-withdraw-mutation.ts new file mode 100644 index 0000000..12fcf5c --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/wallet/data-access/use-withdraw-mutation.ts @@ -0,0 +1,37 @@ +import { getTransferSolInstruction } from '@solana-program/system' +import { lamports } from '@solana/kit' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { usePlayerSigner } from '../../player/data-access/use-player-signer' +import { useSendPlayerInstructions } from '../../transactions/data-access/use-send-player-instructions' + +const TRANSACTION_FEE = 5_000n + +// Sends the player wallet's balance back to the owner wallet. The player pays +// the fee out of the amount, so this needs no wallet approval either. +export function useWithdrawMutation() { + const { account, chain, client } = useMobileWallet() + const { data: player } = usePlayerSigner() + const sendPlayerInstructions = useSendPlayerInstructions() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + if (!account || !player) { + throw new Error('Wallet not connected') + } + const { value: balance } = await client.rpc.getBalance(player.address).send() + if (balance <= TRANSACTION_FEE) { + throw new Error('The player wallet has nothing to withdraw') + } + return await sendPlayerInstructions([ + getTransferSolInstruction({ + amount: lamports(balance - TRANSACTION_FEE), + destination: account.address, + source: player, + }), + ]) + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: ['balance', chain] }), + }) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/features/wallet/wallet-feature.tsx b/mobile/sample-expo-kit-farming-idle/src/features/wallet/wallet-feature.tsx new file mode 100644 index 0000000..06b01c4 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/features/wallet/wallet-feature.tsx @@ -0,0 +1,110 @@ +import { FARMING_IDLE_PROGRAM_ADDRESS } from '@project/anchor' +import { useMobileWallet } from '@wallet-ui/react-native-kit' +import { openURL } from 'expo-linking' +import { Alert, Pressable, ScrollView, Text, View } from 'react-native' +import { AppAddressLink } from '../../components/app-address-link' +import { AppButton } from '../../components/app-button' +import { useAirdropMutation } from '../account/data-access/use-airdrop-mutation' +import { useBalanceQuery } from '../account/data-access/use-balance-query' +import { NetworkUiSelect } from '../network/network-ui-select' +import { useNetwork } from '../network/use-network' +import { usePlayerSigner } from '../player/data-access/use-player-signer' +import { useResetPlayerMutation } from '../player/data-access/use-reset-player-mutation' +import { formatError } from '../../utils/format-error' +import { formatSol } from '../../utils/format-sol' +import { useDepositMutation } from './data-access/use-deposit-mutation' +import { useWithdrawMutation } from './data-access/use-withdraw-mutation' + +export function WalletFeature() { + const { account, disconnect } = useMobileWallet() + const { chain, getFaucetUrl } = useNetwork() + const { data: player } = usePlayerSigner() + const ownerBalanceQuery = useBalanceQuery({ address: account?.address }) + const playerBalanceQuery = useBalanceQuery({ address: player?.address }) + const airdropMutation = useAirdropMutation() + const depositMutation = useDepositMutation() + const resetPlayerMutation = useResetPlayerMutation() + const withdrawMutation = useWithdrawMutation() + + const hasNoSol = ownerBalanceQuery.data === 0n + const isLocalnet = chain === 'solana:localnet' + const error = + airdropMutation.error ?? depositMutation.error ?? resetPlayerMutation.error ?? withdrawMutation.error ?? null + + function onResetPlayer() { + Alert.alert( + 'Reset the player wallet?', + 'This deletes the player keypair from this device and generates a new one. The current farm and any SOL left in the player wallet become unreachable — withdraw first.', + [ + { style: 'cancel', text: 'Cancel' }, + { onPress: () => resetPlayerMutation.mutate(), style: 'destructive', text: 'Reset' }, + ], + ) + } + + return ( + + + Main wallet + + {ownerBalanceQuery.data != null ? formatSol(ownerBalanceQuery.data) : '…'} + + {account ? : null} + + Holds your funds and signs through the wallet app: creating the farm, funding the player wallet, and + submitting scores. + + depositMutation.mutate()} + /> + {hasNoSol && isLocalnet ? ( + airdropMutation.mutate()} + /> + ) : null} + {hasNoSol && !isLocalnet ? ( + void openURL(getFaucetUrl(account?.address.toString()))}> + + Your wallet has no SOL to pay for transactions.{'\n'} + Get some at faucet.solana.com + + + ) : null} + + + + Player wallet + + {playerBalanceQuery.data != null ? formatSol(playerBalanceQuery.data) : '…'} + + {player ? : null} + + A burner keypair in this device's keystore. It only holds gas money and signs every harvest and upgrade + without prompts. + + withdrawMutation.mutate()} + /> + + + + + + + void disconnect()} variant="danger" /> + {error ? {formatError(error)} : null} + + + ) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/global.css b/mobile/sample-expo-kit-farming-idle/src/global.css new file mode 100644 index 0000000..ae108db --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/global.css @@ -0,0 +1,2 @@ +@import 'tailwindcss'; +@import 'uniwind'; diff --git a/mobile/sample-expo-kit-farming-idle/src/global.d.ts b/mobile/sample-expo-kit-farming-idle/src/global.d.ts new file mode 100644 index 0000000..5894ae0 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/global.d.ts @@ -0,0 +1 @@ +declare module '*.css' diff --git a/mobile/sample-expo-kit-farming-idle/src/uniwind-types.d.ts b/mobile/sample-expo-kit-farming-idle/src/uniwind-types.d.ts new file mode 100644 index 0000000..cc09941 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/uniwind-types.d.ts @@ -0,0 +1,10 @@ +// NOTE: This file is generated by uniwind and it should not be edited manually. +/// + +declare module 'uniwind' { + export interface UniwindConfig { + themes: readonly ['light', 'dark'] + } +} + +export {} diff --git a/mobile/sample-expo-kit-farming-idle/src/utils/ellipsify.ts b/mobile/sample-expo-kit-farming-idle/src/utils/ellipsify.ts new file mode 100644 index 0000000..b895f70 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/utils/ellipsify.ts @@ -0,0 +1,5 @@ +export function ellipsify(str = '', len = 4, delimiter = '..') { + const strLen = str.length + const limit = len * 2 + delimiter.length + return strLen >= limit ? str.substring(0, len) + delimiter + str.substring(strLen - len, strLen) : str +} diff --git a/mobile/sample-expo-kit-farming-idle/src/utils/format-error.ts b/mobile/sample-expo-kit-farming-idle/src/utils/format-error.ts new file mode 100644 index 0000000..a883c95 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/utils/format-error.ts @@ -0,0 +1,12 @@ +export function formatError(error: unknown) { + if (error instanceof Error) { + return error.message + } + if (error && typeof error === 'object' && 'message' in error) { + return String(error.message) + } + if (typeof error === 'string' && error.trim().length > 0) { + return error + } + return 'Unknown error occurred' +} diff --git a/mobile/sample-expo-kit-farming-idle/src/utils/format-points.ts b/mobile/sample-expo-kit-farming-idle/src/utils/format-points.ts new file mode 100644 index 0000000..f46e3ac --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/utils/format-points.ts @@ -0,0 +1,19 @@ +const units = [ + { suffix: 'q', value: 1_000_000_000_000_000 }, + { suffix: 't', value: 1_000_000_000_000 }, + { suffix: 'b', value: 1_000_000_000 }, + { suffix: 'm', value: 1_000_000 }, + { suffix: 'k', value: 1_000 }, +] + +// Compact display for harvest points: 1234 -> "1.2k". Idle-game scores outgrow +// safe integers eventually, but far beyond what a rounded display can show. +export function formatPoints(points: bigint): string { + const value = Number(points) + for (const unit of units) { + if (value >= unit.value) { + return (value / unit.value).toFixed(1).replace(/\.0$/, '') + unit.suffix + } + } + return value.toFixed(0) +} diff --git a/mobile/sample-expo-kit-farming-idle/src/utils/format-sol.ts b/mobile/sample-expo-kit-farming-idle/src/utils/format-sol.ts new file mode 100644 index 0000000..e4aecd5 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/utils/format-sol.ts @@ -0,0 +1,6 @@ +const LAMPORTS_PER_SOL = 1_000_000_000 + +export function formatSol(lamports: bigint): string { + const sol = Number(lamports) / LAMPORTS_PER_SOL + return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 5 }).format(sol)} SOL` +} diff --git a/mobile/sample-expo-kit-farming-idle/src/utils/wait-for-confirmation.ts b/mobile/sample-expo-kit-farming-idle/src/utils/wait-for-confirmation.ts new file mode 100644 index 0000000..6d59dea --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/src/utils/wait-for-confirmation.ts @@ -0,0 +1,22 @@ +import type { createSolanaRpc, Signature } from '@solana/kit' + +type SolanaRpc = ReturnType + +// Polls until the cluster confirms the transaction, for flows where someone +// else (the wallet, the faucet) submitted it. +export async function waitForConfirmation(rpc: SolanaRpc, transactionSignature: Signature, timeoutMs = 30_000) { + const startedAt = Date.now() + while (Date.now() - startedAt < timeoutMs) { + const { + value: [status], + } = await rpc.getSignatureStatuses([transactionSignature]).send() + if (status?.err) { + throw new Error(`Transaction ${transactionSignature} failed: ${JSON.stringify(status.err)}`) + } + if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') { + return + } + await new Promise((resolve) => setTimeout(resolve, 1_000)) + } + throw new Error(`Timed out waiting for transaction ${transactionSignature} to confirm.`) +} diff --git a/mobile/sample-expo-kit-farming-idle/tsconfig.json b/mobile/sample-expo-kit-farming-idle/tsconfig.json new file mode 100644 index 0000000..1cbc5b0 --- /dev/null +++ b/mobile/sample-expo-kit-farming-idle/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@project/anchor": ["./anchor/src"] + } + }, + "exclude": ["anchor", "node_modules"] +} diff --git a/templates.json b/templates.json index 71552d9..0ad4ef2 100644 --- a/templates.json +++ b/templates.json @@ -174,6 +174,25 @@ "displayName": "Cause Pots sample", "usecase": "Mobile" }, + { + "description": "A sample idle farming game with a burner player wallet, on-chain upgrades, and a global leaderboard, built with Expo, React Native, Solana Kit, Anchor, and Uniwind.", + "id": "gh:solana-mobile/templates/mobile/sample-expo-kit-farming-idle", + "image": "mobile/sample-expo-kit-farming-idle/og-image.png", + "keywords": [ + "anchor", + "expo", + "mobile-wallet-adapter", + "react-native", + "sample", + "solana-kit", + "tailwind", + "uniwind" + ], + "name": "sample-expo-kit-farming-idle", + "path": "mobile/sample-expo-kit-farming-idle", + "displayName": "Farming Idle sample", + "usecase": "Mobile" + }, { "description": "A sample Solana mobile app with Expo, Privy auth, Solana Kit, and Uniwind.", "id": "gh:solana-mobile/templates/mobile/sample-expo-kit-privy",