From 573b57d1bda92d4c7acffe3d59c3fb5fef5665cf Mon Sep 17 00:00:00 2001 From: Elliot Voris Date: Wed, 27 Mar 2024 12:01:37 -0500 Subject: [PATCH] [Soroban Merge] Create a "Smart Contracts" section (#388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move Learn and Under-the-Hood to Guides (#39) * Changes from meeting today (#40) * Expand the create a project section (#41) * added category indexes * Add title of file to example (#42) * Update optimized page (#44) * Fix links (#45) * update first project to create a project * Change "their" to "there" Grammar. * Changed "environments" to "environment's" Grammar. * Facilities spelling error * Remove double "the" * Added apostrophe in "project's" for possession * Fix quick-start example (#53) * A few tweaks to quick start guide to make it work (#54) * fix cli usage (#55) * Update crate links to use crates.io (#56) * Merge the rustup sections (#57) * Fix cli version (#58) * Fix formatting (#59) * Variety of small fixes (#65) * Add warning for hyphenated project names (#67) * Correct few typos in examples and tutorials (#83) * Update sdk and cli versions and output (#87) * Update code samples in quickstart (#88) * empty * Update the tutorial for creating a project (#89) * Update the tutorial for writing a contract (#90) * Update the tutorial for testing (#91) * Put auth example earlier in list of examples (#96) * noop * noop * Update sdk and cli versions and output (#87) * Update code samples in quickstart (#88) * empty * Update the tutorial for creating a project (#89) * Update the tutorial for writing a contract (#90) * Update the tutorial for testing (#91) * Update the hello_world contract (#92) * Small fix to hello contract * Update the increment contract (#93) * Small fix to increment example * Update the custom_types contract (#94) * Add auth example and update auth learn page (#95) * Put auth example earlier in list of examples Co-authored-by: Tyler van der Hoeven Co-authored-by: Siddharth Suresh * Revert "Put auth example earlier in list of examples (#96)" (#97) This reverts commit 1dc54990726ce713b3e760fab62067bf687775e8. * Add auth sdk page (#108) * noop * noop * Update sdk and cli versions and output (#87) * Update code samples in quickstart (#88) * empty * Update the tutorial for creating a project (#89) * Update the tutorial for writing a contract (#90) * Update the tutorial for testing (#91) * Update the hello_world contract (#92) * Small fix to hello contract * Update the increment contract (#93) * Small fix to increment example * Update the custom_types contract (#94) * Add auth example and update auth learn page (#95) * Put auth example earlier in list of examples (#98) (cherry picked from commit fc2fc75d34aad6eecae9d5f8de10f16268edd7c2) * Add events example (#100) * Update the cross_contract contract (#99) * Small tweak to contract call doc * Fix to contract call code sample * Fix to contract call code sample * noop to try and make it deploy properly * Reorder examples so that contract events appears higher in list * Fix headings of cross contract example (#102) * Add soroban-cli read example * Tweak language about developer discord * Some changes to the events example page (#101) * Rename standard-contracts and update token-contract (#103) * Rename Standard Contracts to Built-In Contracts * Update built-in-contracts/token-contract and examples/authorization * Fix title capitalization of built-in contracts page (#104) * Add stub for token example (#105) * Update authorization docs to line up with contract examples (#106) * Small fixes for token contract doc * Make authorization docs line up with the example * Instruct users to checkout v0.0.4 tag for examples (#107) * fix pages and sdk instructions * Add auth sdk page Co-authored-by: Tyler van der Hoeven Co-authored-by: Siddharth Suresh Co-authored-by: Jay Geng Co-authored-by: jonjove <31668823+jonjove@users.noreply.github.com> * Revert "Add auth sdk page (#108)" (#109) This reverts commit ee9e03fbc565bec36f56ac811e65a851d4bf453b. * Add rust-src component install to build optimized tutorial (#117) * ran a linter on the docs * build optimized: fix admonitions (#118) * fix admonitions (#121) * Revert "fix admonitions (#121)" This reverts commit 59853b35d93fd25465c7ee95b9fcce44a36a076d. * Revert "build optimized: fix admonitions (#118)" This reverts commit 42f53d1133c3606cd3ef4f5446b7aafeb7c502b9. * Revert "ran a linter on the docs" This reverts commit c7984f63996fee7fab06fa97d5e31bc482b7b09c. * Rename soroban-cli to soroban * Add deployer example (#138) * Revert "Add deployer example (#138)" (#139) This reverts commit 2f4a60f24350dc2ea23f05f2ffc0c0d827a15987. * Add logging example (#148) * Update examples (#150) * Fix soroban-cli links (#152) * Fix soroban-cli links (#155) * Add `mod test;` to lib.rs and use `first_project` (#160) * Add run in pages (#179) * Tweaks to run on local network page * Move the FAQ to the top level (#182) * Add vscode links (#184) * Update versions of sdks and tools (#185) * Remove out-dated recommendation about testutils feature (#187) * Tweak recommendation * Add details about building with logs to project setup (#188) * Update the links for reporting issues (#189) * Move the optimized build instructions inside the build page to shorten the list of tutorials (#190) * Update soroban-cli version (#192) * Fix broken links (#195) * Fix a typo * Update for built-in token docs (#193) Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> * Fix typo * Fix typos in command examples * Update soroban-cli to v0.1.2 (#196) * Tweaks to pages * Fixes to examples and docs (#199) * Move Build Optimized out of details section (#202) * fix broken link in setup.mdx (#204) Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> * Make it clearer that optimization is unnecessary (#205) * Make it clearer that optimization is unnecessary * less words * Update build-optimized.mdx Co-authored-by: Tyler van der Hoeven * Remove recommendation to use the rlib crate type (#212) * Remove recommendation to use the rlib crate type (#212) * Fix typo (#213) * Update Examples, SDK and CLI v0.2.1 * Pin docker image * Update Examples, SDK and CLI v0.2.1, and Quickstart Image (#221) * Fix typos (#222) * add quotes around curl argument (#224) needed for zsh and maybe some other shells * quote curl argument for zsh & maybe other shells (#226) * Update version of SDK (#236) * Remove BigInt and replace with i128 (#237) * Update soroban-cli to v0.3.1 (#239) * Next Release Dev Branch (#231) * add instructions on how to Sign Transactions with Freighter (#200) * Reorder the nav bar (#244) * Update Examples, SDK and CLI v0.2.1 * Pin docker image * Add release notes (#234) * Rename and move releases page * reorder EVERYTHING * reorder Learn between Examples and SDKs * update wallet position Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> * Update soroban-{sdk, auth, example, cli} versions (#245) * added [package] entries (#248) allowing full 'Cargo.toml' code to be copied * Updating the futurenet and localnet tutorials with latest quickstart (#249) The previous image hash was used in these two tutorials, but is no longer working (I assume because there are no other nodes running that version). Here, both tutorials are updated to the latest preview version of the quickstart image. Signed-off-by: Elliot Voris Signed-off-by: Elliot Voris * Update Soroban transaction document. (#251) * Update outdated token terminology * Update various sdk, auth, examples versions from 0.3.2 to 0.4.2 * fix doc * fix doc (#268) * Support arm64 images in docs for quickstart (#274) * update all admonitions such that prettier doesn't yell * Lots of formatting * Reorg (#290) * Change SDK section to Reference section * Adding JS SDK to list of SDKs * Adding Overcat's Python SDK to list of SDKs * Moving Networks -> Futurenet to Reference section * Moving Releases to Reference section * Delete Networks from main nav Networks now lives under Reference section * Deleting as Releases now live in Reference section * Changing sidebar position Temporary, this will eventually move to How-To Guides * Update byo.mdx * Edited Quick Start to 1. Hello World Includes Tutorials -> Create a Project / Write a Contract / Test / Build / Run on Sandbox / Optimizing Builds * Move Storing Data from Examples to Getting Started * Move Deploy to Local Network to Getting Started Moved this section from Tutorials to Getting Started * Move Freighter wallet tutorial to Getting Started * Update and rename 6.-connect-Freighter-wallet.mdx to 4. Connect Freighter Wallet * Move Deploy to Futurenet to Getting Started * Rename 4. Connect Freighter Wallet to 4. Connect Freighter Wallet.mdx * Moved to Getting Started * Moved to Getting Started Removing this section * Moved to Getting Started Removing this as it has been moved to Getting Started * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * Moved to Getting Started Removing due to this section being moved * Update Examples to How-To Guides * Move BYO SDK to How-To Guides * Moved to How-To Guides * Moved invoking contracts with transactions to How-To Guides * Removing because it's moved to How-To Guides * Moved Token Interface to Token How-To Guide * Remove Token Interface Token Interface has been moved to How-To Guides -> Token * Move Stellar Asset Contract to How-To Guides * Delete docs/built-in-contracts directory Moved to How-To Guides * Move Stellar FAQs to Learn * Delete faq.mdx Moved to Learn * Update authorization.mdx Alphabetize sidebar * Update contract-lifecycle.mdx Alphabetizing sidebar * Update custom-types.mdx Alphabetizing sidebar * Update rust-dialect.mdx Alphabetizing sidebar * Update debugging.mdx Alphabetizing sidebar * Update environment-concepts.mdx Alphabetizing sidebar * Update errors.mdx Alphabetizing sidebar * Update events.mdx Alphabetizing sidebar * Update gas-and-metering.mdx Alphabetizing sidebar * Update interacting-with-contracts.mdx Alphabetizing sidebar * Update persisting-data.mdx Alphabetizing sidebar * Update high-level-overview.mdx Update spelling * formatting * formatting * formatting * formatting * Update faq.mdx * fixed broken links * fix broken links * fix broken links * moar link fixes * fixed some broken links * added sorobanathon homepage * add google tag manager * Content edits made * back to original * Content Edit * button location * Content & Eligibility guidelines added * Update sorobanathon.js * update all admonitions such that prettier doesn't yell * Lots of formatting * --prose-wrap preserve * Update releases.mdx * update prettier * fixed the callouts * formatting * fix module import * Update sorobanathon.js * Fixed Reorg PR Fixed links, moved sections, deleted double sections * Update index.js * Updating Sorobanathon Info * Fixed links Many links. * Formatting * Change SDK section to Reference section * Adding JS SDK to list of SDKs * Adding Overcat's Python SDK to list of SDKs * Moving Networks -> Futurenet to Reference section * Moving Releases to Reference section * Delete Networks from main nav Networks now lives under Reference section * Deleting as Releases now live in Reference section * Changing sidebar position Temporary, this will eventually move to How-To Guides * Update byo.mdx * formatting * fix broken links * Edited Quick Start to 1. Hello World Includes Tutorials -> Create a Project / Write a Contract / Test / Build / Run on Sandbox / Optimizing Builds * Move Storing Data from Examples to Getting Started * Move Deploy to Local Network to Getting Started Moved this section from Tutorials to Getting Started * Move Freighter wallet tutorial to Getting Started * Update and rename 6.-connect-Freighter-wallet.mdx to 4. Connect Freighter Wallet * Move Deploy to Futurenet to Getting Started * Rename 4. Connect Freighter Wallet to 4. Connect Freighter Wallet.mdx * Moved to Getting Started Removing this as it has been moved to Getting Started * Moved to Getting Started -> Hello World * Moved to Getting Started -> Hello World * formatting * fix broken links * moar link fixes * Update Examples to How-To Guides * Move BYO SDK to How-To Guides * Moved invoking contracts with transactions to How-To Guides * Moved Token Interface to Token How-To Guide * Remove Token Interface Token Interface has been moved to How-To Guides -> Token * Move Stellar Asset Contract to How-To Guides * Delete docs/built-in-contracts directory Moved to How-To Guides * formatting * fixed broken links * Move Stellar FAQs to Learn * Update authorization.mdx Alphabetize sidebar * Update contract-lifecycle.mdx Alphabetizing sidebar * Update custom-types.mdx Alphabetizing sidebar * Update rust-dialect.mdx Alphabetizing sidebar * Update debugging.mdx Alphabetizing sidebar * Update environment-concepts.mdx Alphabetizing sidebar * Update errors.mdx Alphabetizing sidebar * Update events.mdx Alphabetizing sidebar * Update gas-and-metering.mdx Alphabetizing sidebar * Update interacting-with-contracts.mdx Alphabetizing sidebar * Update persisting-data.mdx Alphabetizing sidebar * Update high-level-overview.mdx Update spelling * formatting * Update faq.mdx * fixed some broken links * Update releases.mdx * update prettier * fixed the callouts * formatting * fix module import * Fixed Reorg PR Fixed links, moved sections, deleted double sections * Fixed links Many links. * Formatting * Merge cleanup * More cleanup * Formatting * some small design tweaks * copy tweaks --------- Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> Co-authored-by: Anuxhya <36203801+achallagundla@users.noreply.github.com> * Update setup.mdx (#279) I had one of my devs try the Windows install and when you look at the existing docs, it looks like installing the wasm target is just for macOS/Linux/Unix-like systems. Moving it below the Windows section and making it it's own section might help. * Including a hyphen in `how-to-guides` directory (#298) * Including a hyphen in `how-to-guides` directory This removes any spaces from generated URLs for the pages contained therein, and is a bit more consistent with the existing directory structure. Signed-off-by: Elliot Voris * Changing links `how-to guides` -> `how-to-guides` Signed-off-by: Elliot Voris --------- Signed-off-by: Elliot Voris * Update hello-world.mdx (#304) * update setup instructions. * Merge dev docs to main (#316) * proposed auth-next support for simulateTransaction method * Add note about multiple results * auth is an array * Updated example docs for Auth Next. (#306) * Updated example docs for Auth Next. * Update token documentation for Auth Next (#307) * Update token doc to use auth next. * Add AUTH_REQUIRED section * Mention set_auth and link to classic docs * feat: update for upcoming CLI version * feat: CLI reference page * Update optimize contracts section (#310) * Update hello-world.mdx * Update hello-world.mdx * don't need to manually install wasm-opt via binaryen * remove the -Z flags * More Auth Next docs updates (#311) * Remove rust-auth.mdx * Update authorization docs and `Address` type info. * Added auth info to transactions doc. * Added a document about migrating to auth next. * Add some high-level docs for Auth Next preflight (#312) * Add some preflight information for Auth Next * Update docs/reference/command-line.mdx * Update docs/reference/releases.mdx * Update docs/reference/releases.mdx * Update simulateTransaction.mdx * Update examples to point to 0.6.0 * Update releases page to current state * Add change log for cli and rpc * Added a note on the breaking auth changes to the release notes. (#315) --------- Co-authored-by: Paul Bellamy Co-authored-by: Siddharth Suresh Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Willem Wyndham Co-authored-by: Tyler van der Hoeven Co-authored-by: Paul Bellamy * Use soroban-sdk the lasted version `0.6.0` in hello world example (#326) * Combine Hello World sections (#345) * Clean up page descriptions in category indexes (#348) * Add descriptions to docs that appear in generated category indexes. * Add category indexes and descriptions for SDKs and interfaces * Clean up descriptions of API documents. * compile -> create * remove apostrophe * added oxford comma * capitalization * added oxford comma --------- Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> * Update symbol functions and some related docs. * bump examples to 0.7.0 * Update setup with 0.7.0 (#364) * Pin docker images to release version (#365) * Update the cli docs for preview 8 release (#371) * Update deploy-to-futurenet to use public rpc (#372) * Create Run an RPC Adding a new page for the RPC * Rename Run an RPC to run-rpc * Rename run-rpc to run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * Update run-rpc.mdx * unbork docs (#377) * a Removed symbol from soroban-sdk imports (#380) * Fix rpc-url for Futurenet deployment (#383) * a Removed symbol from soroban-sdk imports * Fix the rpc-url for Futurenet deployment * Anup doc updates (#378) * Updates per Anup's instructions * 10 -> 32 characters * 10 -> 32 character limit * 10 -> 32 characters * A comes before B * Update docs/how-to-guides/cross-contract-call.mdx Co-authored-by: Elliot Voris * changed wording --------- Co-authored-by: Elliot Voris * Update run-rpc.mdx (#386) Added back section RPC providers list (w/ fixed typo for URL reference) * add Freighter instructions (#390) * edition version (#393) * fixes malformed hello world link (#412) * Release preview 9 (#447) * update target directories (#405) * Add some basic documentation on fees & limits. (#417) * Add code and contract metadata (#418) * Update alloc crate for SDK v0.8.4 (#420) * Update atomic swap example for SDK v0.8.4 (#421) * Update atomic swap example for SDK v0.8.4 (#422) * Update auth example for SDK v0.8.4 (#423) * Add note about contract meta to build-your-own-sdk page (#424) * Add note about contract meta to build-your-own-sdk page * add link * Update cross-contract example for SDK v0.8.4 (#425) * Update custom account example for SDK v0.8.4 (#426) * Update custom types example for SDK v0.8.4 (#427) * Update deployer example for SDK v0.8.4 (#428) * Update errors example for SDK v0.8.4 (#429) * Update events example for SDK v0.8.4 (#430) * Update liqpool example for SDK v0.8.4 (#431) * Update logging example for SDK v0.8.4 (#432) * Update single-offer-sale example for SDK v0.8.4 (#433) * Update stellar asset contract page for SDK v0.8.4 (#434) * Update timelock example for SDK v0.8.4 (#435) * Update token example for SDK v0.8.4 (#436) * Update contractmeta! page for SDK v0.8.4 (#437) * Update getting started setup page for SDK v0.8.4 (#438) * update soroban-cli and example versions (#441) * update soroban-cli and example versions * update token contract interface link * update-release-page (#446) * update-release-page * add changelog * Add explaination to metering (#443) --------- Co-authored-by: Dmytro Kozhevin Co-authored-by: Siddharth Suresh Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Co-authored-by: Jay Geng * update quickstart hash (#456) * updates quickstart versions (#459) * Evm to Soroban (#409) * initial commit * add code breakdown * add mdx * remove mdx * update Rust SC * add advanced concepts * add vault contract and tests * Update solidity-and-rust-advanced-concepts.mdx * remove un tabbed code * update vault.rs; add shell cmds * update yield amount * add vault deployment and interaction * add wdraw message * fix script sequence * add EVM comparison * update error handling * 1st round review * Update script descroption * add solidity to approved languages * fixed a broken link * move file folder; add token inheritance * update links * fixed broken links * update Soroban Description * nit: spelling * Fix bytes and arrays * spelling errors * spelling errors * no capitalization on build * grammar spelling errors * spelling and grammar errors * fixes; post PR review * macros description * Correction: Data Types * update Symbol Definition * update traits definition: fix Solidity Contract * Merge main into evm-to-soroban (#414) * update code block (#400) * additional copy changes to site (#401) * copy edits from Dom (#403) * update broken link (#404) * update token interface (#406) * update token intereface Interface was referenced from : https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/token.rs * update fn description * nit: update fn description * nit: format * nit: bracket format * fix event topics * update events * revert per PR to fix event name https://github.com/stellar/rs-soroban-env/pull/778/files * Improve Failure Condition Handling in Token Interface (#394) * Add Error Handling Section * update authorization info * add more context to authorization * nit: call not specific to callers address * Add event docs (#375) * Add event docs * Remove note * Add link to events example * fixes malformed hello world link (#412) * Remove derive Default (#413) --------- Co-authored-by: Tyler van der Hoeven Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> Co-authored-by: Siddharth Suresh Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> * fix spelling and add string example * move evm-to-soroban * resolve conflict * add auth section. Add comparison for addresses * embolden main point about replay prevention * nit update section * nit wording * improve Address and Auth Callouts * update contract deposit fn. Update ID to addr conversion * nit: var placeholders * example vault update * update vault example to 8.1 * update to align w main * updates Env:: to env. ; nits * add section on composability * nit: update soroban-sdk version * update example to use sdk 8.4 * remove admin from mint * nit: grammar; update tx messages --------- Co-authored-by: Tyler van der Hoeven Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> Co-authored-by: Siddharth Suresh Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> * copy edits (#467) * copy edits * Update connect-freighter-wallet.mdx * Update run-rpc.mdx * docs reorg 2.0 (#474) * docs reorg 2.0 * fixed links * Move invoking contracts with Stellar transactions (#488) * Update hello-world.mdx (#490) * Release Preview 10 (#480) * Change command recommended for building .wasm files for deployment (#476) * Fix logging example (#481) * Update docs with auth XDR changes. (#483) * Update docs with auth XDR changes. * WASM->Wasm rename. Wasm is the official abbreviation of WebAssembly. * Typo fixes (#484) * Add fuzzing docs. (#486) * State expiration operations and token updates (#485) * Add docs for state expiration operations * Updates * update token * Update custom account * More token updates * Update atomic swap * More detailed state expiration docs (#492) * More fixes (#489) * More fixes * Add approve warning * Update releases page * Fix link * Update example links from 0.8.4 to 0.9.2 * fix link * Fixed instance bump bug in docs (#493) --------- Co-authored-by: Dmytro Kozhevin Co-authored-by: Brian Anderson Co-authored-by: Siddharth Suresh Co-authored-by: Garand Tyson Co-authored-by: Julian Martinez * Update setup.mdx (#495) * fix versions (#497) * add #[contract] and symbol_short (#498) * #[contract]; symbol_short description * Update hello-world.mdx * add missing symbol_short and #[contract] references * fix: move State Expiration to Concepts (#505) This is not part of the core onboarding tutorial. It probably belongs in Fundamentals and Concepts instead. For background, these docs are attempting to use [Divio's documentation system](https://documentation.divio.com/), with the "Getting Started" section as _The_ Tutorial. The "Fundamentals and Concepts" section is what Divio describes as "Explanation" docs. * fix: move RPC doc out of Getting Started (#504) This seems like part of the RPC reference material, not an essential thing that someone needs to know as part of the "official onboarding tutorial," which is what Getting Started is supposed to be. * Format (#508) * package updates * cleaned up formatting * add initial gh action (#507) * add initial gh action * fix links --------- Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> * Clean up Getting Started tutorial; add "Deploy to Futurenet" (#516) * feat: move Freighter and Running RPC to Reference We want to keep the stuff in Getting Started hyper-focused on a quick end-to-end tour of Soroban. Now that there's a public RPC endpoint, it is much more straightforward to have people deploy contract right to it, rather than running their own nodes. Running your own node is a more advanced concept that dabblers need not muck with. So this moves both the "Deploy to a Local Network" and "Deploy to Futurenet" pages to the RPC page, in the Reference section. This required some reorganization & rework of the concepts to make it fit the new context. I also updated some instructions, such as teaching people to use `soroban config network` and `soroban config identity`, which were not included in the old "Deploy to…" pages. This commit also moves the "Connect Freighter Wallet" to Reference, since it's a bit in-the-weeds, which is a good feature for a Reference document but not for an end-to-end Tutorial. Coming Soon™: a new "Deploy Contracts" step in the Getting Started section, which uses the public RPC Futurenet endpoint. This will be followed by a "Create a Dapp" step, which will use the new JS Lib generation built into the CLI, and go over relevant Freighter details. * fix: clean up Hello World walk-through - explicitly instruct people to call it `hello-soroban` - move some single-use footnote-style links inline, to make them easier to see/understand/update later - remove spacing around tips for easier visual parsing - remove "Hello World Example" section, since this IS the Hello World example - remove unused & unneeded footnote-style link definitions - clean up example code so it doesn't give warnings on copy/paste * feat(getting-started): clean up Storing Data I'm working toward having people create a frontend Dapp with multiple contracts in a `contracts` folder, so I added a whole part at the beginning walking people through setting up a Cargo Workspace. This page also had more information than necessary about State Expiration; I moved this to the State Expiration doc, which had been practically blank. I reorganized this information somewhat; most notably, the "Best Practices" section had been practically empty. I moved some miscellaneous information to it; it should maybe be renamed "more information about state expiration" rather than "best practices", since it DOESN'T currently contain best practices, and I am not qualified to say what Best Practices might actually be. (Note that in the old version, the "Best Practices" section was blank! It was a misplaced heading, with nothing relevant below.) * fix: add missing "be" Co-authored-by: Willem Wyndham * feat: add 3. Deploy to Futurenet This contains the relevant details people will probably need to know when first getting started with Soroban. * fix: run prettier * feat: commit, summarize, save IDs to files Instructing people to commit to git will help them think about their work more clearly, and will also help them avoid mistakes and get better help when they need it. I also added summaries to each of these tutorial pages, to review what was learned and set up the next lesson. Finally, I decided to have them save contract IDs to files in their `.soroban` folder, rather than in more-ephemeral environment variables. This will make it easier to have them use `soroban contract bindings` in the "Build a Dapp" step, since they'll be able to reference these files in their package.json scripts. * fix: run lint --------- Co-authored-by: Willem Wyndham * Clean up Getting Started tutorial; add "Deploy to Futurenet" (#516) * feat: move Freighter and Running RPC to Reference We want to keep the stuff in Getting Started hyper-focused on a quick end-to-end tour of Soroban. Now that there's a public RPC endpoint, it is much more straightforward to have people deploy contract right to it, rather than running their own nodes. Running your own node is a more advanced concept that dabblers need not muck with. So this moves both the "Deploy to a Local Network" and "Deploy to Futurenet" pages to the RPC page, in the Reference section. This required some reorganization & rework of the concepts to make it fit the new context. I also updated some instructions, such as teaching people to use `soroban config network` and `soroban config identity`, which were not included in the old "Deploy to…" pages. This commit also moves the "Connect Freighter Wallet" to Reference, since it's a bit in-the-weeds, which is a good feature for a Reference document but not for an end-to-end Tutorial. Coming Soon™: a new "Deploy Contracts" step in the Getting Started section, which uses the public RPC Futurenet endpoint. This will be followed by a "Create a Dapp" step, which will use the new JS Lib generation built into the CLI, and go over relevant Freighter details. * fix: clean up Hello World walk-through - explicitly instruct people to call it `hello-soroban` - move some single-use footnote-style links inline, to make them easier to see/understand/update later - remove spacing around tips for easier visual parsing - remove "Hello World Example" section, since this IS the Hello World example - remove unused & unneeded footnote-style link definitions - clean up example code so it doesn't give warnings on copy/paste * feat(getting-started): clean up Storing Data I'm working toward having people create a frontend Dapp with multiple contracts in a `contracts` folder, so I added a whole part at the beginning walking people through setting up a Cargo Workspace. This page also had more information than necessary about State Expiration; I moved this to the State Expiration doc, which had been practically blank. I reorganized this information somewhat; most notably, the "Best Practices" section had been practically empty. I moved some miscellaneous information to it; it should maybe be renamed "more information about state expiration" rather than "best practices", since it DOESN'T currently contain best practices, and I am not qualified to say what Best Practices might actually be. (Note that in the old version, the "Best Practices" section was blank! It was a misplaced heading, with nothing relevant below.) * fix: add missing "be" Co-authored-by: Willem Wyndham * feat: add 3. Deploy to Futurenet This contains the relevant details people will probably need to know when first getting started with Soroban. * fix: run prettier * feat: commit, summarize, save IDs to files Instructing people to commit to git will help them think about their work more clearly, and will also help them avoid mistakes and get better help when they need it. I also added summaries to each of these tutorial pages, to review what was learned and set up the next lesson. Finally, I decided to have them save contract IDs to files in their `.soroban` folder, rather than in more-ephemeral environment variables. This will make it easier to have them use `soroban contract bindings` in the "Build a Dapp" step, since they'll be able to reference these files in their package.json scripts. * fix: run lint --------- Co-authored-by: Willem Wyndham * Updating `soroban-cli` version and adding bash completion note (#527) * Updating `soroban-cli` version and adding bash completion note * Adding example commands to enable autocompletion * update SEO titles and descriptions (#534) * update SEO titles in Getting Started * Update setup.mdx * SEO titles and descriptions in Getting Started * formatting * update titles and descriptions for basic tutorials * update SEO in advanced tutorials * updated header for SEO * fix * updating... again! * feat: add "4. Build an App" step to "Getting Started" (#528) Uses smartdeploy CLI branch for now, for futurenet compatibility Co-authored-by: Julian Martinez Co-authored-by: tomerweller Co-authored-by: Bri Wylde <92327786+briwylde08@users.noreply.github.com> Co-authored-by: Tyler van der Hoeven * Update soroban versions to 20.0.0-rc2 (#574) * update soroban versions * update docs to use rc2 * More updates to old CLI versions (#575) * docs: fixing a link to create a new issue for Soroban CLI * docs: Updating the `soroban` usage for `v20.0.0-rc2` * docs: updating link to auto-generated cli docs * docs: Updating tutorials to point to `v20` tag of the examples repo * docs: Updating "migrating from evm" article to use `v20.0.0-rc2` examples * docs: changing to `v20.0.0-rc2` examples repo in getting started section * Update Futurenet to Testnet Across Documentation (#577) * Futurenet -> Testnet * fix broken links * update-rpc-page * Update testnet.mdx * Pass 1 * format * update table in testnet.mdx * update state expiration example script to use testnet * replacing futurenet with testnet in contract deployment example * revert SDC contnet * include testnet network passphrase on releases page * update freighter to include testnet alongside futurenet * revert SDC to Use Futurenet for the time being * style: fix up markdown syntax and style in `rpc.mdx` Also adds some small linguistic and grammer changes, as well as more updates from Futurenet to Testnet * docs: include Testnet when describing what RPC servers SDF maintains * style: changing a single ellipsis character with three periods * docs: include gh repo links to the software that runs in quickstart * add the Testnet network to the example `initialize.sh` script * change contract-invoking transaction example code to use Testnet * change to valid futurenet URLs in dapp challenges * Apply suggestions from code review minor tweaks that slipped through the cracks --------- Co-authored-by: Elliot Voris * Update Futurenet to Testnet Across Documentation (#577) * Futurenet -> Testnet * fix broken links * update-rpc-page * Update testnet.mdx * Pass 1 * format * update table in testnet.mdx * update state expiration example script to use testnet * replacing futurenet with testnet in contract deployment example * revert SDC contnet * include testnet network passphrase on releases page * update freighter to include testnet alongside futurenet * revert SDC to Use Futurenet for the time being * style: fix up markdown syntax and style in `rpc.mdx` Also adds some small linguistic and grammer changes, as well as more updates from Futurenet to Testnet * docs: include Testnet when describing what RPC servers SDF maintains * style: changing a single ellipsis character with three periods * docs: include gh repo links to the software that runs in quickstart * add the Testnet network to the example `initialize.sh` script * change contract-invoking transaction example code to use Testnet * change to valid futurenet URLs in dapp challenges * Apply suggestions from code review minor tweaks that slipped through the cracks --------- Co-authored-by: Elliot Voris * update helloworld to use `project_name` in line with Rust compilation behavior (#588) Signed-off-by: sonichen <1606673007@qq.com> * update release versions and add new Freighter setup directions (#589) * update release versions and add new Freighter setup directions * reformat Freighter docs * re-adding swap dapp * adds docs for Freighters signAuthEntry API (#592) * adds docs for Freighters signAuthEntry API * format new docs * Update docs/reference/freighter.mdx Co-authored-by: Elliot Voris --------- Co-authored-by: Elliot Voris * docs: update sdk version to 20.0.0-rc2 (#601) * Create rpc-list.mdx (#606) * Create rpc-list.mdx Adding draft directory page * Update rpc-list.mdx updated * Update rpc-list.mdx Updated section * Update rpc-list.mdx Removed comments * fix markdown formatting in rpc provider table * add front-matter to rpc-list document, and small wording nits * Re-titling RPC -> RPC Usage * Adjust some wording surrounding ecosystem RPC providers * add description about what services SDF provides * style: cleanup some markdown formatting and remove a redundant line --------- Co-authored-by: Elliot Voris * docs: explain the diagnostic events that are emitted in sandbox (#593) * docs: explain the diagnostic events that are emitted in sandbox Perhaps this approach is too verbose? I think having the explanation of what is being seen right there in the tutorial is useful at this early step in the "getting started" section. A new developer is more likely to understand what they see, and remember what it is in the future if we don't try to interrupt their flow at this point by sending them to another page for some (possibly irrelevant) info. I may be incorrect there, and I'm happy to hear opinions from others. Refs: #521 * docs: add a space between two words * editorial in Update hello-world.mdx --------- Co-authored-by: Bri Wylde <92327786+briwylde08@users.noreply.github.com> * Update getting-started for 20.0.0-rc.4.1 cli (#636) * Updates to setup.mdx * Add CLI for testnet configuration to Setup * Updates to hello-world.mdx Reorder some code snippets to be after the description * Small edits in storing-data.mdx * Add high_expiration_watermark argument to bump in incrementor * Move deploy-to-testnet after hello-world to allow the user to interact with their contract on testnet * Add Optimizing Builds to hello-world * Update storing-data to remove sandbox interation * Add a deploy-incrementor-to-testnet step * Update position of Create an App * Apply suggestions from code review Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> * Update create-an-app.mdx * Fix typo * Add a mv command for .soroban dir when reorganizing to a multi-contract project * Apply mdx prettier updates * Update astro port in create-an-app.mdx * Apply suggestions from code review Co-authored-by: Elliot Voris * Add .mdx to end of internal markdown links for docusaurus magic * Make sure there are new lines before and after ::: tags * Some additional edits/improvements * Mention that Freighter is available as a firefox add-on * Update the deploy-incrementor url * Apply prettier updates --------- Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Elliot Voris * docs: move data providers page out of sdks directory (#645) * Update getting started tutorial to use an Astro template (#642) * Update create-an-app.mdx to use an Astro template * Apply prettier formatting * Add some additional gotchas to the troubleshooting section * Reworks hello-world.mdx using the Astro template * Rework deploy-to-testnet.mdx to interact with the contarct via the frontend * Update storing-data.mdx * Update deploy-incrementor.mdx * Change create-an-app.mdx to wrapping-up.mdx * Apply prettier updates * Apply suggestions from code review Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> * Address PR feedback - move "???" bummer to "taking it further" - add command expansion note to deploy-to-testnet - remove mention of incrementor in deploy-to-testnet * Small cleanup * Address PR feedback * Remove release-with-logs section * Update code snippets to match updated tutorial code * Apply suggestions from code review Co-authored-by: Elliot Voris * Address PR feedback * Check in prettier updates * update getting started links on the landing page * redirect the old create an app page to the hello world page --------- Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Elliot Voris Co-authored-by: Elliot Voris * build: a couple small fixes to get the build to work (#657) * build: a couple small fixes to get the build to work * style: changing the way a couple codefences work * Revert "Update getting started tutorial to use an Astro template (#642)" (#658) This reverts commit fb7ecc0db4f90fdb9ab5586f1bd0565c1ca994a2. * Remove step adding target dir to gitignore (#660) * Update soroban-sdk to v20.0.0-rc2.2 (#667) * Remove git init from hello world steps (#666) * Remove some outdated cautions from the docs, now that Soroban should be stable. (#672) * State archival refactor (#678) * State archival refactor * Small fixes * Remove cache `gitignore` from hello-world.mdx (#674) * Remove cache `gitignore` from hello-world.mdx Current `soroban` version doesn't cache files in the project dir. * Retry helping with the confusion * Update docs/getting-started/hello-world.mdx Co-authored-by: Elliot Voris --------- Co-authored-by: Elliot Voris * Add v20.0.0 (#668) * add P12 Release * formatting * Update releases.mdx * formatting * update horizon,stellar-base,cli * add stellar sdk + base, soroban-client, Quickstart, changelog * remove http from quickstart endpoint * v20.0.0-rc2 -> v20.0.0 * Preview 12 -> Stable v20.0.0 * update CLI, RPC, Core Versions * update stellar cor version * Content updates to go along with the preview 12 version updates (#682) * update the token interface to reflect the Rust SDK * feat: get Getting Started ready for v20.0.x I don't know what the exact version of the CLI will be out by next Monday. In the source here, I've guessed that maybe it will be `20.0.3`. * update `openrpc.json` file to reflect new updates * Update static/openrpc.json Co-authored-by: Alfonso Acosta --------- Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Molly Karcher Co-authored-by: Alfonso Acosta * Update token-interface.mdx * nit:formatting --------- Co-authored-by: Elliot Voris Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Molly Karcher Co-authored-by: Alfonso Acosta * remove rc2 references (#685) * Update soroban-cli version in Setup, and other pages. #693 (#694) * Documentation Mismatch for Logging after SorobanSDK Update #615 * soroban-docs: Update soroban-cli version to 20.1.0 and change auto generated links (#693) * Update JavaScript SDK references to use the `@stellar/stellar-sdk` package (#687) * Upgrade js sdk dependencies to latest * Track down references to soroban-client and move them * Ran linter * Getting started: update CLI version (#697) * Add SDK examples alongside XDR structure explanations (#699) * Pre mainnet restructure (#644) * docs: rename "fundamentals and concepts" to "soroban internals" * docs: remove old "under the hood" section * docs: add tags to the various tutorials * docs: move tutorials into one main directory * fix broken links to old pages * docs: renaming soroban internals in category file * docs: remove old "command line reference" category * docs: add tokens directory, rearrange sidebars * docs: rearrange migration from evm guide * docs: remove unused reference/interfaces category * docs: move "reference" section to "resources" * docs: move "releases" page up a level * docs: move FAQ page up one level * docs: move dev tools into resources directory * docs: move testnet.mdx to networks.mdx * docs: rearrange items in resources directory * docs: collapse various SDK pages into two pages * docs: move data-providers up one level * docs: remove some empty categories, move the tutorial template * docs: add a new getting-started page, reorganize that section * docs: change sidebar position integers in soroban-internals * docs: shuffling contract interaction around in soroban-internals * style: fixing a couple small markdown nits in dapps directory * docs: fix a broken link * feat: start to the "guides" page(s) * style(lint): fixing a small linting error * remove guides placeholder * fixing broken links * work on guides listing. might revert this * formatting mdx * docs: fixing a broken link to tutorials * style: crack at making the tutorials filterable and hidden in the sidebar * customizing some components for the `/guides` pages and layouts * rename index page for guides to README * remove commented configuration option * remove some console logging in components * change name of index page in guides sidebar * remove commented sidebar generation code * remove comments and add description to sidebar generator * rename sidebar generator file * more work on how the 'guides' pages might look. * docs: adjusting sidebar positions of new getting started pages * fix some broken links in the getting-started section * prefer "README.mdx" files where possible * some more guides placeholder stubs * docs(guides): More placeholder stubs for guides and categories * feat: don't display "guides in category" page on `/guides` * build: check/fix MDX formatting in more directories than just docs * style: add a larger margin before more category guides * style: more selectively increase that top margin * docs(guides): a quick stab at a "publishing" events guide * docs(guides): first stab at the "publish events" guide * docs: updating tutorial descriptions * fix a few broken links * docs: add note about a tuple with one element * style: couple link changes and reformats * move fuzzing tutorial back * change to README file in contract interactions category * docs(guides): give a better title for the wasm metadata guide * fixing some broken markdown links * fix (another!) broken link * move the guides back into the main layout of the docs directory * add placeholder for testnet reset automation stuff * some initial content for some guides * markdown formatting and fixing broken links * moving "resources" back to "reference" to fit the definition better More like "technical reference" rather than a "reference encyclopedia" * change some styles of the tutorial list * removing most category pages * some more first-drafts of guides * fixing some category links * include a period at the end of each tutorial description * simplify tutorial search box placeholder text * better description for the tutorials page * making the pre-commit script executable * flesh out some of the state archival guides * first effort for some rpc ledger key guides * fix some broken links * add a guide on ingesting events into a db * first effort at storage type guides * Some more stubs, marking drafts, and a couple additions * make not a draft to fix a broken link * guides category descriptions in README.mdx files * marking incomplete chain migration docs as drafts * change some tutorial difficulty levels * fine-tune the tutorials component styling a bit * improving some guides organization * fix linting errors and broken links * include wrap instructions for native lumens * fix broken link in contract metadata guide * final changes to existing guides * moving developer tools into the main sidebar * create real redirects instead of just notes * fix broken links in a dapps challenge page * fix a redirect syntax error * fix a tutorials redirect * Pre mainnet restructure (#644) * docs: rename "fundamentals and concepts" to "soroban internals" * docs: remove old "under the hood" section * docs: add tags to the various tutorials * docs: move tutorials into one main directory * fix broken links to old pages * docs: renaming soroban internals in category file * docs: remove old "command line reference" category * docs: add tokens directory, rearrange sidebars * docs: rearrange migration from evm guide * docs: remove unused reference/interfaces category * docs: move "reference" section to "resources" * docs: move "releases" page up a level * docs: move FAQ page up one level * docs: move dev tools into resources directory * docs: move testnet.mdx to networks.mdx * docs: rearrange items in resources directory * docs: collapse various SDK pages into two pages * docs: move data-providers up one level * docs: remove some empty categories, move the tutorial template * docs: add a new getting-started page, reorganize that section * docs: change sidebar position integers in soroban-internals * docs: shuffling contract interaction around in soroban-internals * style: fixing a couple small markdown nits in dapps directory * docs: fix a broken link * feat: start to the "guides" page(s) * style(lint): fixing a small linting error * remove guides placeholder * fixing broken links * work on guides listing. might revert this * formatting mdx * docs: fixing a broken link to tutorials * style: crack at making the tutorials filterable and hidden in the sidebar * customizing some components for the `/guides` pages and layouts * rename index page for guides to README * remove commented configuration option * remove some console logging in components * change name of index page in guides sidebar * remove commented sidebar generation code * remove comments and add description to sidebar generator * rename sidebar generator file * more work on how the 'guides' pages might look. * docs: adjusting sidebar positions of new getting started pages * fix some broken links in the getting-started section * prefer "README.mdx" files where possible * some more guides placeholder stubs * docs(guides): More placeholder stubs for guides and categories * feat: don't display "guides in category" page on `/guides` * build: check/fix MDX formatting in more directories than just docs * style: add a larger margin before more category guides * style: more selectively increase that top margin * docs(guides): a quick stab at a "publishing" events guide * docs(guides): first stab at the "publish events" guide * docs: updating tutorial descriptions * fix a few broken links * docs: add note about a tuple with one element * style: couple link changes and reformats * move fuzzing tutorial back * change to README file in contract interactions category * docs(guides): give a better title for the wasm metadata guide * fixing some broken markdown links * fix (another!) broken link * move the guides back into the main layout of the docs directory * add placeholder for testnet reset automation stuff * some initial content for some guides * markdown formatting and fixing broken links * moving "resources" back to "reference" to fit the definition better More like "technical reference" rather than a "reference encyclopedia" * change some styles of the tutorial list * removing most category pages * some more first-drafts of guides * fixing some category links * include a period at the end of each tutorial description * simplify tutorial search box placeholder text * better description for the tutorials page * making the pre-commit script executable * flesh out some of the state archival guides * first effort for some rpc ledger key guides * fix some broken links * add a guide on ingesting events into a db * first effort at storage type guides * Some more stubs, marking drafts, and a couple additions * make not a draft to fix a broken link * guides category descriptions in README.mdx files * marking incomplete chain migration docs as drafts * change some tutorial difficulty levels * fine-tune the tutorials component styling a bit * improving some guides organization * fix linting errors and broken links * include wrap instructions for native lumens * fix broken link in contract metadata guide * final changes to existing guides * moving developer tools into the main sidebar * create real redirects instead of just notes * fix broken links in a dapps challenge page * fix a redirect syntax error * fix a tutorials redirect * Pre mainnet restructure (#644) * docs: rename "fundamentals and concepts" to "soroban internals" * docs: remove old "under the hood" section * docs: add tags to the various tutorials * docs: move tutorials into one main directory * fix broken links to old pages * docs: renaming soroban internals in category file * docs: remove old "command line reference" category * docs: add tokens directory, rearrange sidebars * docs: rearrange migration from evm guide * docs: remove unused reference/interfaces category * docs: move "reference" section to "resources" * docs: move "releases" page up a level * docs: move FAQ page up one level * docs: move dev tools into resources directory * docs: move testnet.mdx to networks.mdx * docs: rearrange items in resources directory * docs: collapse various SDK pages into two pages * docs: move data-providers up one level * docs: remove some empty categories, move the tutorial template * docs: add a new getting-started page, reorganize that section * docs: change sidebar position integers in soroban-internals * docs: shuffling contract interaction around in soroban-internals * style: fixing a couple small markdown nits in dapps directory * docs: fix a broken link * feat: start to the "guides" page(s) * style(lint): fixing a small linting error * remove guides placeholder * fixing broken links * work on guides listing. might revert this * formatting mdx * docs: fixing a broken link to tutorials * style: crack at making the tutorials filterable and hidden in the sidebar * customizing some components for the `/guides` pages and layouts * rename index page for guides to README * remove commented configuration option * remove some console logging in components * change name of index page in guides sidebar * remove commented sidebar generation code * remove comments and add description to sidebar generator * rename sidebar generator file * more work on how the 'guides' pages might look. * docs: adjusting sidebar positions of new getting started pages * fix some broken links in the getting-started section * prefer "README.mdx" files where possible * some more guides placeholder stubs * docs(guides): More placeholder stubs for guides and categories * feat: don't display "guides in category" page on `/guides` * build: check/fix MDX formatting in more directories than just docs * style: add a larger margin before more category guides * style: more selectively increase that top margin * docs(guides): a quick stab at a "publishing" events guide * docs(guides): first stab at the "publish events" guide * docs: updating tutorial descriptions * fix a few broken links * docs: add note about a tuple with one element * style: couple link changes and reformats * move fuzzing tutorial back * change to README file in contract interactions category * docs(guides): give a better title for the wasm metadata guide * fixing some broken markdown links * fix (another!) broken link * move the guides back into the main layout of the docs directory * add placeholder for testnet reset automation stuff * some initial content for some guides * markdown formatting and fixing broken links * moving "resources" back to "reference" to fit the definition better More like "technical reference" rather than a "reference encyclopedia" * change some styles of the tutorial list * removing most category pages * some more first-drafts of guides * fixing some category links * include a period at the end of each tutorial description * simplify tutorial search box placeholder text * better description for the tutorials page * making the pre-commit script executable * flesh out some of the state archival guides * first effort for some rpc ledger key guides * fix some broken links * add a guide on ingesting events into a db * first effort at storage type guides * Some more stubs, marking drafts, and a couple additions * make not a draft to fix a broken link * guides category descriptions in README.mdx files * marking incomplete chain migration docs as drafts * change some tutorial difficulty levels * fine-tune the tutorials component styling a bit * improving some guides organization * fix linting errors and broken links * include wrap instructions for native lumens * fix broken link in contract metadata guide * final changes to existing guides * moving developer tools into the main sidebar * create real redirects instead of just notes * fix broken links in a dapps challenge page * fix a redirect syntax error * fix a tutorials redirect * Update maximum contract size limit in the `Getting Started` tutorial (#705) * Refactor wrap/deploy of SAC (#718) Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Co-authored-by: Elliot Voris * Refactor deprecated calls to `soroban config` (#731) * [Do Not Merge] Updates pending vote (#729) * update to the welcome page to reflect the live on Mainnet status * remove note about non-production software on networks page * add the link definition for the phased rollout * [Stellar Merge] Tools Section (#739) * create redirects for tools and SDKs pages * adjust some redirect URLs to better reflect new stellar-docs pages * forgot a semicolon! * linking to migrated developer tools page * fine-tuning some redirects * add a list of "already migrated" stuff to the sidebar * remove SDKs content that's being migrated * fix the resulting broken links * Revert "remove SDKs content that's being migrated" This reverts commit 6dd2a28a8f124e876607a9bf30894a420ea6eb08. * Revert "linking to migrated developer tools page" This reverts commit fc2340818a08c42fc1f13f6a49d97111610c9100. * try this method of turning docs into external links * include soroban-cli page in the tools migration * change cli links to developer docs * use by-name params in pagination example (#743) Refs: #728 * Update the community RPC list (#744) * Update RPC list with network info * Add run your own RPC * Fix urls for soroban cli * Fix path to cli * Fix relative link * Add formatted mdx --------- Co-authored-by: Jane Wang * update soroban-cli version in getting started: 20.3.0 (#749) * Update soroban cli version to the latest (#755) Co-authored-by: Jane Wang * Remove version from soroban-cli install instruction (#758) * Update outdated code. (#759) * Use `soroban contract init` command in getting started tutorial (#730) * Updates to setup.mdx * Rework hello-world to use the soroban contract init command * Rework deploy-to-testnet.mdx to use init command * Rework storing-data.mdx * Rework storing-data.mdx * Rework deploy-increment-contract.mdx * Rework create-an-app * Udpate deploy increment file name * Apply suggestions from code review Co-authored-by: Elliot Voris * Update re-installing to enable opt to not include version Co-authored-by: Elliot Voris * Update docs/getting-started/setup.mdx Co-authored-by: Elliot Voris * Address PR feedback * Simplfy generate and fund command calls * Update .env.example code block to match the fe template * Small updates toe create-an-app.mdx --------- Co-authored-by: Elliot Voris * Update errors example to use the latest cli. (#768) Co-authored-by: Elliot Voris * Clean up the stellar asset contract overview (#771) Co-authored-by: Elliot Voris * move content into "smart-contracts" directory * initial import of "Smart Contracts" section content * first batch of fixed links * fix some more broken links * format and lint markdown * display DocCards on all "Guides" pages * change "tutorials" to "example contracts" * fix some "tutorials" -> "example-contracts" broken links * use README.mdx files in smart contracts section * break "freighter" page into a few different guides * change a link to a freighter guide * markdown linting * fix a broken link * change a freighter link * remove unnecessary (and wrong) canonical url meta tags * simplify the hidden "guides" and "example contracts" stuff! * move FAQ from "smart contracts" into "learn" sub-section * get rid of the old soroban "welcome" page * update links and pull in latest changes from soroban repo Co-authored-by: Siddharth Suresh * back n forth between issue assets n SAC * adding a couple build app notifications * add meaningful sidebar positions to the example contract pages Refs: stellar/soroban-docs#717 * replace "tutorial" verbiage with "examples" and such * add badges to denote level to example contracts * let's do away with the difficulty, filtering, etc. this is a cleaner way to present them, and the extra bells and whistles we had before were just getting in the way of communicating anything meaningful. --------- Signed-off-by: Elliot Voris Signed-off-by: sonichen <1606673007@qq.com> Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Co-authored-by: Tyler van der Hoeven Co-authored-by: Bri <92327786+briwylde08@users.noreply.github.com> Co-authored-by: Siddharth Suresh Co-authored-by: Alex Cordeiro Co-authored-by: Fabricio Leonardo Sodano Pascazi Co-authored-by: Thibault Co-authored-by: Jay Geng Co-authored-by: jonjove <31668823+jonjove@users.noreply.github.com> Co-authored-by: tomerweller Co-authored-by: Giuliano Co-authored-by: Dmytro Kozhevin Co-authored-by: Justin Rice Co-authored-by: Matias Wald Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Co-authored-by: Piyal Basu Co-authored-by: Jay Geng Co-authored-by: M. Gaylor <115509084+mgaylor@users.noreply.github.com> Co-authored-by: tsachiherman <24438559+tsachiherman@users.noreply.github.com> Co-authored-by: Anuxhya <36203801+achallagundla@users.noreply.github.com> Co-authored-by: lmorgan824 <63170084+lmorgan824@users.noreply.github.com> Co-authored-by: Paul Bellamy Co-authored-by: Willem Wyndham Co-authored-by: Paul Bellamy Co-authored-by: Arthur Ming Co-authored-by: Brian Anderson Co-authored-by: jcx120 <91218921+jcx120@users.noreply.github.com> Co-authored-by: anupsdf <127880479+anupsdf@users.noreply.github.com> Co-authored-by: Julian Martinez <73849597+Julian-dev28@users.noreply.github.com> Co-authored-by: Garand Tyson Co-authored-by: Julian Martinez Co-authored-by: Willem Wyndham Co-authored-by: sonichen <57282299+sonichen@users.noreply.github.com> Co-authored-by: aristides Co-authored-by: Sarju Co-authored-by: Elizabeth Co-authored-by: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Co-authored-by: Sergey Kaunov Co-authored-by: Molly Karcher Co-authored-by: Alfonso Acosta Co-authored-by: Matin Kaboli Co-authored-by: George Co-authored-by: Mason Fischer Co-authored-by: Pamphile Roy Co-authored-by: Jane Wang Co-authored-by: Jane Wang Co-authored-by: Jun Luo <4catcode@gmail.com> Co-authored-by: Nando Vieira --- .../example-application-tutorial/overview.mdx | 2 +- docs/building-apps/overview.mdx | 14 +- docs/building-apps/wallet/overview.mdx | 4 +- docs/issuing-assets/anatomy-of-an-asset.mdx | 2 + docs/issuing-assets/how-to-issue-an-asset.mdx | 6 + docs/learn/glossary.mdx | 2 +- .../solidity-and-rust-advanced-concepts.mdx | 2 +- .../authorization.mdx | 12 +- .../learn/smart-contract-internals/errors.mdx | 4 +- .../learn/smart-contract-internals/events.mdx | 2 +- docs/learn/smart-contract-internals/faq.mdx | 54 + .../types/custom-types.mdx | 2 +- docs/reference/software-versions.mdx | 2 +- .../example-contracts/README.mdx | 16 + .../example-contracts/TEMPLATE.mdx | 159 +++ .../example-contracts/_category_.json | 3 + .../example-contracts/alloc.mdx | 128 +++ .../example-contracts/atomic-multi-swap.mdx | 30 + .../example-contracts/atomic-swap.mdx | 262 +++++ .../example-contracts/auth.mdx | 388 +++++++ .../example-contracts/cross-contract-call.mdx | 275 +++++ .../example-contracts/custom-account.mdx | 358 ++++++ .../example-contracts/custom-types.mdx | 275 +++++ .../example-contracts/deployer.mdx | 494 ++++++++ .../example-contracts/errors.mdx | 349 ++++++ .../example-contracts/events.mdx | 258 +++++ .../example-contracts/fuzzing.mdx | 797 +++++++++++++ .../example-contracts/liquidity-pool.mdx | 900 +++++++++++++++ .../example-contracts/logging.mdx | 256 +++++ .../example-contracts/single-offer-sale.mdx | 27 + .../example-contracts/timelock.mdx | 29 + .../example-contracts/tokens.mdx | 1002 +++++++++++++++++ .../getting-started/README.mdx | 10 + .../getting-started/create-an-app.mdx | 437 +++++++ .../deploy-increment-contract.mdx | 80 ++ .../getting-started/deploy-to-testnet.mdx | 102 ++ .../getting-started/hello-world.mdx | 354 ++++++ .../smart-contracts/getting-started/setup.mdx | 180 +++ .../getting-started/storing-data.mdx | 209 ++++ docs/smart-contracts/guides/README.mdx | 9 + docs/smart-contracts/guides/_category_.json | 3 + .../guides/archival/README.mdx | 6 + .../create-restoration-footprint-js.mdx | 7 + .../guides/archival/restore-contract-js.mdx | 73 ++ .../guides/archival/restore-data-js.mdx | 95 ++ docs/smart-contracts/guides/cli/README.mdx | 8 + .../guides/cli/deploy-contract.mdx | 13 + .../cli/deploy-stellar-asset-contract.mdx | 54 + .../guides/cli/extend-contract-instance.mdx | 23 + .../guides/cli/extend-contract-storage.mdx | 36 + .../guides/cli/extend-contract-wasm.mdx | 34 + .../guides/cli/install-deploy.mdx | 13 + .../guides/cli/install-wasm.mdx | 19 + .../guides/cli/restore-contract-instance.mdx | 14 + .../guides/cli/restore-contract-storage.mdx | 32 + .../guides/conventions/README.mdx | 6 + .../guides/conventions/error-enum.mdx | 40 + .../conventions/upgrading-contracts.mdx | 278 +++++ .../guides/conventions/wasm-metadata.mdx | 31 + .../conventions/work-contractspec-js.mdx | 7 + .../guides/conversions/README.mdx | 6 + .../guides/conversions/address-to-bytesn.mdx | 11 + .../guides/conversions/address-to-id.mdx | 7 + .../guides/conversions/id-to-address.mdx | 7 + docs/smart-contracts/guides/events/README.mdx | 6 + .../smart-contracts/guides/events/consume.mdx | 7 + docs/smart-contracts/guides/events/ingest.mdx | 104 ++ .../smart-contracts/guides/events/publish.mdx | 50 + docs/smart-contracts/guides/fees/README.mdx | 6 + .../guides/fees/cost-analysis.mdx | 7 + .../guides/freighter/README.mdx | 6 + .../guides/freighter/connect-futurenet.mdx | 17 + .../freighter/enable-soroban-tokens.mdx | 14 + .../guides/freighter/prompt-to-sign-tx.mdx | 12 + .../guides/freighter/send-token-payments.mdx | 16 + .../guides/freighter/sign-auth-entries.mdx | 10 + .../guides/freighter/sign-soroban-xdrs.mdx | 14 + docs/smart-contracts/guides/rpc/README.mdx | 6 + .../rpc/generate-ledger-keys-python.mdx | 30 + .../guides/rpc/retrieve-contract-code-js.mdx | 125 ++ .../rpc/retrieve-contract-code-python.mdx | 123 ++ .../guides/rpc/self-deploy-rpc.mdx | 7 + .../guides/rpc/use-public-rpc.mdx | 7 + .../smart-contracts/guides/storage/README.mdx | 6 + .../guides/storage/choose-type.mdx | 7 + .../guides/storage/use-instance.mdx | 31 + .../guides/storage/use-persistent.mdx | 37 + .../guides/storage/use-temporary.mdx | 23 + .../smart-contracts/guides/testing/README.mdx | 6 + .../guides/testing/basic-contract-tests.mdx | 26 + ...integration-testing-multiple-contracts.mdx | 7 + .../guides/testing/mint-native-token.mdx | 11 + .../guides/testing/test-contract-auth.mdx | 42 + .../guides/testing/testnet-reset.mdx | 7 + .../guides/transactions/README.mdx | 8 + .../transactions/invoke-contract-tx-js.mdx | 113 ++ .../submit-transaction-wait-js.mdx | 60 + docs/smart-contracts/tokens/README.mdx | 10 + .../tokens/stellar-asset-contract.mdx | 172 +++ .../tokens/token-interface.mdx | 206 ++++ docusaurus.config.js | 6 + sidebars.js | 6 + src/css/custom.scss | 6 + src/theme/DocCardList/index.js | 75 ++ src/theme/DocCardList/style.module.css | 37 + src/theme/DocItem/Footer/index.js | 8 + 106 files changed, 9772 insertions(+), 23 deletions(-) create mode 100644 docs/learn/smart-contract-internals/faq.mdx create mode 100644 docs/smart-contracts/example-contracts/README.mdx create mode 100644 docs/smart-contracts/example-contracts/TEMPLATE.mdx create mode 100644 docs/smart-contracts/example-contracts/_category_.json create mode 100644 docs/smart-contracts/example-contracts/alloc.mdx create mode 100644 docs/smart-contracts/example-contracts/atomic-multi-swap.mdx create mode 100644 docs/smart-contracts/example-contracts/atomic-swap.mdx create mode 100644 docs/smart-contracts/example-contracts/auth.mdx create mode 100644 docs/smart-contracts/example-contracts/cross-contract-call.mdx create mode 100644 docs/smart-contracts/example-contracts/custom-account.mdx create mode 100644 docs/smart-contracts/example-contracts/custom-types.mdx create mode 100644 docs/smart-contracts/example-contracts/deployer.mdx create mode 100644 docs/smart-contracts/example-contracts/errors.mdx create mode 100644 docs/smart-contracts/example-contracts/events.mdx create mode 100644 docs/smart-contracts/example-contracts/fuzzing.mdx create mode 100644 docs/smart-contracts/example-contracts/liquidity-pool.mdx create mode 100644 docs/smart-contracts/example-contracts/logging.mdx create mode 100644 docs/smart-contracts/example-contracts/single-offer-sale.mdx create mode 100644 docs/smart-contracts/example-contracts/timelock.mdx create mode 100644 docs/smart-contracts/example-contracts/tokens.mdx create mode 100644 docs/smart-contracts/getting-started/README.mdx create mode 100644 docs/smart-contracts/getting-started/create-an-app.mdx create mode 100644 docs/smart-contracts/getting-started/deploy-increment-contract.mdx create mode 100644 docs/smart-contracts/getting-started/deploy-to-testnet.mdx create mode 100644 docs/smart-contracts/getting-started/hello-world.mdx create mode 100644 docs/smart-contracts/getting-started/setup.mdx create mode 100644 docs/smart-contracts/getting-started/storing-data.mdx create mode 100644 docs/smart-contracts/guides/README.mdx create mode 100644 docs/smart-contracts/guides/_category_.json create mode 100644 docs/smart-contracts/guides/archival/README.mdx create mode 100644 docs/smart-contracts/guides/archival/create-restoration-footprint-js.mdx create mode 100644 docs/smart-contracts/guides/archival/restore-contract-js.mdx create mode 100644 docs/smart-contracts/guides/archival/restore-data-js.mdx create mode 100644 docs/smart-contracts/guides/cli/README.mdx create mode 100644 docs/smart-contracts/guides/cli/deploy-contract.mdx create mode 100644 docs/smart-contracts/guides/cli/deploy-stellar-asset-contract.mdx create mode 100644 docs/smart-contracts/guides/cli/extend-contract-instance.mdx create mode 100644 docs/smart-contracts/guides/cli/extend-contract-storage.mdx create mode 100644 docs/smart-contracts/guides/cli/extend-contract-wasm.mdx create mode 100644 docs/smart-contracts/guides/cli/install-deploy.mdx create mode 100644 docs/smart-contracts/guides/cli/install-wasm.mdx create mode 100644 docs/smart-contracts/guides/cli/restore-contract-instance.mdx create mode 100644 docs/smart-contracts/guides/cli/restore-contract-storage.mdx create mode 100644 docs/smart-contracts/guides/conventions/README.mdx create mode 100644 docs/smart-contracts/guides/conventions/error-enum.mdx create mode 100644 docs/smart-contracts/guides/conventions/upgrading-contracts.mdx create mode 100644 docs/smart-contracts/guides/conventions/wasm-metadata.mdx create mode 100644 docs/smart-contracts/guides/conventions/work-contractspec-js.mdx create mode 100644 docs/smart-contracts/guides/conversions/README.mdx create mode 100644 docs/smart-contracts/guides/conversions/address-to-bytesn.mdx create mode 100644 docs/smart-contracts/guides/conversions/address-to-id.mdx create mode 100644 docs/smart-contracts/guides/conversions/id-to-address.mdx create mode 100644 docs/smart-contracts/guides/events/README.mdx create mode 100644 docs/smart-contracts/guides/events/consume.mdx create mode 100644 docs/smart-contracts/guides/events/ingest.mdx create mode 100644 docs/smart-contracts/guides/events/publish.mdx create mode 100644 docs/smart-contracts/guides/fees/README.mdx create mode 100644 docs/smart-contracts/guides/fees/cost-analysis.mdx create mode 100644 docs/smart-contracts/guides/freighter/README.mdx create mode 100644 docs/smart-contracts/guides/freighter/connect-futurenet.mdx create mode 100644 docs/smart-contracts/guides/freighter/enable-soroban-tokens.mdx create mode 100644 docs/smart-contracts/guides/freighter/prompt-to-sign-tx.mdx create mode 100644 docs/smart-contracts/guides/freighter/send-token-payments.mdx create mode 100644 docs/smart-contracts/guides/freighter/sign-auth-entries.mdx create mode 100644 docs/smart-contracts/guides/freighter/sign-soroban-xdrs.mdx create mode 100644 docs/smart-contracts/guides/rpc/README.mdx create mode 100644 docs/smart-contracts/guides/rpc/generate-ledger-keys-python.mdx create mode 100644 docs/smart-contracts/guides/rpc/retrieve-contract-code-js.mdx create mode 100644 docs/smart-contracts/guides/rpc/retrieve-contract-code-python.mdx create mode 100644 docs/smart-contracts/guides/rpc/self-deploy-rpc.mdx create mode 100644 docs/smart-contracts/guides/rpc/use-public-rpc.mdx create mode 100644 docs/smart-contracts/guides/storage/README.mdx create mode 100644 docs/smart-contracts/guides/storage/choose-type.mdx create mode 100644 docs/smart-contracts/guides/storage/use-instance.mdx create mode 100644 docs/smart-contracts/guides/storage/use-persistent.mdx create mode 100644 docs/smart-contracts/guides/storage/use-temporary.mdx create mode 100644 docs/smart-contracts/guides/testing/README.mdx create mode 100644 docs/smart-contracts/guides/testing/basic-contract-tests.mdx create mode 100644 docs/smart-contracts/guides/testing/integration-testing-multiple-contracts.mdx create mode 100644 docs/smart-contracts/guides/testing/mint-native-token.mdx create mode 100644 docs/smart-contracts/guides/testing/test-contract-auth.mdx create mode 100644 docs/smart-contracts/guides/testing/testnet-reset.mdx create mode 100644 docs/smart-contracts/guides/transactions/README.mdx create mode 100644 docs/smart-contracts/guides/transactions/invoke-contract-tx-js.mdx create mode 100644 docs/smart-contracts/guides/transactions/submit-transaction-wait-js.mdx create mode 100644 docs/smart-contracts/tokens/README.mdx create mode 100644 docs/smart-contracts/tokens/stellar-asset-contract.mdx create mode 100644 docs/smart-contracts/tokens/token-interface.mdx create mode 100644 src/theme/DocCardList/index.js create mode 100644 src/theme/DocCardList/style.module.css diff --git a/docs/building-apps/example-application-tutorial/overview.mdx b/docs/building-apps/example-application-tutorial/overview.mdx index 431da0173..e99c46e5a 100644 --- a/docs/building-apps/example-application-tutorial/overview.mdx +++ b/docs/building-apps/example-application-tutorial/overview.mdx @@ -5,7 +5,7 @@ sidebar_position: 10 :::note -This tutorial walks through how to build an application with the [`js-stellar-sdk`], to build with the Wallet SDK, please follow the [Build a Wallet tutorial](../wallet/overview). +This tutorial walks through how to build an application with the [`js-stellar-sdk`], to build with the Wallet SDK, please follow the [Build a Wallet tutorial](../wallet/overview). To build with smart contracts, navigate to the [Smart Contracts section](../../smart-contracts/getting-started/setup.mdx). ::: diff --git a/docs/building-apps/overview.mdx b/docs/building-apps/overview.mdx index cd2318e20..a6f3e3ae0 100644 --- a/docs/building-apps/overview.mdx +++ b/docs/building-apps/overview.mdx @@ -3,6 +3,12 @@ title: Overview sidebar_position: 0 --- +:::note + +This section details how to build applications without smart contracts. Navigate to the [Smart Contracts section](../smart-contracts/getting-started/setup.mdx) to learn about writing contracts on Stellar. + +::: + Stellar is an open-source distributed ledger that you can use as a backend to power various applications and services, such as wallets, payment apps, currency exchanges, micropayment services, platforms for in-game purchases, and more — check out projects being built on Stellar: [Stellar Ecosystem Projects](https://stellar.org/ecosystem/projects#Projects). Stellar has built-in logic for key storage, creating accounts, signing transactions, tracking balances, and queries to the Stellar database, and anyone can use the network to issue, store, transfer, and trade assets. @@ -18,11 +24,3 @@ Many Stellar assets connect to real-world currencies, and Stellar has open proto Stellar-based products and services interoperate by implementing various Stellar Ecosystem Proposals (SEPs), which are publicly created, open-source documents that live in a [GitHub repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem#stellar-ecosystem-proposals-seps) and define how asset issuers, anchors, wallets, and other service providers interact with each other. As a wallet, the most important SEPs are [SEP-24: Hosted Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md), and [SEP-31: Cross Border Payments API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md), [SEP-10: Stellar Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md), [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md), and [SEP-38: Anchor RFQ API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0038.md). - -**This documentation will walk you through how to build wallets using the Wallet SDK (Kotlin and Typescript are currently supported) and how to build a comprehensive basic payment application.** - -:::info - -This documentation is a work in progress and more will be added as it is created. - -::: diff --git a/docs/building-apps/wallet/overview.mdx b/docs/building-apps/wallet/overview.mdx index 51b2e59d0..061edb2ce 100644 --- a/docs/building-apps/wallet/overview.mdx +++ b/docs/building-apps/wallet/overview.mdx @@ -10,8 +10,8 @@ import Header from "./component/header.mdx"; In this guide we will use the Wallet SDK to integrate with the Stellar blockchain and connect to anchors. -:::info +:::note -This documentation is a work in progress and more will be added as it is created. +This documentation walks you through how to build a wallet without smart contracts. To build with smart contracts, navigate to the [Smart Contracts section](../../smart-contracts/getting-started/setup.mdx). ::: diff --git a/docs/issuing-assets/anatomy-of-an-asset.mdx b/docs/issuing-assets/anatomy-of-an-asset.mdx index 039c49a29..35b8d14db 100644 --- a/docs/issuing-assets/anatomy-of-an-asset.mdx +++ b/docs/issuing-assets/anatomy-of-an-asset.mdx @@ -9,6 +9,8 @@ Issuing assets is a core feature of Stellar: any asset can be tokenized (or mint Issuing an asset on Stellar is easy and only takes a few operations. However, there are additional considerations you may need to think about depending on your use case, such as publishing asset information, compliance, and asset supply, which we’ll cover in this documentation. Assets on Stellar have two identifying characteristics: the asset code and the issuer. Since more than one organization can issue a credit representing the same asset, asset codes often overlap (for example, multiple companies offer a USD token on Stellar). Assets are uniquely identified by the combination of their asset code and issuer. +Assets issued on the Stellar network are accessible to smart contracts. Every Stellar asset has reserved a [Stellar Asset Contract](../smart-contracts/tokens/stellar-asset-contract.mdx) that can be deployed by anyone who wants to be able to interact with the asset from a contract. + ## Stablecoins One major category of assets is the stablecoin. A stablecoin is a blockchain-based token whose value is tied to another asset, such as the US dollar, other fiat currencies, commodities like gold, or even cryptocurrencies. There are two types of stablecoin: 1) reserve-backed stablecoins that must have a mechanism for redeeming the asset backing them, and 2) algorithmic stablecoins that don’t have assets backing them and instead rely on an algorithm to control the stablecoin supply. When discussing stablecoins, our documentation will focus on reserve-backed stablecoins. diff --git a/docs/issuing-assets/how-to-issue-an-asset.mdx b/docs/issuing-assets/how-to-issue-an-asset.mdx index b9d7b2d70..b84245cf0 100644 --- a/docs/issuing-assets/how-to-issue-an-asset.mdx +++ b/docs/issuing-assets/how-to-issue-an-asset.mdx @@ -8,6 +8,12 @@ import { Alert } from "@site/src/components/Alert"; In this tutorial, we will walk through the steps to issue an asset on the Stellar test network. +:::note + +If you'd like to interact with an asset issued on the Stellar network in smart contracts, you can create or deploy the [Stellar Asset Contract](../smart-contracts/tokens/stellar-asset-contract.mdx) for that asset. + +::: + ## Prerequisites You must ensure you have the required amount of XLM to create your issuing and distribution accounts and cover the minimum balance and transaction fees. If you’re issuing an asset on the testnet, you can fund your account by getting test XLM from friendbot. If you’re issuing an asset in production, you will need to acquire XLM from another wallet or exchange. diff --git a/docs/learn/glossary.mdx b/docs/learn/glossary.mdx index b1d8c0692..68124f9d2 100644 --- a/docs/learn/glossary.mdx +++ b/docs/learn/glossary.mdx @@ -103,7 +103,7 @@ Flags control access to an asset on the account level. Learn more about flags in An automated test that rapidly stuffs massive amounts of randomized, malformed data into a system to reveal adverse or unexpected results that indicate vulnerabilities. -Read more in the [Fuzz Testing Tutorial](https://soroban.stellar.org/docs/tutorials/fuzzing). +Read more in the [Fuzz Testing Tutorial](../smart-contracts/example-contracts/fuzzing.mdx). ### GitHub diff --git a/docs/learn/migrate/evm/solidity-and-rust-advanced-concepts.mdx b/docs/learn/migrate/evm/solidity-and-rust-advanced-concepts.mdx index 175ff5151..275e8ed2e 100644 --- a/docs/learn/migrate/evm/solidity-and-rust-advanced-concepts.mdx +++ b/docs/learn/migrate/evm/solidity-and-rust-advanced-concepts.mdx @@ -170,7 +170,7 @@ impl AllocContract { In this example, the `alloc` crate is imported into the smart contract using the `extern crate alloc;` statement. The `alloc` crate is then used to create a temporary vector that holds values from 0 to `count`. The values in the vector are then summed and returned. -For more details on how to use the `alloc` crate, including a hands-on practical exercise, visit the [alloc section](https://soroban.stellar.org/docs/tutorials/alloc#how-it-works) of the documentation. +For more details on how to use the `alloc` crate, including a hands-on practical exercise, visit the [alloc example contract](../../../smart-contracts/example-contracts/alloc.mdx#how-it-works). #### Inheriting Functionality from Other Crates diff --git a/docs/learn/smart-contract-internals/authorization.mdx b/docs/learn/smart-contract-internals/authorization.mdx index e5f61d59f..58ccd023a 100644 --- a/docs/learn/smart-contract-internals/authorization.mdx +++ b/docs/learn/smart-contract-internals/authorization.mdx @@ -52,7 +52,7 @@ From the contract perspective `Address` is an opaque identifier type. The contra Both functions ensure that the `Address` has authorized the call of the current function within the current context (where context is defined by `require_auth` calls in the current call stack; see more formal definition in the [section below](#require_auth-implementation-details)). The authentication rules for this authorization are defined by the `Address` and are enforced by the Soroban host. Replay protection is also implemented in the host, i.e., there is normally no need for a contract to manage its own nonces. -[auth example]: https://soroban.stellar.org/docs/tutorials/auth +[auth example]: ../../smart-contracts/example-contracts/auth.mdx #### Authorizing Sub-contract Calls @@ -60,7 +60,7 @@ One of the key features of Soroban Authorization Framework is the ability to eas Contracts don't need to do anything special to benefit from this feature. Just calling a sub-contract that calls `require_auth` will ensure that the sub-contract call has been properly authorized. -[timelock example]: https://soroban.stellar.org/docs/tutorials/timelock +[timelock example]: ../../smart-contracts/example-contracts/timelock.mdx #### When to `require_auth` @@ -74,7 +74,7 @@ The main authorization-related decision a contract writer needs to make for any There is no explicit restriction on how many `Address` entities the contract uses and how many `Address`es have `require_auth` called. That means that it is possible to authorize a contract call on behalf of multiple users, which may even have different authorization contexts (customized via arguments in `require_auth_for_args`). [Atomic swap] is an example that deals with authorization of two `Address`es. -[atomic swap]: https://soroban.stellar.org/docs/tutorials/atomic-swap +[atomic swap]: ../../smart-contracts/example-contracts/atomic-swap.mdx Note though, that contracts that deal with multiple authorized `Address`es need a bit more complex support on the client side (to collect and attach the proper signatures). @@ -90,7 +90,7 @@ Account abstraction provides a convenient extension point for every contract tha Conceptually, every abstract account is a special contract that defines authentication rules and potentially some additional account-specific authorization policies. However, for the sake of optimization and integration with the existing Stellar accounts, Soroban supports 4 different kinds of the account implementations. -Below are the general descriptions of these implementations. See the transaction [guide](contract-interactions/stellar-transaction.mdx) for the concrete information of how different accounts are represented. +Below are the general descriptions of these implementations. See the transaction [guide](./contract-interactions/stellar-transaction.mdx) for the concrete information of how different accounts are represented. ##### Stellar Account @@ -100,7 +100,7 @@ This is a special, built-in 'account contract' that handles all the Stellar acco This supports the Stellar multisig with medium threshold. See Stellar [documentation] for more details on multisig and thresholds. -[documentation]: https://developers.stellar.org/docs/encyclopedia/signatures-multisig +[documentation]: ../encyclopedia/signatures-multisig.mdx ##### Transaction Invoker @@ -128,7 +128,7 @@ Custom account can also be treated as a custodial smart wallet. It holds the use For the exact interface and more details, see the [custom account example]. -[custom account example]: https://soroban.stellar.org/docs/tutorials/custom-account +[custom account example]: ../../smart-contracts/example-contracts/custom-account.mdx ### Advanced Concepts diff --git a/docs/learn/smart-contract-internals/errors.mdx b/docs/learn/smart-contract-internals/errors.mdx index 7ead2acfd..f2547f4ad 100644 --- a/docs/learn/smart-contract-internals/errors.mdx +++ b/docs/learn/smart-contract-internals/errors.mdx @@ -20,10 +20,12 @@ One way is error enum types, that are defined by contracts and that map errors t :::info -The [errors example] demonstrates how to define your own error types. [errors example]: https://soroban.stellar.org/docs/tutorials/errors +The [errors example] demonstrates how to define your own error types. ::: +[errors example]: ../../smart-contracts/example-contracts/errors.mdx + ## Error Enums Errors are a special type of enum integer type that are stored on ledger as `Status` values containing a `u32` code. diff --git a/docs/learn/smart-contract-internals/events.mdx b/docs/learn/smart-contract-internals/events.mdx index 5658f6753..8d9d9dd21 100644 --- a/docs/learn/smart-contract-internals/events.mdx +++ b/docs/learn/smart-contract-internals/events.mdx @@ -21,7 +21,7 @@ Events are the mechanism that applications off-chain can use to monitor changes ## How are events emitted? -`ContractEvents` are emitted in Stellar Core's `TransactionMeta`. You can see in the [TransactionMetaV3] XDR below that there is a list of `OperationEvents` called `events`. Each `OperationEvent` corresponds to an operation in a transaction, and itself contains a list of `ContractEvents`. Note that `events` will only be populated if the transaction succeeds. Take a look at [this example](https://soroban.stellar.org/docs/tutorials/events) to learn more about how to emit an event in your contract. +`ContractEvents` are emitted in Stellar Core's `TransactionMeta`. You can see in the [TransactionMetaV3] XDR below that there is a list of `OperationEvents` called `events`. Each `OperationEvent` corresponds to an operation in a transaction, and itself contains a list of `ContractEvents`. Note that `events` will only be populated if the transaction succeeds. Take a look at [this example](../../smart-contracts/example-contracts/events.mdx) to learn more about how to emit an event in your contract. [transactionmetav3]: #transactionmetav3 diff --git a/docs/learn/smart-contract-internals/faq.mdx b/docs/learn/smart-contract-internals/faq.mdx new file mode 100644 index 000000000..8a625b8cd --- /dev/null +++ b/docs/learn/smart-contract-internals/faq.mdx @@ -0,0 +1,54 @@ +--- +sidebar_position: 170 +title: FAQs +description: Frequently asked questions about Soroban on Stellar. +--- + + + Frequently asked questions about Soroban on Stellar. + + + + + +### What is Soroban to Stellar? Is it a new blockchain? + +Soroban is not a new blockchain. Soroban is a smart contract platform that is integrated into the existing Stellar blockchain. It is an _additive_ feature that lives alongside, and doesn't replace, the existing set of Stellar operations. + +### How do I invoke a Soroban contract on Stellar? + +A Soroban contract can be invoked by submitting a transaction that contains the new operation: `InvokeHostFunctionOp`. + +### Can Soroban contracts use Stellar accounts for authentication? + +Yes. Stellar accounts are shared with Soroban. Smart contacts have access to Stellar account signer configuration and know the source account that directly invoked them in a transaction. Check out the Auth and Advanced Auth examples for more information. + +### Can Soroban contracts interact with Stellar assets? + +Yes. Soroban contains a built-in Stellar Asset Contract that is able to interact with classic trustlines. Read more about this [here](../../smart-contracts/tokens/stellar-asset-contract.mdx). + +### Do issuers of Stellar assets maintain their authorization over an asset that has been sent to a non-account identifier in Soroban? (AUTH_REQUIRED, AUTH_REVOCABLE, AUTH_CLAWBACK) + +Yes. Issuers retain the same level of control on Soroban as they have on Classic. This functionality is accessible through a set of admin functions (clawback, set_auth) on the built-in Stellar Asset Contract. + +### Can Soroban contracts interact with any other Stellar operations? + +No. Aside from the interactions with accounts and assets as mentioned above. This means that Soroban contracts can not interact with SDEX, AMMs, Claimable Balances, or Sponsorships. + +### Does the Stellar base reserve apply to Soroban contracts? + +No. Soroban has a different fee structure and ledger entries that are allocated by Soroban contracts do not add to an account's required minimal balance. + +### Should I issue my token as a Stellar asset or a custom Soroban token? + +We recommend, to the extent possible, issuing tokens as Stellar assets. These tokens will benefit from being interoperable with the existing ecosystem of tools available in the Stellar ecosystem, as well as being more performant because the Stellar Asset Contract is built into the host. + +### Haven't found what you're looking for? + +Join #soroban on the [Stellar Developer Discord](https://discord.gg/stellardev) diff --git a/docs/learn/smart-contract-internals/types/custom-types.mdx b/docs/learn/smart-contract-internals/types/custom-types.mdx index c6f9be784..8e710f687 100644 --- a/docs/learn/smart-contract-internals/types/custom-types.mdx +++ b/docs/learn/smart-contract-internals/types/custom-types.mdx @@ -23,7 +23,7 @@ Custom types are struct, union, and enum types defined by contracts. They are us The [custom types example] demonstrates how to define your own types. -[custom types example]: https://soroban.stellar.org/docs/tutorials/custom-types +[custom types example]: ../../../smart-contracts/example-contracts/custom-types.mdx ::: diff --git a/docs/reference/software-versions.mdx b/docs/reference/software-versions.mdx index 43d6fcb2a..bc2e19a73 100644 --- a/docs/reference/software-versions.mdx +++ b/docs/reference/software-versions.mdx @@ -1255,7 +1255,7 @@ See https://github.com/stellar/soroban-tools/releases v0.7.0 for more details. ### Breaking changes note -This release comes with a revamp of authorization approach that is breaking for most of the contracts that did any sort of auth logic or used tokens. [example](https://soroban.stellar.org/docs/tutorials/auth) and [authorization overview](../learn/smart-contract-internals/authorization.mdx) for more details. +This release comes with a revamp of authorization approach that is breaking for most of the contracts that did any sort of auth logic or used tokens. [example](../smart-contracts/example-contracts/auth.mdx) and [authorization overview](../learn/smart-contract-internals/authorization.mdx) for more details. ### Changelog diff --git a/docs/smart-contracts/example-contracts/README.mdx b/docs/smart-contracts/example-contracts/README.mdx new file mode 100644 index 000000000..6d3e6058a --- /dev/null +++ b/docs/smart-contracts/example-contracts/README.mdx @@ -0,0 +1,16 @@ +--- +title: Example Contracts +sidebar_position: 30 +hide_table_of_contents: true +sidebar_class_name: sidebar-category-items-hidden +--- + +import DocCardList from "@theme/DocCardList"; + +The Soroban team has put together a large collection of [example contracts] to demonstrate use of the Soroban smart contracts platform. For many of these example contracts, we've written an accompanying tutorial that will walk you through the example contract and describe a bit more about its design. + +The examples listed below are provided in a sequential manner. The first listed example contracts create a solid foundation of concepts that will be required during the later examples. While you are absolutely free to choose, read, and use any of the example contracts you like, please keep in mind that the order you see is intentional. + + + +[example contracts]: https://github.com/stellar/soroban-examples diff --git a/docs/smart-contracts/example-contracts/TEMPLATE.mdx b/docs/smart-contracts/example-contracts/TEMPLATE.mdx new file mode 100644 index 000000000..e9e8fdb3c --- /dev/null +++ b/docs/smart-contracts/example-contracts/TEMPLATE.mdx @@ -0,0 +1,159 @@ +--- +title: Tutorial Template +description: A description of the tutorial that is being written. +draft: true +sidebar_position: 1000 +--- + +Quick note about what this [example demonstrates]. Maybe it's also based on some [other example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +:::tip + +Place each link definition at the bottom of the section it (first) is used In + +::: + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 +[example demonstrates]: https://github.com/stellar/soroban-examples/tree/v20.0.0/hello_world +[other example]: ../getting-started/ + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +```bash +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `hello_world` directory, and use `cargo test`. + +```bash +cd hello_world +cargo test +``` + +You should see the output: + +```bash +running 1 test +test test::test ... ok +``` + +[setup]: ../getting-started/setup.mdx + +## Code + +```rust title="hello_world/src/lib.rs" +#![no_std] +use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; + +pub struct HelloContract; + +#[contractimpl] +impl HelloContract { + pub fn hello(env: Env, to: Symbol) -> Vec { + vec![&env, Symbol::short("Hello"), to] + } +} + +mod test; +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/hello_world + +## How it Works + +This is the written part of each guide. You can call out each thing unique to this contract, sometimes referencing other important concepts from other example contracts, too. + +### Major Concepts + +You could add sub-headings for highlighting even further important bits or concepts to know. The `events` example guide has the following `h3` headings: + +- Event Topics +- Event Data +- Publishing + +Underneath each of those headings is a brief discussion of how that concept ties into the example contract code. + +## Tests + +Open the `/hello_world/src/test.rs` file to follow along. + +```rust title="hello_world/src/test.rs" +#![cfg(test)] + +use super::*; +use soroban_sdk::{vec, Env, Symbol}; + +#[test] +fn test() { + let env = Env::default(); + let contract_id = env.register_contract(None, HelloContract); + let client = HelloContractClient::new(&env, &contract_id); + + let words = client.hello(&Symbol::short("Dev")); + assert_eq!( + words, + vec![&env, Symbol::short("Hello"), Symbol::short("Dev"),] + ); +} +``` + +You can describe what's happening in the above test here. Some of the stuff will likely be the same from one guide to another. It looks like, in particular, the following chunk is shared among a few of them: + +{/* BEGIN shared chunk */} + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, IncrementContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `Contract`, and the client is named `ContractClient`. + +{/* /END shared chunk */} + +Then you can describe the intricacies of what your contract test does uniquely. + +## Build the Contract + +To build the contract, use the `cargo build` command. + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +A `.wasm` file should be outputted in the `target` directory: + +```bash +target/wasm32-unknown-unknown/release/soroban_hello_world_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions using it. + +```bash +soroban contract invoke \ + --wasm target/wasm32-unknown-unknown/release/soroban_hello_world_contract.wasm \ + --id 1 \ + -- \ + hello \ + --to Soroban +``` + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli + +## Further Reading + +If you have some further links to share or background knowledge you can link to, this is the place to share it. Or, maybe you want to point out how this example is similar or not when compared with other examples. diff --git a/docs/smart-contracts/example-contracts/_category_.json b/docs/smart-contracts/example-contracts/_category_.json new file mode 100644 index 000000000..163cb24ca --- /dev/null +++ b/docs/smart-contracts/example-contracts/_category_.json @@ -0,0 +1,3 @@ +{ + "collapsible": false +} diff --git a/docs/smart-contracts/example-contracts/alloc.mdx b/docs/smart-contracts/example-contracts/alloc.mdx new file mode 100644 index 000000000..bd1cc78d6 --- /dev/null +++ b/docs/smart-contracts/example-contracts/alloc.mdx @@ -0,0 +1,128 @@ +--- +title: Allocator +description: Use the allocator feature to emulate heap memory in a smart contract. +sidebar_position: 80 +--- + + + + Use the allocator feature to emulate heap memory in a smart contract. + + + + + + +The [allocator example] demonstrates how to utilize the allocator feature when writing a contract. + +[allocator example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/alloc + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +The `soroban-sdk` crate provides a lightweight bump-pointer allocator which can be used to emulate heap memory allocation in a Wasm smart contract. + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `alloc` directory, and use `cargo test`. + +``` +cd alloc +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Dependencies + +This example depends on the `alloc` feature in `soroban-sdk`. To include it, add "alloc" to the "features" list of `soroban-sdk` in the `Cargo.toml` file: + +```rust title="alloc/Cargo.toml" +[dependencies] +soroban-sdk = { version = "20.0.0", features = ["alloc"] } + +[dev_dependencies] +soroban-sdk = { version = "20.0.0", features = ["testutils", "alloc"] } +``` + +## Code + +```rust title="alloc/src/lib.rs" +#![no_std] +use soroban_sdk::{contractimpl, Env}; + +extern crate alloc; + +#[contract] +pub struct AllocContract; + +#[contractimpl] +impl AllocContract { + /// Allocates a temporary vector holding values (0..count), then computes and returns their sum. + pub fn sum(_env: Env, count: u32) -> u32 { + let mut v1 = alloc::vec![]; + (0..count).for_each(|i| v1.push(i)); + + let mut sum = 0; + for i in v1 { + sum += i; + } + + sum + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/alloc + +## How it Works + +```rust +extern crate alloc; +``` + +Imports the `alloc` crate, which is required in order to support allocation under `no_std`. See [Contract Rust dialect] for more info about `no_std`. + +[contract rust dialect]: ../../learn/smart-contract-internals/rust-dialect.mdx + +```rust +let mut v1 = alloc::vec![]; +``` + +Creates a contiguous growable array `v1` with contents allocated on the heap memory. + +:::info + +The heap memory in the context of a smart contract actually refers to the Wasm linear memory. The `alloc` will use the global allocator provided by the soroban sdk to interact with the linear memory. + +::: + +:::caution + +Using heap allocated array is typically slow and computationally expensive. Try to avoid it and instead use a fixed-sized array or `soroban_sdk::vec!` whenever possible. + +This is especially the case for a large-size array. Whenever the array size grows beyond the current linear memory size, which is multiple of the page size (64KB), the [`wasm32::memory_grow`](https://doc.rust-lang.org/core/arch/wasm32/fn.memory_grow.html) is invoked to grow the linear memory by more pages as necessary, which is very computationally expensive. + +::: + +The remaining code pushes values `(0..count)` to `v1`, then computes and returns their sum. This is the simplest example to illustrate how to use the allocator. diff --git a/docs/smart-contracts/example-contracts/atomic-multi-swap.mdx b/docs/smart-contracts/example-contracts/atomic-multi-swap.mdx new file mode 100644 index 000000000..c001ab9a6 --- /dev/null +++ b/docs/smart-contracts/example-contracts/atomic-multi-swap.mdx @@ -0,0 +1,30 @@ +--- +title: Batched Atomic Swaps +description: Swap a token pair among groups of authorized users. +sidebar_position: 100 +--- + + + Swap a token pair among groups of authorized users. + + + + + +The [atomic swap batching example] swaps a pair of tokens between the two groups of users that authorized the `swap` operation from the [Atomic Swap] example. + +This contract basically batches the multiple swaps while following some simple rules to match the swap participants. + +Follow the comments in the code for more information. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 +[atomic swap]: atomic-swap.mdx +[atomic swap batching example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/atomic_multiswap diff --git a/docs/smart-contracts/example-contracts/atomic-swap.mdx b/docs/smart-contracts/example-contracts/atomic-swap.mdx new file mode 100644 index 000000000..e50bf474a --- /dev/null +++ b/docs/smart-contracts/example-contracts/atomic-swap.mdx @@ -0,0 +1,262 @@ +--- +title: Atomic Swap +description: Swap tokens atomically between authorized users. +sidebar_position: 90 +--- + + + Swap tokens atomically between authorized users. + + + + + +The [atomic swap example] swaps two tokens between two authorized parties atomically while following the limits they set. + +This is example demonstrates advanced usage of Soroban auth framework and assumes the reader is familiar with the [auth example](../example-contracts/auth.mdx) and with Soroban token usage. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 +[atomic swap example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/atomic_swap + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example use `cargo test`. + +``` +cargo test -p soroban-atomic-swap-contract +``` + +You should see the output: + +``` +running 1 test +test test::test_atomic_swap ... ok +``` + +## Code + +```rust title="atomic_swap/src/lib.rs" +#[contract] +pub struct AtomicSwapContract; + +#[contractimpl] +impl AtomicSwapContract { + // Swap token A for token B atomically. Settle for the minimum requested price + // for each party (this is an arbitrary choice to demonstrate the usage of + // allowance; full amounts could be swapped as well). + pub fn swap( + env: Env, + a: Address, + b: Address, + token_a: Address, + token_b: Address, + amount_a: i128, + min_b_for_a: i128, + amount_b: i128, + min_a_for_b: i128, + ) { + // Verify preconditions on the minimum price for both parties. + if amount_b < min_b_for_a { + panic!("not enough token B for token A"); + } + if amount_a < min_a_for_b { + panic!("not enough token A for token B"); + } + // Require authorization for a subset of arguments specific to a party. + // Notice, that arguments are symmetric - there is no difference between + // `a` and `b` in the call and hence their signatures can be used + // either for `a` or for `b` role. + a.require_auth_for_args( + (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), + ); + b.require_auth_for_args( + (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), + ); + + // Perform the swap by moving tokens from a to b and from b to a. + move_token(&env, &token_a, &a, &b, amount_a, min_a_for_b); + move_token(&env, &token_b, &b, &a, amount_b, min_b_for_a); + } +} + +fn move_token( + env: &Env, + token: &Address, + from: &Address, + to: &Address, + max_spend_amount: i128, + transfer_amount: i128, +) { + let token = token::Client::new(env, token); + let contract_address = env.current_contract_address(); + // This call needs to be authorized by `from` address. It transfers the + // maximum spend amount to the swap contract's address in order to decouple + // the signature from `to` address (so that parties don't need to know each + // other). + token.transfer(from, &contract_address, &max_spend_amount); + // Transfer the necessary amount to `to`. + token.transfer(&contract_address, to, &transfer_amount); + // Refund the remaining balance to `from`. + token.transfer( + &contract_address, + from, + &(&max_spend_amount - &transfer_amount), + ); +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/atomic_swap + +## How it Works + +The example contract requires two `Address`-es to authorize their parts of the swap operation: one `Address` wants to sell a given amount of token A for token B at a given price and another `Address` wants to sell token B for token A at a given price. The contract swaps the tokens atomically, but only if the requested minimum price is respected for both parties. + +Open the `atomic_swap/src/lib.rs` file or see the code above to follow along. + +### Swap authorization + +```rust +... +a.require_auth_for_args( + (token_a.clone(), token_b.clone(), amount_a, min_b_for_a).into_val(&env), +); +b.require_auth_for_args( + (token_b.clone(), token_a.clone(), amount_b, min_a_for_b).into_val(&env), +); +... +``` + +Authorization of `swap` function leverages `require_auth_for_args` Soroban host function. Both `a` and `b` need to authorize symmetric arguments: token they sell, token they buy, amount of token they sell, minimum amount of token they want to receive. This means that `a` and `b` can be freely exchanged in the invocation arguments (as long as the respective arguments are changed too). + +### Moving the tokens + +```rust +... +// Perform the swap via two token transfers. +move_token(&env, token_a, &a, &b, amount_a, min_a_for_b); +move_token(&env, token_b, &b, &a, amount_b, min_b_for_a); +... +fn move_token( + env: &Env, + token: &Address, + from: &Address, + to: &Address, + max_spend_amount: i128, + transfer_amount: i128, +) { + let token = token::Client::new(env, token); + let contract_address = env.current_contract_address(); + // This call needs to be authorized by `from` address. It transfers the + // maximum spend amount to the swap contract's address in order to decouple + // the signature from `to` address (so that parties don't need to know each + // other). + token.transfer(from, &contract_address, &max_spend_amount); + // Transfer the necessary amount to `to`. + token.transfer(&contract_address, to, &transfer_amount); + // Refund the remaining balance to `from`. + token.transfer( + &contract_address, + from, + &(&max_spend_amount - &transfer_amount), + ); +} +``` + +The swap itself is implemented via two token moves: from `a` to `b` and from `b` to `a`. The token move is implemented via allowance: the users don't need to know each other in order to perform the swap, and instead they authorize the swap contract to spend the necessary amount of token on their behalf via `incr_allow`. Soroban auth framework makes sure that the `incr_allow` signatures would have the proper context, and they won't be usable outside the `swap` contract invocation. + +### Tests + +Open the [`atomic_swap/src/test.rs`] file to follow along. + +[`atomic_swap/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/atomic_swap/src/test.rs + +Refer to another examples for the general information on the test setup. + +The interesting part for this example is verification of `swap` authorization: + +```rust +contract.swap( + &a, + &b, + &token_a.address, + &token_b.address, + &1000, + &4500, + &5000, + &950, +); + +assert_eq!( + env.auths(), + std::vec![ + ( + a.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + contract.address.clone(), + symbol_short!("swap"), + ( + token_a.address.clone(), + token_b.address.clone(), + 1000_i128, + 4500_i128 + ) + .into_val(&env), + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token_a.address.clone(), + symbol_short!("transfer"), + (a.clone(), contract.address.clone(), 1000_i128,).into_val(&env), + )), + sub_invocations: std::vec![] + }] + } + ), + ( + b.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + contract.address.clone(), + symbol_short!("swap"), + ( + token_b.address.clone(), + token_a.address.clone(), + 5000_i128, + 950_i128 + ) + .into_val(&env), + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token_b.address.clone(), + symbol_short!("transfer"), + (b.clone(), contract.address.clone(), 5000_i128,).into_val(&env), + )), + sub_invocations: std::vec![] + }] + } + ), + ] +); +``` + +`env.auths()` returns all the authorizations. In the case of `swap` four authorizations are expected. Two for each address authorizing, because each address authorizes not only the swap, but the `approve` all on the token being sent. diff --git a/docs/smart-contracts/example-contracts/auth.mdx b/docs/smart-contracts/example-contracts/auth.mdx new file mode 100644 index 000000000..b126aa675 --- /dev/null +++ b/docs/smart-contracts/example-contracts/auth.mdx @@ -0,0 +1,388 @@ +--- +title: Auth +description: Implement authentication and authorization. +sidebar_position: 50 +--- + + + Implement authentication and authorization. + + + + + +The [auth example] demonstrates how to implement authentication and authorization using the Soroban Host-managed auth framework. + +This example is an extension of the [storing data example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[storing data example]: ../getting-started/storing-data.mdx +[auth example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/auth + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `auth` directory, and use `cargo test`. + +``` +cd auth +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="auth/src/lib.rs" +#[contracttype] +pub enum DataKey { + Counter(Address), +} + +#[contract] +pub struct IncrementContract; + +#[contractimpl] +impl IncrementContract { + /// Increment increments a counter for the user, and returns the value. + pub fn increment(env: Env, user: Address, value: u32) -> u32 { + // Requires `user` to have authorized call of the `increment` of this + // contract with all the arguments passed to `increment`, i.e. `user` + // and `value`. This will panic if auth fails for any reason. + // When this is called, Soroban host performs the necessary + // authentication, manages replay prevention and enforces the user's + // authorization policies. + // The contracts normally shouldn't worry about these details and just + // write code in generic fashion using `Address` and `require_auth` (or + // `require_auth_for_args`). + user.require_auth(); + + // This call is equilvalent to the above: + // user.require_auth_for_args((&user, value).into_val(&env)); + + // The following has less arguments but is equivalent in authorization + // scope to the above calls (the user address doesn't have to be + // included in args as it's guaranteed to be authenticated). + // user.require_auth_for_args((value,).into_val(&env)); + + // Construct a key for the data being stored. Use an enum to set the + // contract up well for adding other types of data to be stored. + let key = DataKey::Counter(user.clone()); + + // Get the current count for the invoker. + let mut count: u32 = env.storage().persistent().get(&key).unwrap_or_default(); + + // Increment the count. + count += value; + + // Save the count. + env.storage().persistent().set(&key, &count); + + // Return the count to the caller. + count + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/auth + +## How it Works + +The example contract stores a per-`Address` counter that can only be incremented by the owner of that `Address`. + +Open the `auth/src/lib.rs` file or see the code above to follow along. + +### `Address` + +```rust +#[contracttype] +pub enum DataKey { + Counter(Address), +} +``` + +`Address` is a universal Soroban identifier that may represent a Stellar account, a contract or an 'account contract' (a contract that defines a custom authentication scheme and authorization policies). Contracts don't need to distinguish between these internal representations though. `Address` can be used any time some network identity needs to be represented, like to distinguish between counters for different users in this example. + +:::tip Enum keys like `DataKey` are useful for organizing contract storage. + +Different enum values create different key 'namespaces'. + +In the example the counter for each address is stored against `DataKey::Counter(Address)`. If the contract needs to start storing other types of data, it can do so by adding additional variants to the enum. ::: + +### `require_auth` + +```rust +impl IncrementContract { + pub fn increment(env: Env, user: Address, value: u32) -> u32 { + user.require_auth(); +``` + +`require_auth` method can be called for any `Address`. Semantically `user.require_auth()` here means 'require `user` to have authorized calling `increment` function of the current `IncrementContract` instance with the current call arguments, i.e. the current `user` and `value` argument values'. In simpler terms, this ensures that the `user` has allowed incrementing their counter value and nobody else can increment it. + +When using `require_auth` the contract implementation doesn't need to worry about the signatures, authentication, and replay prevention. All these features are implemented by the Soroban host and happen automatically as long as the `Address` type is used. + +`Address` has another method called `require_auth_for_args`. It works in the same fashion as `require_auth`, but allows customizing the arguments that need to be authorized. Note though, this should be used with care to ensure that there is a deterministic mapping between the contract invocation arguments and the `require_auth_for_args` arguments. + +The following two calls are functionally equivalent to `user.require_auth`: + +```rust +// Completely equivalent +user.require_auth_for_args((&user, value).into_val(&env)); +// The following has less arguments but is equivalent in authorization +// scope to the above call (the user address doesn't have to be +// included in args as it's guaranteed to be authenticated). +user.require_auth_for_args((value,).into_val(&env)); +``` + +### Tests + +Open the [`auth/src/test.rs`] file to follow along. + +[`auth/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/auth/src/test.rs + +```rust title="auth/src/test.rs" +fn test() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); + + let user_1 = Address::random(&env); + let user_2 = Address::random(&env); + + assert_eq!(client.increment(&user_1, &5), 5); + // Verify that the user indeed had to authorize a call of `increment` with + // the expected arguments: + assert_eq!( + env.auths(), + [( + // Address for which auth is performed + user_1.clone(), + // Identifier of the called contract + contract_id.clone(), + // Name of the called function + symbol_short!("increment"), + // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) + (user_1.clone(), 5_u32).into_val(&env) + )] + ); + + // Do more `increment` calls. It's not necessary to verify authorizations + // for every one of them as we don't expect the auth logic to change from + // call to call. + assert_eq!(client.increment(&user_1, &2), 7); + assert_eq!(client.increment(&user_2, &1), 1); + assert_eq!(client.increment(&user_1, &3), 10); + assert_eq!(client.increment(&user_2, &4), 5); +} + +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The test instructs the environment to mock all auths. All calls to `require_auth` or `require_auth_for_args` will succeed. + +```rust +env.mock_all_auths(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, IncrementContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. + +```rust +let client = IncrementContractClient::new(&env, &contract_id); +``` + +Generate `Address`es for two users. Normally the exact value of the `Address` shouldn't matter for testing, so they're simply generated randomly. + +```rust +let user_1 = Address::random(&env); +let user_2 = Address::random(&env); +``` + +Invoke `increment` function for `user_1`. + +```rust +assert_eq!(client.increment(&user_1, &5), 5); +``` + +In order to verify that the `require_auth` call(s) have indeed happened, use `auths` function that returns a vector of tuples containing the authorizations from the most recent contract invocation. + +```rust +assert_eq!( + env.auths(), + [( + // Address for which auth is performed + user_1.clone(), + // Identifier of the called contract + contract_id.clone(), + // Name of the called function + symbol_short!("increment"), + // Arguments used to call `increment` (converted to the env-managed vector via `into_val`) + (user_1.clone(), 5_u32).into_val(&env) + )] +); +``` + +Invoke `increment` function several more times for both users. Notice, that the values are tracked separately for each users. + +```rust +assert_eq!(client.increment(&user_1, &2), 7); +assert_eq!(client.increment(&user_2, &1), 1); +assert_eq!(client.increment(&user_1, &3), 10); +assert_eq!(client.increment(&user_2, &4), 5); +``` + +## Build the Contract + +To build the contract into a `.wasm` file, use the `soroban contract build` command. + +```sh +soroban contract build +``` + +The `.wasm` file should be found in the `target` directory after building: + +``` +target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke functions on the contract. + +But since we are dealing with authorization and signatures, we need to set up some identities to use for testing and get their public keys: + +```sh +soroban keys generate acc1 && \ +soroban keys generate acc2 && \ +soroban keys address acc1 && \ +soroban keys address acc2 +``` + +Example output with two public keys of identities: + +``` +GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU +GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B +``` + +Now the contract itself can be invoked. Notice the `--source` must be the identity name matching the address passed to the `--user` argument. This allows `soroban` tool to automatically sign the necessary payload for the invocation. + +```sh +soroban contract invoke \ + --source acc1 \ + --wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \ + --id 1 \ + -- \ + increment \ + --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \ + --value 2 +``` + +Run a few more increments for both accounts. + +```sh +soroban contract invoke \ + --source acc2 \ + --wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \ + --id 1 \ + -- \ + increment \ + --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ + --value 5 +``` + +```sh +soroban contract invoke \ + --source acc1 \ + --wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \ + --id 1 \ + -- \ + increment \ + --user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \ + --value 3 +``` + +```sh +soroban contract invoke \ + --source acc2 \ + --wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \ + --id 1 \ + -- \ + increment \ + --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ + --value 10 +``` + +View the data that has been stored against each user with `soroban contract read`. + +```sh +soroban contract read --id 1 +``` + +``` +"[""Counter"",""GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU""]",5 +"[""Counter"",""GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B""]",15 +``` + +It is also possible to preview the authorization payload that is being signed by providing `--auth` flag to the invocation: + +```sh +soroban contract invoke \ + --source acc2 \ + --auth \ + --wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \ + --id 1 \ + -- \ + increment \ + --user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \ + --value 123 +``` + +```json +Contract auth: [{"address_with_nonce":null,"root_invocation":{"contract_id":"0000000000000000000000000000000000000000000000000000000000000001","function_name":"increment","args":[{"object":{"address":{"account":{"public_key_type_ed25519":"c7bab0288753d58d3e21cc3fa68cd2546b5f78ae6635a6f1b3fe07e03ee846e9"}}}},{"u32":123}],"sub_invocations":[]},"signature_args":[]}] +``` + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli + +## Further reading + +[Authorization documentation](../../learn/smart-contract-internals/authorization.mdx) provides more details on how Soroban auth framework works. + +[Timelock](../example-contracts/timelock.mdx) and [Single Offer](../example-contracts/single-offer-sale.mdx) examples demonstrate authorizing token operations on behalf of the user, which can be extended to any nested contract invocations. + +[Atomic Swap](../example-contracts/atomic-swap.mdx) example demonstrates multi-party authorization where multiple users sign their parts of the contract invocation. + +[Custom Account](../example-contracts/custom-account.mdx) example for demonstrates an account contract that defines a custom authentication scheme and user-defined authorization policies. diff --git a/docs/smart-contracts/example-contracts/cross-contract-call.mdx b/docs/smart-contracts/example-contracts/cross-contract-call.mdx new file mode 100644 index 000000000..4ad20edf9 --- /dev/null +++ b/docs/smart-contracts/example-contracts/cross-contract-call.mdx @@ -0,0 +1,275 @@ +--- +title: Cross Contract Calls +description: Call a smart contract from another smart contract. +sidebar_position: 60 +--- + + + Call a smart contract from another smart contract. + + + + + +The [cross contract call example] demonstrates how to call a contract from another contract. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +:::info + +In this example there are two contracts that are compiled separately, deployed separately, and then tested together. There are a variety of ways to develop and test contracts with dependencies on other contracts, and the Soroban SDK and tooling is still building out the tools to support these workflows. Feedback appreciated [here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). + +::: + +[cross contract call example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/cross_contract + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `cross_contract/contract_b` directory, and use `cargo test`. + +``` +cd cross_contract/contract_b +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="cross_contract/contract_a/src/lib.rs" +#[contract] +pub struct ContractA; + +#[contractimpl] +impl ContractA { + pub fn add(x: u32, y: u32) -> u32 { + x.checked_add(y).expect("no overflow") + } +} +``` + +```rust title="cross_contract/contract_b/src/lib.rs" +mod contract_a { + soroban_sdk::contractimport!( + file = "../contract_a/target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm" + ); +} + +#[contract] +pub struct ContractB; + +#[contractimpl] +impl ContractB { + pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { + let client = contract_a::Client::new(&env, &contract); + client.add(&x, &y) + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/cross_contract + +## How it Works + +Cross contract calls are made by invoking another contract by its contract ID. + +Contracts to invoke can be imported into your contract with the use of `contractimport!(file = "...")`. The import will code generate: + +- A `ContractClient` type that can be used to invoke functions on the contract. +- Any types in the contract that were annotated with `#[contracttype]`. + +:::tip + +The `contractimport!` macro will generate the types in the module it is used, so it's a good idea to use the macro inside a `mod { ... }` block, or inside its own file, so that the names of generated types don't collide with names of types in your own contract. + +::: + +Open the files above to follow along. + +### Contract A: The Contract to be Called + +The contract to be called is Contract A. It is a simple contract that accepts `x` and `y` parameters, adds them together and returns the result. + +```rust title="cross_contract/contract_a/src/lib.rs" +#[contract] +pub struct ContractA; + +#[contractimpl] +impl ContractA { + pub fn add(x: u32, y: u32) -> u32 { + x.checked_add(y).expect("no overflow") + } +} +``` + +:::tip + +The contract uses the `checked_add` method to ensure that there is no overflow, and if there is overflow, panics rather than returning an overflowed value. Rust's primitive integer types all have checked operations available as functions with the prefix `checked_`. + +::: + +### Contract B: The Contract doing the Calling + +The contract that does the calling is Contract B. It accepts a contract ID that it will call, as well as the same parameters to pass through. In many contracts the contract to call might have been stored as contract data and be retrieved, but in this simple example it is being passed in as a parameter each time. + +The contract imports Contract A into the `contract_a` module. + +The `contract_a::Client` is constructed pointing at the contract ID passed in. + +The client is used to execute the `add` function with the `x` and `y` parameters on Contract A. + +```rust title="cross_contract_calls/src/a.rs" +mod contract_a { + soroban_sdk::contractimport!( + file = "../contract_a/target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm" + ); +} + +#[contract] +pub struct ContractB; + +#[contractimpl] +impl ContractB { + pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 { + let client = contract_a::Client::new(&env, &contract); + client.add(&x, &y) + } +} +``` + +### Tests + +Open the `cross_contract/contract_b/src/test.rs` file to follow along. + +```rust title="cross_contract/contract_b/src/test.rs" +#[test] +fn test() { + let env = Env::default(); + + // Register contract A using the imported Wasm. + let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm); + + // Register contract B defined in this crate. + let contract_b_id = env.register_contract(None, ContractB); + + // Create a client for calling contract B. + let client = ContractBClient::new(&env, &contract_b_id); + + // Invoke contract B via its client. Contract B will invoke contract A. + let sum = client.add_with(&contract_a_id, &5, &7); + assert_eq!(sum, 12); +} +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +Contract A is registered with the environment using the imported Wasm. + +```rust +let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm); +``` + +Contract B is registered with the environment using the contract type. + +```rust +let contract_b_id = env.register_contract(None, ContractB); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `ContractB`, and the client is named `ContractBClient`. The client can be constructed and used in the same way that client generated for Contract A can be. + +```rust +let client = ContractBClient::new(&env, &contract_b_id); +``` + +The client is used to invoke the `add_with` function on Contract B. Contract B will invoke Contract A, and the result will be returned. + +```rust +let sum = client.add_with(&contract_a_id, &5, &7); +``` + +The test asserts that the result that is returned is as we expect. + +```rust +assert_eq!(sum, 12); +``` + +## Build the Contracts + +To build the contract into a `.wasm` file, use the `soroban contract build` command. Both `contract_call/contract_a` and `contract_call/contract_b` must be built, with `contract_a` being built first. + +```sh +soroban contract build +``` + +Both `.wasm` files should be found in both contract `target` directories after building both contracts: + +``` +target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm +``` + +``` +target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions. Both contracts must be deployed. + +```sh +soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm \ + --id a +``` + +```sh +soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \ + --id b +``` + +Invoke Contract B's `add_with` function, passing in values for `x` and `y` (e.g. as `5` and `7`), and then pass in the contract ID of Contract A. + +```sh +soroban contract invoke \ + --id b \ + -- \ + add_with \ + --contract_id a \ + --x 5 \ + --y 7 +``` + +The following output should occur using the code above. + +```json +12 +``` + +Contract B's `add_with` function invoked Contract A's `add` function to do the addition. + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/custom-account.mdx b/docs/smart-contracts/example-contracts/custom-account.mdx new file mode 100644 index 000000000..a0de89857 --- /dev/null +++ b/docs/smart-contracts/example-contracts/custom-account.mdx @@ -0,0 +1,358 @@ +--- +title: Custom Account +description: Implement an account contract supporting multisig and custom authorization policies. +sidebar_position: 150 +--- + + + + Implement an account contract supporting multisig and custom authorization + policies. + + + + + + +The [custom account example] demonstrates how to implement a simple account contract that supports multisig and customizable authorization policies. This account contract can be used with the Soroban auth framework, so that any time an `Address` pointing at this contract instance is used, the custom logic implemented here is applied. + +Custom accounts are exclusive to Soroban and can't be used to perform other Stellar operations. + +:::danger + +Implementing a custom account contract requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract - use it as an API reference only. + +::: + +:::caution + +While custom accounts are supported by the Stellar protocol and Soroban SDK, the full client support (such as transaction simulation) is still under development. + +::: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[custom account example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/account + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example use `cargo test`. + +``` +cargo test -p soroban-account-contract +``` + +You should see the output: + +``` +running 1 test +test test::test_token_auth ... ok +``` + +## How it Works + +Open the `account/src/lib.rs` file to follow along. + +Account contracts implement a special function `__check_auth` that takes the signature payload, signatures and authorization context. The function should error if auth is declined, otherwise auth will be approved. + +This example contract uses ed25519 keys for signature verification and supports multiple equally weighted signers. It also implements a policy that allows setting per-token limits on transfers. The token can be spent beyond the limit only if every signature is provided. + +For example, the user may initialize this contract with 2 keys and introduce 100 USDC spend limit. This way they can use a single key to sign their contract invocations and be sure that even if they sign a malicious transaction they won't spend more than 100 USDC. + +### Initialization + +```rust +#[contracttype] +#[derive(Clone)] +enum DataKey { + SignerCnt, + Signer(BytesN<32>), + SpendLimit(BytesN<32>), +} +... +// Initialize the contract with a list of ed25519 public key ('signers'). +pub fn init(env: Env, signers: Vec>) { + // In reality this would need some additional validation on signers + // (deduplication etc.). + for signer in signers.iter() { + env.storage().instance().set(&DataKey::Signer(signer), &()); + } + env.storage() + .instance() + .set(&DataKey::SignerCnt, &signers.len()); +} +``` + +This account contract needs to work with the public keys explicitly. Here we initialize the contract with ed25519 keys. + +### Policy modification + +```rust +// Adds a limit on any token transfers that aren't signed by every signer. +pub fn add_limit(env: Env, token: BytesN<32>, limit: i128) { + // The current contract address is the account contract address and has + // the same semantics for `require_auth` call as any other account + // contract address. + // Note, that if a contract *invokes* another contract, then it would + // authorize the call on its own behalf and that wouldn't require any + // user-side verification. + env.current_contract_address().require_auth(); + env.storage() + .instance() + .set(&DataKey::SpendLimit(token), &limit); +} +``` + +This function allows users to set and modify the per-token spend limit described above. The neat trick here is that `require_auth` can be used for the `current_contract_address()`, i.e. the account contract may be used to verify authorization for its own administrative functions. This way there is no need to write duplicate authorization and authentication logic. + +### `__check_auth` + +```rust +pub fn __check_auth( + env: Env, + signature_payload: BytesN<32>, + signatures: Vec, + auth_context: Vec, +) -> Result<(), AccError> { + // Perform authentication. + authenticate(&env, &signature_payload, &signatures)?; + + let tot_signers: u32 = env + .storage() + .instance() + .get::<_, u32>(&DataKey::SignerCnt) + .unwrap(); + let all_signed = tot_signers == signatures.len(); + + let curr_contract = env.current_contract_address(); + + // This is a map for tracking the token spend limits per token. This + // makes sure that if e.g. multiple `transfer` calls are being authorized + // for the same token we still respect the limit for the total + // transferred amount (and not the 'per-call' limits). + let mut spend_left_per_token = Map::::new(&env); + // Verify the authorization policy. + for context in auth_context.iter() { + verify_authorization_policy( + &env, + &context, + &curr_contract, + all_signed, + &mut spend_left_per_token, + )?; + } + Ok(()) +} +``` + +`__check_auth` is a special function that account contracts implement. It will be called by the Soroban environment every time `require_auth` or `require_auth_for_args` is called for the address of the account contract. + +Here it is implemented in two steps. First, authentication is performed using the signature payload and a vector of signatures. Second, authorization policy is enforced using the `auth_context` vector. This vector contains all the contract calls that are being authorized by the provided signatures. + +`__check_auth` is a reserved function and can only be called by the Soroban environment in response to a call to `require_auth`. Any direct call to `__check_auth` will fail. This makes it safe to write to the account contract storage from `__check_auth`, as it's guaranteed to not be called in unexpected context. In this example it's possible to persist the spend limits without worrying that they'll be exhausted via a bad actor calling `__check_auth` directly. + +### Authentication + +```rust +fn authenticate( + env: &Env, + signature_payload: &BytesN<32>, + signatures: &Vec, +) -> Result<(), AccError> { + for i in 0..signatures.len() { + let signature = signatures.get_unchecked(i); + if i > 0 { + let prev_signature = signatures.get_unchecked(i - 1); + if prev_signature.public_key >= signature.public_key { + return Err(AccError::BadSignatureOrder); + } + } + if !env + .storage() + .instance() + .has(&DataKey::Signer(signature.public_key.clone())) + { + return Err(AccError::UnknownSigner); + } + env.crypto().ed25519_verify( + &signature.public_key, + &signature_payload.clone().into(), + &signature.signature, + ); + } + Ok(()) +} +``` + +Authentication here simply checks that the provided signatures are valid given the payload and also that they belong to the signers of this account contract. + +### Authorization policy + +```rust +fn verify_authorization_policy( + env: &Env, + context: &Context, + curr_contract: &Address, + all_signed: bool, + spend_left_per_token: &mut Map, +) -> Result<(), AccError> { + // For the account control every signer must sign the invocation. + let contract_context = match context { + Context::Contract(c) => { + if &c.contract == curr_contract { + if !all_signed { + return Err(AccError::NotEnoughSigners); + } + } + c + } + Context::CreateContractHostFn(_) => return Err(AccError::InvalidContext), + }; +``` + +We verify the policy per `Context`. i.e. Per one `require_auth` call. The policy for the account contract itself enforces every signer to have signed the method call. + +```rust +// Otherwise, we're only interested in functions that spend tokens. +if contract_context.fn_name != TRANSFER_FN + && contract_context.fn_name != Symbol::new(env, "approve") +{ + return Ok(()); +} + +let spend_left: Option = + if let Some(spend_left) = spend_left_per_token.get(contract_context.contract.clone()) { + Some(spend_left) + } else if let Some(limit_left) = env + .storage() + .instance() + .get::<_, i128>(&DataKey::SpendLimit(contract_context.contract.clone())) + { + Some(limit_left) + } else { + None + }; + +// 'None' means that the contract is outside of the policy. +if let Some(spend_left) = spend_left { + // 'amount' is the third argument in both `approve` and `transfer`. + // If the contract has a different signature, it's safer to panic + // here, as it's expected to have the standard interface. + let spent: i128 = contract_context + .args + .get(2) + .unwrap() + .try_into_val(env) + .unwrap(); + if spent < 0 { + return Err(AccError::NegativeAmount); + } + if !all_signed && spent > spend_left { + return Err(AccError::NotEnoughSigners); + } + spend_left_per_token.set(contract_context.contract.clone(), spend_left - spent); +} +Ok(()) +``` + +Then we check for the standard token function names and verify that for these function we don't exceed the spending limits. + +### Tests + +Open the [`account/src/test.rs`] file to follow along. + +[`account/src/test.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/account/src/test.rs + +Refer to another examples for the general information on the test setup. + +Here we only look at some points specific to the account contracts. + +```rust +fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> RawVal { + Signature { + public_key: signer_public_key(e, signer), + signature: signer + .sign(payload.to_array().as_slice()) + .to_bytes() + .into_val(e), + } + .into_val(e) +} +``` + +Unlike most of the contracts that may simply use `Address`, account contracts deal with the signature verification and hence need to actually sign the payloads. + +```rust +let payload = BytesN::random(&env); +let token = BytesN::random(&env); +env.try_invoke_contract_check_auth::( + &account_contract.address.contract_id(), + &payload, + &vec![&env, sign(&env, &signers[0], &payload)], + &vec![ + &env, + token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1000), + ], +) +.unwrap(); +``` + +`__check_auth` can't be called directly as regular contract functions, hence we need to use `try_invoke_contract_check_auth` testing utility that emulates being called by the Soroban host during a `require_auth` call. + +```rust +// Add a spend limit of 1000 per 1 signer. +account_contract.add_limit(&token, &1000); +// Verify that this call needs to be authorized. +assert_eq!( + env.auths(), + std::vec![( + account_contract.address.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + account_contract.address.clone(), + symbol_short!("add_limit"), + (token.clone(), 1000_i128).into_val(&env), + )), + sub_invocations: std::vec![] + } + )] +); +``` + +Asserting the contract-specific error to `try_invoke_contract_check_auth` allows verifying the exact error code and makes sure that the verification has failed due to not having enough signers and not for any other reason. + +It's a good idea for the account contract to have detailed error codes and verify that they are returned when they are expected. + +```rust +assert_eq!( + env.try_invoke_contract_check_auth::( + &account_contract.address.contract_id(), + &payload, + &vec![&env, sign(&env, &signers[0], &payload)], + &vec![ + &env, + token_auth_context(&env, &token, Symbol::new(&env, "transfer"), 1001) + ], + ) + .err() + .unwrap() + .unwrap(), + AccError::NotEnoughSigners +); +``` diff --git a/docs/smart-contracts/example-contracts/custom-types.mdx b/docs/smart-contracts/example-contracts/custom-types.mdx new file mode 100644 index 000000000..2a96ac5cb --- /dev/null +++ b/docs/smart-contracts/example-contracts/custom-types.mdx @@ -0,0 +1,275 @@ +--- +title: Custom Types +description: Define your own data structures in a smart contract. +sidebar_position: 20 +--- + + + Define your own data structures in a smart contract. + + + + + +The [custom types example] demonstrates how to define your own data structures that can be stored on the ledger, or used as inputs and outputs to contract invocations. This example is an extension of the [storing data example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[custom types example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/custom_types +[storing data example]: ../getting-started/storing-data.mdx + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `custom_types` directory, and use `cargo test`. + +``` +cd custom_types +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="custom_types/src/lib.rs" +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct State { + pub count: u32, + pub last_incr: u32, +} + +const STATE: Symbol = symbol_short!("STATE"); + +#[contract] +pub struct IncrementContract; + +#[contractimpl] +impl IncrementContract { + /// Increment increments an internal counter, and returns the value. + pub fn increment(env: Env, incr: u32) -> u32 { + // Get the current count. + let mut state = Self::get_state(env.clone()); + + // Increment the count. + state.count += incr; + state.last_incr = incr; + + // Save the count. + env.storage().instance().set(&STATE, &state); + + // Return the count to the caller. + state.count + } + /// Return the current state. + pub fn get_state(env: Env) -> State { + env.storage().instance().get(&STATE).unwrap_or(State { + count: 0, + last_incr: 0, + }) // If no value set, assume 0. + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/custom_types + +## How it Works + +Custom types are defined using the `#[contracttype]` attribute on either a `struct` or an `enum`. + +Open the `custom_types/src/lib.rs` file to follow along. + +### Custom Type: Struct + +Structs are stored on ledger as a map of key-value pairs, where the key is up to a 32 character string representing the field name, and the value is the value encoded. + +Field names must be no more than 32 characters. + +```rust +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct State { + pub count: u32, + pub last_incr: u32, +} +``` + +### Custom Type: Enum + +The example does not contain enums, but enums may also be contract types. + +Enums containing unit and tuple variants are stored on ledger as a two element vector, where the first element is the name of the enum variant as a string up to 32 characters in length, and the value is the value if the variant has one. + +Only unit variants and single value variants, like `A` and `B` below, are supported. + +```rust +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Enum { + A, + B(...), +} +``` + +Enums containing integer values are stored on ledger as the `u32` value. + +```rust +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Enum { + A = 1, + B = 2, +} +``` + +### Using Types in Functions + +Types that have been annotated with `#[contracttype]` can be stored as contract data and retrieved later. + +Types can also be used as inputs and outputs on contract functions. + +```rust +pub fn increment(env: Env, incr: u32) -> u32 { + let mut state = Self::get_state(env.clone()); + state.count += incr; + state.last_incr = incr; + env.storage().instance().set(&STATE, &state); + state.count +} + +pub fn get_state(env: Env) -> State { + env.storage().instance().get(&STATE).unwrap_or(State { + count: 0, + last_incr: 0, + }) // If no value set, assume 0. +} +``` + +## Tests + +Open the `custom_types/src/test.rs` file to follow along. + +```rust title="custom_types/src/test.rs" +#[test] +fn test() { + let env = Env::default(); + let contract_id = env.register_contract(None, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); + + assert_eq!(client.increment(&1), 1); + assert_eq!(client.increment(&10), 11); + assert_eq!( + client.get_state(), + State { + count: 11, + last_incr: 10 + } + ); +} +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, IncrementContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. + +```rust +let client = IncrementContractClient::new(&env, &contract_id); +``` + +The test invokes the `increment` function on the registered contract that causes the `State` type to be stored and updated a couple times. + +```rust +assert_eq!(client.increment(&1), 1); +assert_eq!(client.increment(&10), 11); +``` + +The test then invokes the `get_state` function to get the `State` value that was stored, and can assert on its values. + +```rust +assert_eq!( + client.get_state(), + State { + count: 11, + last_incr: 10 + } +); +``` + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +```sh +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory: + +``` +target/wasm32-unknown-unknown/release/soroban_custom_types_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions in the Wasm using it. + +```sh +soroban contract invoke \ + --wasm target/wasm32-unknown-unknown/release/soroban_custom_types_contract.wasm \ + --id 1 \ + -- \ + increment \ + --incr 5 +``` + +The following output should occur using the code above. + +``` +5 +``` + +Run it a few more times with different increment amounts to watch the count change. + +Use the `soroban` to inspect what the counter is after a few runs. + +```sh +soroban contract read --id 1 --key STATE +``` + +``` +STATE,"{""count"":25,""last_incr"":15}" +``` + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/deployer.mdx b/docs/smart-contracts/example-contracts/deployer.mdx new file mode 100644 index 000000000..d479c5033 --- /dev/null +++ b/docs/smart-contracts/example-contracts/deployer.mdx @@ -0,0 +1,494 @@ +--- +title: Deployer +description: Deploy and initialize a smart contract using another smart contract. +sidebar_position: 70 +--- + + + + Deploy and initialize a smart contract using another smart contract. + + + + + + +The [deployer example] demonstrates how to deploy contracts using a contract. + +Here we deploy a contract on behalf of any address and initialize it atomically. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +:::info + +In this example there are two contracts that are compiled separately, and the tests deploy one with the other. + +::: + +[deployer example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/deployer + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `deployer/deployer` directory, and use `cargo test`. + +``` +cd deployer/deployer +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="deployer/deployer/src/lib.rs" +#[contract] +pub struct Deployer; + +#[contractimpl] +impl Deployer { + /// Deploy the contract Wasm and after deployment invoke the init function + /// of the contract with the given arguments. + /// + /// This has to be authorized by `deployer` (unless the `Deployer` instance + /// itself is used as deployer). This way the whole operation is atomic + /// and it's not possible to frontrun the contract initialization. + /// + /// Returns the contract address and result of the init function. + pub fn deploy( + env: Env, + deployer: Address, + wasm_hash: BytesN<32>, + salt: BytesN<32>, + init_fn: Symbol, + init_args: Vec, + ) -> (Address, Val) { + // Skip authorization if deployer is the current contract. + if deployer != env.current_contract_address() { + deployer.require_auth(); + } + + // Deploy the contract using the uploaded Wasm with given hash. + let deployed_address = env + .deployer() + .with_address(deployer, salt) + .deploy(wasm_hash); + + // Invoke the init function with the given arguments. + let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args); + // Return the contract ID of the deployed contract and the result of + // invoking the init result. + (deployed_address, res) + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/deployer + +## How it Works + +Contracts can deploy other contracts using the SDK `deployer()` method. + +The contract address of the deployed contract is deterministic and is derived from the address of the deployer. The deployment also has to be authorized by the deployer. + +Open the `deployer/deployer/src/lib.rs` file to follow along. + +### Contract Wasm Upload + +Before deploying the new contract instances, the Wasm code needs to be uploaded on-chain. Then it can be used to deploy an arbitrary number of contract instances. The upload should typically happen outside of the deployer contract, as it needs to happen just once. However, it is possible to use `env.deployer().upload_contract_wasm()` function to upload Wasm from a contract as well. + +See the [tests](#tests) for an example of uploading the contract code programmatically. For the actual on-chain installation see the general deployment [tutorial](https://soroban.stellar.org/docs/getting-started/deploy-to-testnet). + +### Authorization + +:::info + +This section can be skipped for factory contracts that deploy another contract from their own address (`deployer == env.current_contract_address()``). + +::: + +:::info + +For introduction to Soroban authorization see the [auth tutorial](./auth.mdx) + +::: + +We start with verifying authorization of the `deployer`, unless its the current contract (at which point the authorization is implied). + +```rust +if deployer != env.current_contract_address() { + deployer.require_auth(); +} +``` + +While `deployer().with_address()` performs authorization as well, we want to make sure that `deployer` has also authorized the whole operation, as besides deployment it also performs atomic contract initialization. If we didn't require deployer authorization here, then it would be possible to frontrun the deployment operation performed by `deployer` and initialize it differently, thus breaking the promise of atomic initialization. + +See more details on the actual authorization payloads in [tests](#tests). + +### `deployer()` + +The `deployer()` SDK function comes with a few deployment-related utilities. Here we use the most generic deployer kind, `with_address(deployer_address, salt)`. + +```rust +let deployed_address = env + .deployer() + .with_address(deployer, salt) + .deploy(wasm_hash); +``` + +`with_address()` accepts the `deployer` address and salt. Both are used to derive the address of the deployed contract deterministically. It is not possible to re-deploy an already existing contract. + +`deploy()` function performs the actual deployment using the provided `wasm_hash`. The implementation of the new contract is defined by the Wasm file uploaded under `wasm_hash`. + +:::tip + +Only the `wasm_hash` itself is stored per contract ID thus saving the ledger space and fees. + +::: + +When only deploying the contract on behalf of the current contract, i.e. when `deployer` address is always `env.current_contract_address()` it is possible to use `deployer().with_current_contract(salt)` function for brevity. + +### Initialization + +The contract can be called immediately after deployment, which is useful for initialization. + +```rust +let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args); +``` + +`invoke_contract` can call any defined contract function with any arguments. We pass the actual function to call and the arguments from `deploy` inputs. The result can be any value, depending on the `init_fn`'s return value. + +If the initialization fails, then the whole `deploy` call falls and thus the contract won't be deployed. This behavior is required for the atomic initialization guarantee as well. + +The contract returns the deployed contract's address and the result of executing the initialization function. + +```rust + (deployed_address, res) +``` + +### Tests + +Open the `deployer/deployer/src/test.rs` file to follow along. + +Import the test contract Wasm to be deployed. + +```rust +// The contract that will be deployed by the deployer contract. +mod contract { + soroban_sdk::contractimport!( + file = + "../contract/target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm" + ); +} +``` + +That contract contains the following code that exports two functions: initialization function that takes a value and a getter function for the stored initialized value. + +```rust title="deployer/contract/src/lib.rs" +#[contract] +pub struct Contract; + +const KEY: Symbol = symbol_short!("value"); + +#[contractimpl] +impl Contract { + pub fn init(env: Env, value: u32) { + env.storage().instance().set(&KEY, &value); + } + pub fn value(env: Env) -> u32 { + env.storage().instance().get(&KEY).unwrap() + } +} +``` + +This test contract will be used when testing the deployer. The deployer contract will deploys the test contract and invoke its `init` function. + +There are two tests: deployment from the current contract without authorization and deployment from an arbitrary address with authorization. Besides authorization, these tests are very similar. + +#### Curent contract deployer + +In the first test we deploy contract from the `Deployer` contract instance itself. + +```rust +#[test] +fn test_deploy_from_contract() { + let env = Env::default(); + let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); + + // Upload the Wasm to be deployed from the deployer contract. + // This can also be called from within a contract if needed. + let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); + + // Deploy contract using deployer, and include an init function to call. + let salt = BytesN::from_array(&env, &[0; 32]); + let init_fn = symbol_short!("init"); + let init_fn_args: Vec = (5u32,).into_val(&env); + let (contract_id, init_result) = deployer_client.deploy( + &deployer_client.address, + &wasm_hash, + &salt, + &init_fn, + &init_fn_args, + ); + + assert!(init_result.is_void()); + // No authorizations needed - the contract acts as a factory. + assert_eq!(env.auths(), vec![]); + + // Invoke contract to check that it is initialized. + let client = contract::Client::new(&env, &contract_id); + let sum = client.value(); + assert_eq!(sum, 5); +} +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +Register the deployer contract with the environment and create a client to for it. + +```rust +let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); +``` + +Upload the code of the test contract that we have imported above via `contractimport!` and get the hash of the uploaded Wasm code. + +```rust +let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); +``` + +The client is used to invoke the `deploy` function. The contract will deploy the test contract using the hash of its Wasm code, call the `init` function, and pass in a single `5u32` argument. The expected return value of `init` function is just `void` (i.e. no value). + +```rust +let salt = BytesN::from_array(&env, &[0; 32]); +let init_fn = symbol_short!("init"); +let init_fn_args: Vec = (5u32,).into_val(&env); +let (contract_id, init_result) = deployer_client.deploy( + &deployer_client.address, + &wasm_hash, + &salt, + &init_fn, + &init_fn_args, +); +``` + +The test checks that the test contract was deployed by using its client to invoke it and get back the value set during initialization. + +```rust +let client = contract::Client::new(&env, &contract_id); +let sum = client.value(); +assert_eq!(sum, 5); +``` + +#### External deployer + +The second test is very similar to the first one. + +```rust +#[test] +fn test_deploy_from_address() { + let env = Env::default(); + let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); + + // Upload the Wasm to be deployed from the deployer contract. + // This can also be called from within a contract if needed. + let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); + + // Define a deployer address that needs to authorize the deployment. + let deployer = Address::random(&env); + + // Deploy contract using deployer, and include an init function to call. + let salt = BytesN::from_array(&env, &[0; 32]); + let init_fn = symbol_short!("init"); + let init_fn_args: Vec = (5u32,).into_val(&env); + env.mock_all_auths(); + let (contract_id, init_result) = + deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args); + + assert!(init_result.is_void()); + + let expected_auth = AuthorizedInvocation { + // Top-level authorized function is `deploy` with all the arguments. + function: AuthorizedFunction::Contract(( + deployer_client.address, + symbol_short!("deploy"), + ( + deployer.clone(), + wasm_hash.clone(), + salt, + init_fn, + init_fn_args, + ) + .into_val(&env), + )), + // From `deploy` function the 'create contract' host function has to be + // authorized. + sub_invocations: vec![AuthorizedInvocation { + function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs { + contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress { + address: deployer.clone().try_into().unwrap(), + salt: Uint256([0; 32]), + }), + executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))), + }), + sub_invocations: vec![], + }], + }; + assert_eq!(env.auths(), vec![(deployer, expected_auth)]); + + // Invoke contract to check that it is initialized. + let client = contract::Client::new(&env, &contract_id); + let sum = client.value(); + assert_eq!(sum, 5); +} +``` + +The main difference is that the contract is deployed on behalf of the arbitrary address. + +```rust +// Define a deployer address that needs to authorize the deployment. +let deployer = Address::random(&env); +``` + +Before invoking the contract we need to enable mock authorization in order to get the recorded authorization payload that we can verify. + +```rust +env.mock_all_auths(); +let (contract_id, init_result) = + deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args); +``` + +The expected authorization tree for the `deployer` looks as follows. + +```rust +let expected_auth = AuthorizedInvocation { + // Top-level authorized function is `deploy` with all the arguments. + function: AuthorizedFunction::Contract(( + deployer_client.address, + symbol_short!("deploy"), + ( + deployer.clone(), + wasm_hash.clone(), + salt, + init_fn, + init_fn_args, + ) + .into_val(&env), + )), + // From `deploy` function the 'create contract' host function has to be + // authorized. + sub_invocations: vec![AuthorizedInvocation { + function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs { + contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress { + address: deployer.clone().try_into().unwrap(), + salt: Uint256([0; 32]), + }), + executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))), + }), + sub_invocations: vec![], + }], +}; +``` + +At the top level we have the `deploy` function itself with all the arguments that we've passed to it. From the `deploy` function the `CreateContractHostFn` has to be authorized. This is the authorization payload that has to be authorized by any deployer in any context. It contains the deployer address, salt and executable. + +This authorization tree proves that the deployment and initialization are authorized atomically: actual deployment happens within the context of `deploy` and all of salt, executable, and initialization arguments are authorized together (i.e. there is one signature to authorizes this exact combination). + +Then we make sure that deployer has authorized the expected tree and that expected value has been stored. + +```rust +assert_eq!(env.auths(), vec![(deployer, expected_auth)]); + +let client = contract::Client::new(&env, &contract_id); +let sum = client.value(); +assert_eq!(sum, 5); +``` + +## Build the Contracts + +To build the contract into a `.wasm` file, use the `soroban contract build` command. Build both the deployer contract and the test contract. + +```sh +soroban contract build +``` + +Both `.wasm` files should be found in both contract `target` directories after building both contracts: + +``` +target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm +``` + +``` +target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke the contract function to deploy the test contract. + +Before deploying the test contract with the deployer, install the test contract Wasm using the `install` command. The `install` command will print out the hash derived from the Wasm file (it's not just the hash of the Wasm file itself though) which should be used by the deployer. + +```sh +soroban contract install --wasm contract/target/wasm32-unknown-unknown/release/soroban_deployer_test_contract.wasm +``` + +The command prints out the hash as hex. It will look something like `7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15`. + +We also need to deploy the `Deployer` contract: + +```sh +soroban contract deploy --wasm deployer/target/wasm32-unknown-unknown/release/soroban_deployer_contract.wasm --id 1 +``` + +This will return the deployer address: `CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM`. + +Then the deployer contract may be invoked with the Wasm hash value above. + +```sh +soroban contract invoke --id 1 -- deploy \ + --deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM + --salt 123 \ + --wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 \ + --init_fn init \ + --init_args '[{"u32":5}]' +``` + +And then invoke the deployed test contract using the identifier returned from the previous command. + +```sh +soroban contract invoke \ + --id ead19f55aec09bfcb555e09f230149ba7f72744a5fd639804ce1e934e8fe9c5d \ + -- \ + value +``` + +The following output should occur using the code above. + +```json +5 +``` + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/errors.mdx b/docs/smart-contracts/example-contracts/errors.mdx new file mode 100644 index 000000000..916ec2ce0 --- /dev/null +++ b/docs/smart-contracts/example-contracts/errors.mdx @@ -0,0 +1,349 @@ +--- +title: Errors +description: Define and generate errors in a smart contract. +sidebar_position: 30 +--- + + + Define and generate errors in a smart contract. + + + + + +The [errors example] demonstrates how to define and generate errors in a contract that invokers of the contract can understand and handle. This example is an extension of the [storing data example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[errors example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/errors +[storing data example]: ../getting-started/storing-data.mdx + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `errors` directory, and use `cargo test`. + +``` +cd errors +cargo test +``` + +You should see output that begins like this: + +``` +running 2 tests + +count: U32(0) +count: U32(1) +count: U32(2) +count: U32(3) +count: U32(4) +count: U32(5) +Status(ContractError(1)) +contract call invocation resulted in error Status(ContractError(1)) +test test::test ... ok + +thread 'test::test_panic' panicked at 'called `Result::unwrap()` on an `Err` value: HostError +Value: Status(ContractError(1)) + +Debug events (newest first): + 0: "Status(ContractError(1))" + 1: "count: U32(5)" + 2: "count: U32(4)" + 3: "count: U32(3)" +... +test test::test_panic - should panic ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s +``` + +## Code + +```rust title="errors/src/lib.rs" +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + LimitReached = 1, +} + +const COUNTER: Symbol = symbol_short!("COUNTER"); +const MAX: u32 = 5; + +#[contract] +pub struct IncrementContract; + +#[contractimpl] +impl IncrementContract { + /// Increment increments an internal counter, and returns the value. Errors + /// if the value is attempted to be incremented past 5. + pub fn increment(env: Env) -> Result { + // Get the current count. + let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. + log!(&env, "count: {}", count); + + // Increment the count. + count += 1; + + // Check if the count exceeds the max. + if count <= MAX { + // Save the count. + env.storage().instance().set(&COUNTER, &count); + + // Return the count to the caller. + Ok(count) + } else { + // Return an error if the max is exceeded. + Err(Error::LimitReached) + } + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/errors + +## How it Works + +Open the `errors/src/lib.rs` file to follow along. + +### Defining an Error + +Contract errors are Rust u32 enums where every variant of the enum is assigned an integer. The `#[contracterror]` attribute is used to set the error up so it can be used in the return value of contract functions. + +The enum has some constraints: + +- It must have the `#[repr(u32)]` attribute. +- It must have the `#[derive(Copy)]` attribute. +- Every variant must have an explicit integer value assigned. + +```rust +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + LimitReached = 1, +} +``` + +Contract errors cannot be stored as contract data, and therefore cannot be used as types on fields of contract types. + +:::tip + +If an error is returned from a function anything the function has done is rolled back. If ledger entries have been altered, or contract data stored, all those changes are reverted and will not be persisted. + +::: + +### Returning an Error + +Errors can be returned from contract functions by returning `Result<_, E>`. + +The increment function returns a `Result`, which means it returns `Ok(u32)` in the successful case, and `Err(Error)` in the error case. + +```rust +pub fn increment(env: Env) -> Result { + // ... + if count <= MAX { + // ... + Ok(count) + } else { + // ... + Err(Error::LimitReached) + } +} +``` + +### Panicking with an Error + +Errors can also be panicked instead of being returned from the function. + +The increment function could also be written as follows with a `u32` return value. The error can be passed to the environment using the `panic_with_error!` macro. + +```rust +pub fn increment(env: Env) -> u32 { + // ... + if count <= MAX { + // ... + count + } else { + // ... + panic_with_error!(&env, Error::LimitReached) + } +} +``` + +:::caution + +Functions that do not return a `Result<_, E>` type do not include in their specification what the possible error values are. This makes it more difficult for other contracts and clients to integrate with the contract. However, this might be ideal if the errors are diagnostic and debugging, and not intended to be handled. + +::: + +## Tests + +Open the `errors/src/test.rs` file to follow along. + +```rust title="errors/src/test.rs" +#[test] +fn test() { + let env = Env::default(); + let contract_id = env.register_contract(None, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); + + assert_eq!(client.try_increment(), Ok(Ok(1))); + assert_eq!(client.try_increment(), Ok(Ok(2))); + assert_eq!(client.try_increment(), Ok(Ok(3))); + assert_eq!(client.try_increment(), Ok(Ok(4))); + assert_eq!(client.try_increment(), Ok(Ok(5))); + assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached))); + + std::println!("{}", env.logs().all().join("\n")); +} + +#[test] +#[should_panic(expected = "Status(ContractError(1))")] +#E3256B +fn test_panic() { + let env = Env::default(); + let contract_id = env.register_contract(None, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); + + assert_eq!(client.increment(), 1); + assert_eq!(client.increment(), 2); + assert_eq!(client.increment(), 3); + assert_eq!(client.increment(), 4); + assert_eq!(client.increment(), 5); + client.increment(); +} +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, IncrementContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. + +```rust +let client = IncrementContractClient::new(&env, &contract_id); +``` + +Two functions are generated for every contract function, one that returns a `Result<>`, and the other that does not handle errors and panicks if an error occurs. + +### `try_increment` + +In the first test the `try_increment` function is called and returns `Result, Result>`. + +```rust +assert_eq!(client.try_increment(), Ok(Ok(5))); +assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached))); +``` + +- If the function call is successful, `Ok(Ok(u32))` is returned. + +- If the function call is successful but returns a value that is not a `u32`, `Ok(Err(_))` is returned. + +- If the function call is unsuccessful, `Err(Ok(Error))` is returned. + +- If the function call is unsuccessful but returns an error code not in the `Error` enum, or returns a system error code, `Err(Err(Status))` is returned and the `Status` can be inspected. + +### `increment` + +In the second test the `increment` function is called and returns `u32`. When the last call is made the function panicks. + +```rust +assert_eq!(client.increment(), 5); +client.increment(); +``` + +- If the function call is successful, `u32` is returned. + +- If the function call is successful but returns a value that is not a `u32`, a panic occurs. + +- If the function call is unsuccessful, a panic occurs. + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +```sh +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory: + +``` +target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm +``` + +## Run the Contract + +Let's deploy the contract to Testnet so we can run it. The value provided as `--source` was set up in our Getting Started guide; please change accordingly if you created a different identity. + +```sh +soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm \ + --source alice \ + --network testnet +``` + +The command above will output the contract id, which in our case is `CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X`. + +Now that we've deployed the contract, we can invoke it. + +```sh +soroban contract invoke \ + --id CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X \ + --network testnet \ + --source alice \ + -- \ + increment +``` + +Run the command a few times and on the 6th invocation you should see an error like this: + +``` +... +error: transaction simulation failed: host invocation failed + +Caused by: + HostError: Error(Contract, #1) + + Event log (newest first): + 0: [Diagnostic Event] contract:, topics:[error, Error(Contract, #1)], data:"escalating Ok(ScErrorType::Contract) frame-exit to Err" + 1: [Diagnostic Event] topics:[fn_call, Bytes(b7461eb3410fe31861b7d8cfea1de74e118ae6a6ccc45dfadbe15904aa6e6369), increment], data:Void +... +``` + +To retrieve the current counter value, use the command `soroban contract read`. + +```sh +soroban contract read \ + --id CC3UMHVTIEH6GGDBW7MM72Q545HBDCXGU3GMIXP23PQVSBFKNZRWT37X \ + --network testnet \ + --source alice \ + --durability persistent \ + --output json +``` + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/events.mdx b/docs/smart-contracts/example-contracts/events.mdx new file mode 100644 index 000000000..0d813e05f --- /dev/null +++ b/docs/smart-contracts/example-contracts/events.mdx @@ -0,0 +1,258 @@ +--- +title: Events +description: Publish events from a smart contract. +sidebar_position: 10 +--- + + + Publish events from a smart contract. + + + + + +The [events example] demonstrates how to publish events from a contract. This example is an extension of the [storing data example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[events example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/events +[storing data example]: ../getting-started/storing-data.mdx + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `events` directory, and use `cargo test`. + +``` +cd events +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="events/src/lib.rs" +const COUNTER: Symbol = symbol_short!("COUNTER"); + +#[contract] +pub struct IncrementContract; + +#[contractimpl] +impl IncrementContract { + /// Increment increments an internal counter, and returns the value. + pub fn increment(env: Env) -> u32 { + // Get the current count. + let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0. + + // Increment the count. + count += 1; + + // Save the count. + env.storage().instance().set(&COUNTER, &count); + + // Publish an event about the increment occuring. + // The event has two topics: + // - The "COUNTER" symbol. + // - The "increment" symbol. + // The event data is the count. + env.events() + .publish((COUNTER, symbol_short!("increment")), count); + + // Return the count to the caller. + count + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/events + +## How it Works + +This example contract extends the increment example by publishing an event each time the counter is incremented. + +Contract events let contracts emit information about what their contract is doing. + +Contracts can publish events using the environments events publish function. + +```rust +env.events().publish(topics, data); +``` + +### Event Topics + +An event may contain up to four topics. + +Topics are conveniently defined using a tuple. In the sample code two topics of `Symbol` type are used. + +```rust +env.events().publish((COUNTER, symbol_short!("increment")), ...); +``` + +:::tip + +The topics don't have to be made of the same type. You can mix different types as long as the total topic count stays below the limit. + +::: + +### Event Data + +An event also contains a data object of any value or type including types defined by contracts using `#[contracttype]`. In the example the data is the `u32` count. + +```rust +env.events().publish(..., count); +``` + +### Publishing + +Publishing an event is done by calling the `publish` function and giving it the topics and data. The function returns nothing on success, and panics on failure. Possible failure reasons can include malformed inputs (e.g. topic count exceeds limit) and running over the resource budget (TBD). Once successfully published, the new event will be available to applications consuming the events. + +```rust +env.events().publish((COUNTER, symbol_short!("increment")), count); +``` + +:::caution + +Published events are discarded if a contract invocation fails due to a panic, budget exhaustion, or when the contract returns an error. + +::: + +## Tests + +Open the `events/src/test.rs` file to follow along. + +```rust title="events/src/test.rs" +#[test] +fn test() { + let env = Env::default(); + let contract_id = env.register_contract(None, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); + + assert_eq!(client.increment(), 1); + assert_eq!(client.increment(), 2); + assert_eq!(client.increment(), 3); + + assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id.clone(), + (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), + 1u32.into_val(&env) + ), + ( + contract_id.clone(), + (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), + 2u32.into_val(&env) + ), + ( + contract_id, + (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), + 3u32.into_val(&env) + ), + ] + ); +} +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, IncrementContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `IncrementContract`, and the client is named `IncrementContractClient`. + +```rust +let client = IncrementContractClient::new(&env, &contract_id); +``` + +The example invokes the contract several times. + +```rust +assert_eq!(client.increment(), 1); +``` + +The example asserts that the events were published. + +```rust +assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id.clone(), + (symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env), + 1u32.into_val(&env) + ), + // ... + ] +); +``` + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +```sh +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory: + +``` +target/wasm32-unknown-unknown/release/soroban_events_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions in the using it. + +```sh +soroban contract invoke \ + --wasm target/wasm32-unknown-unknown/release/soroban_events_contract.wasm \ + --id 1 \ + -- \ + increment +``` + +The following output should occur using the code above. + +```json +1 +#0: event: {"ext":"v0","contractId":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"type":"contract","body":{"v0":{"topics":[{"symbol":[67,79,85,78,84,69,82]},{"symbol":[105,110,99,114,101,109,101,110,116]}],"data":{"u32":1}}}} +``` + +A single event `#0` is outputted, which is the contract event the contract published. The event contains the two topics, each a `symbol` (displayed as bytes), and the data object containing the `u32`. + +:::info + +Soroban is a pre-release and at this time outputs events in an unstable JSON format. + +::: + +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/fuzzing.mdx b/docs/smart-contracts/example-contracts/fuzzing.mdx new file mode 100644 index 000000000..8bacdfb84 --- /dev/null +++ b/docs/smart-contracts/example-contracts/fuzzing.mdx @@ -0,0 +1,797 @@ +--- +title: Fuzz Testing +description: Increase confidence in a contract's correctness with fuzz testing. +sidebar_position: 160 +--- + + + + Increase confidence in a contract's correctness with fuzz testing. + + + + + + +The [fuzzing example] demonstrates how to fuzz test Soroban contracts with [`cargo-fuzz`] and customize the input to fuzz tests with the [`arbitrary`] crate. It also demonstrates how to adapt fuzz tests into reusable property tests with the [`proptest`] and [`proptest-arbitrary-interop`] crates. It builds on the [timelock example]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[fuzzing example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing +[`cargo-fuzz`]: https://docs.rs/cargo-fuzz +[`arbitrary`]: https://docs.rs/arbitrary +[`proptest`]: https://docs.rs/proptest +[`proptest-arbitrary-interop`]: https://docs.rs/proptest-arbitrary-interop +[timelock example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/timelock + +## Run the Example + +First go through the [setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +```bash +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +You will also need the `cargo-fuzz` tool, and to run `cargo-fuzz` you will need a nightly Rust toolchain: + +```bash +cargo install cargo-fuzz +rustup install nightly +``` + +To run one of the fuzz tests, navigate to the `fuzzing` directory and run the `cargo fuzz` subcommand with the `nightly` toolchain: + +```bash +cd fuzzing +cargo +nightly fuzz run fuzz_target_1 +``` + +:::info + +If you're developing on MacOS you may need to add the `--sanitizer=thread` flag in order to fix some [known linking errors](https://github.com/stellar/rs-soroban-sdk/issues/1056). + +::: + +You should see output that begins like this: + +``` +$ cargo +nightly fuzz run fuzz_target_1 + Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) + Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) + Finished release [optimized + debuginfo] target(s) in 23.74s + Finished release [optimized + debuginfo] target(s) in 0.07s + Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 ...` +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 886588732 +INFO: Loaded 1 modules (1093478 inline 8-bit counters): 1093478 [0x55eb8e2c7620, 0x55eb8e3d2586), +INFO: Loaded 1 PC tables (1093478 PCs): 1093478 [0x55eb8e3d2588,0x55eb8f481be8), +INFO: 105 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: seed corpus: files: 105 min: 32b max: 61b total: 3558b rss: 86Mb +#2 pulse ft: 8355 exec/s: 1 rss: 307Mb +#4 pulse cov: 8354 ft: 11014 corp: 1/32b exec/s: 2 rss: 313Mb +#8 pulse cov: 8495 ft: 12420 corp: 4/128b exec/s: 4 rss: 315Mb +``` + +The rest of this tutorial will explain how to set up this fuzz test, interpret this output, and remedy fuzzing failures. + +## Background: Fuzz Testing and Rust + +Fuzzing is a kind of testing where new inputs are repeatedly fed into a program in hopes of finding unexpected bugs. This style of testing is commonly employed to increase confidence in the correctness of security-sensitive software. + +In Rust, fuzzing is most often performed with the [`cargo-fuzz`] tool, which drives LLVM's [`libfuzzer`], though other fuzzing tools are available. + +[`libfuzzer`]: https://llvm.org/docs/LibFuzzer.html + +Soroban has built-in support for fuzzing Soroban contracts with `cargo-fuzz`. + +`cargo-fuzz` is a mutation-based fuzzer: it runs a test program, passing it generated input; while the program is executing, the fuzzer monitors which branches the program takes, and which functions it executes; after execution the fuzzer uses this information to make decisions about how to _mutate_ the previously-used input to create new input that might discover more branches and functions; it then runs the test again with new input, repeating this process for potentially millions of iterations. In this way `cargo-fuzz` is able to automatically explore execution paths through the program that may never be seen by other types of tests. + +If a fuzz tests panics or hard-crashes, `cargo-fuzz` considers it a failure and provides instructions for repeating the test with the failing inputs. + +Fuzz testing is typically an exploratory and interactive process, with the programmer devising schemes for producing input that will stress the program in interesting ways, observing the behavior of the fuzz test, and iterating on the test itself. + +Resolving a fuzz testing failure typically involves capturing the problematic input in a unit test. The fuzz test itself may or may not be kept, depending on determinations about the cost of maintaining the fuzzer vs the likelihood of it continuing to find bugs in the future. + +While fuzzing non-memory-safe software tends to be more lucrative than fuzzing Rust software, it is still relatively common to find panics and other logic errors in Rust through fuzzing. + +In Rust, multiple fuzzers are maintained by the [`rust-fuzz`] GitHub organization, which also maintains a "trophy case" of Rust bugs found through fuzzing. + +[`rust-fuzz`]: https://github.com/rust-fuzz + +## About the Example + +The example used for this tutorial is based on the [`timelock`] example program, with some changes to demonstrate fuzzing. + +[`timelock`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/timelock + +The contract, `ClaimableBalanceContract`, allows one party to deposit an arbitrary quantity of a token to the contract, specifying additionally: the `claimants`, addresses that may withdraw from the contract; and the `time_bound`, a specification of when those claimants may withdraw from the account. + +The `TimeBound` type looks like + +```rust +#[derive(Clone)] +#[contracttype] +pub struct TimeBound { + pub kind: TimeBoundKind, + pub timestamp: u64, +} + +#[derive(Clone)] +#[contracttype] +pub enum TimeBoundKind { + Before, + After, +} +``` + +`ClaimableBalanceContract` has two methods, `deposit` and `claim`: + +```rust + pub fn deposit( + env: Env, + from: Address, + token: Address, + amount: i128, + claimants: Vec
, + time_bound: TimeBound, + ); + + pub fn claim( + env: Env, + claimant: Address, + amount: i128, + ); +``` + +`deposit` may only be successfully called once, after which `claim` may be called multiple times until the balance is completely drained, at which point the contract becomes dormant and may no longer be used. + +## Fuzz Testing Setup + +For these examples, the fuzz tests have been created for you, but normally you would use the `cargo fuzz init` command to create a fuzzing project as a subdirectory of the contract under test. + +To do that you would navigate to the contract directory, in this case, `soroban-examples/fuzzing`, and execute + +```bash +cargo fuzz init +``` + +A `cargo-fuzz` project is its own crate, which lives in the `fuzz` subdirectory of the crate being tested. This crate has its own `Cargo.toml` and `Cargo.lock`, and another subdirectory, `fuzz_targets`, which contains Rust programs, each its own fuzz test. + +Our `soroban-examples/fuzzing` directory looks like + +- `Cargo.toml` - this is the contract's manifest +- `Cargo.lock` +- `src` + - `lib.rs` - this is the contract code +- `fuzz` - this is the fuzzing crate + - `Cargo.toml` - this is fuzzing crate's manifest + - `Cargo.lock` + - `fuzz_targets` + - `fuzz_target_1.rs` - this is a single fuzz test + - `fuzz_target_2.rs` + +There are special considerations to note in the configuration of both the [contract's manifest] and the [fuzzing crate's manifest]. + +[contract's manifest]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing/Cargo.toml +[fuzzing crate's manifest]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing/fuzz/Cargo.toml + +Within the contract's manifest one must specificy the crate type as both "cdylib" and "rlib": + +```toml +[package] +name = "soroban-fuzzing-contract" +version = "0.0.0" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +testutils = [] +``` + +In most examples, a Soroban contract will only be a "cdylib", a Rust crate that is compiled to a dynamically loadable wasm module. For fuzzing though, the fuzzing crate needs to be able to link to the contract crate as a Rust library, an "rlib". + +:::note + +Note that cargo has a [feature/bug that inhibits LTO][lto] of cdylibs when a crate is both a "cdylib" and "rlib". This can be worked around by building the contract with either `soroban contract build` or `cargo rustc --crate-type cdylib` instead of the typical `cargo build`. + +::: + +[lto]: https://github.com/stellar/soroban-docs/pull/476 + +The contract crate must also provide the "testutils" feature. When "testutils" is activated, the Soroban SDK's [`contracttype`] macro emits additional code needed for running fuzz tests. + +[`contracttype`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/attr.contracttype.html + +Within the fuzzing crate's manifest one must turn on the "testutils" features in both the contract crate and the `soroban-sdk` crate: + +```toml +[package] +name = "soroban-fuzzing-contract-fuzzer" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +soroban-sdk = { version = "20.0.0", features = ["testutils"] } + +[dependencies.soroban-fuzzing-contract] +path = ".." +features = ["testutils"] +``` + +## A Simple Fuzz Test + +First let's look at [`fuzz_target_1.rs`]. This fuzz test does two things: it first deposits an arbitrary amount, then it claims an arbitrary amount. + +[`fuzz_target_1.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs + +Again, you can run this fuzzer from the `soroban-examples/fuzzing` directory with the following command: + +```bash +cargo +nightly fuzz run fuzz_target_1 +``` + +The entry point and setup code for Soroban contract fuzz tests will typically look like: + +```rust +#[derive(Arbitrary, Debug)] +struct Input { + deposit_amount: i128, + claim_amount: i128, +} + +fuzz_target!(|input: Input| { + let env = Env::default(); + + env.mock_all_auths(); + + env.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 1, + sequence_number: 10, + network_id: Default::default(), + base_reserve: 10, + }); + + // Turn off the CPU/memory budget for testing. + env.budget().reset_unlimited(); + + // ... do fuzzing here ... +} +``` + +Instead of a `main` function, `cargo-fuzz` uses a special entry point defined by the [`fuzz_target!`] macro. This macro accepts a Rust closure that accepts `input`, any Rust type that implements the [`Arbitrary`] trait. Here we have defined a struct, `Input`, that derives `Arbitrary`. + +[`fuzz_target!`]: https://docs.rs/libfuzzer-sys/latest/libfuzzer_sys/macro.fuzz_target.html +[`arbitrary`]: https://docs.rs/arbitrary/latest/arbitrary/trait.Arbitrary.html + +`cargo-fuzz` will be responsible for generating `input` and repeatedly calling this closure. + +To test a Soroban contract, we must set up an [`Env`]. Note that we have disabled the CPU and memory budget: this will allow us to fuzz arbitrarily complex code paths without worrying about running out of budget; we can assume that running out of budget during a transaction always correctly fails, canceling the transaction; it is not something we need to fuzz. + +[`env`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html + +Refer to the [`fuzz_target_1.rs`] source code for additional setup for this contract. + +This fuzzer performs two steps: deposit, then claim: + +```rust + // Deposit, then assert invariants. + { + let _ = fuzz_catch_panic(|| { + timelock_client.deposit( + &depositor_address, + &token_contract_id, + &input.deposit_amount, + &vec![ + &env, + claimant_address.clone(), + ], + &TimeBound { + kind: TimeBoundKind::Before, + timestamp: 123456, + }, + ); + }); + + assert_invariants( + &env, + &timelock_contract_id, + &token_client, + &input + ); + } + + // Claim, then assert invariants. + { + let _ = fuzz_catch_panic(|| { + timelock_client.claim( + &claimant_address, + &input.claim_amount, + ); + }); + + assert_invariants( + &env, + &timelock_contract_id, + &token_client, + &input + ); + } +``` + +There are a number of potential strategies for writing fuzz tests. The strategy in this test is to make arbitrary, possibly weird and unrealistic, calls to the contract, disregarding whether those calls succeed or fail, and then to make assertions about the state of the contract. + +Because there are many potential failure cases for any given contract call, we don't want to write a fuzz test by attempting to interpret the success or failure of any given call: that path leads to duplicating the contract's logic within the fuzz test. Instead we just want to ensure that, regardless of what happened during execution, the contract is never left in an invalid state. + +Notice the use of the [`fuzz_catch_panic`] function to invoke the contract: This is a special function in the Soroban SDK for intercepting panics in a way that works with `cargo-fuzz`, and is needed to call contract functions that might fail. Without `fuzz_catch_panic` a panic from within a contract will immediately cause the fuzz test to fail, but in most cases a panic within a contract does not indicate a bug - it is simply how a Soroban contract cancels a transaction. `fuzz_catch_panic` returns a `Result`, but here we discard it. + +[`fuzz_catch_panic`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/arbitrary/fn.fuzz_catch_panic.html + +Finally, the `assert_invariants` function is where we make any assertions we can about the state of the contract: + +```rust +/// Directly inspect the contract state and make assertions about it. +fn assert_invariants( + env: &Env, + timelock_contract_id: &Address, + token_client: &TokenClient, + input: &Input, +) { + // Configure the environment to access the timelock contract's storage. + env.as_contract(timelock_contract_id, || { + let storage = env.storage(); + + // Get the two datums owned by the timelock contract. + let is_initialized = storage.has(&DataKey::Init); + let claimable_balance = storage.get::<_, ClaimableBalance>(&DataKey::Balance); + + // Call the token client to get the balance held in the timelock contract. + // This consumes contract execution budget. + let actual_token_balance = token_client.balance(timelock_contract_id); + + // There can only be a claimaible balance after the contract is initialized, + // but once the balance is claimed there is no balance, + // but the contract remains initialized. + // This is a truth table of valid states. + assert!(match (is_initialized, claimable_balance.is_some()) { + (false, false) => true, + (false, true) => false, + (true, true) => true, + (true, false) => true, + }); + + assert!(actual_token_balance >= 0); + + if let Some(claimable_balance) = claimable_balance { + let claimable_balance = claimable_balance.expect("balance"); + + assert!(claimable_balance.amount > 0); + assert!(claimable_balance.amount <= input.deposit_amount); + assert_eq!(claimable_balance.amount, actual_token_balance); + + assert!(claimable_balance.claimants.len() > 0); + } + }); +} +``` + +## Interpreting `cargo-fuzz` Output + +If you run `cargo-fuzz` with `fuzz_target_1`, from inside the `soroban-examples/fuzzing` directory, you will see output similar to: + +``` +$ cargo +nightly fuzz run fuzz_target_1 + Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) + Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) + Finished release [optimized + debuginfo] target(s) in 25.18s + Finished release [optimized + debuginfo] target(s) in 0.08s + Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 -artifact_prefix=/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/ /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1` +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 1384064486 +INFO: Loaded 1 modules (1122058 inline 8-bit counters): 1122058 [0x561f6ecd4fc0, 0x561f6ede6eca), +INFO: Loaded 1 PC tables (1122058 PCs): 1122058 [0x561f6ede6ed0,0x561f6ff05f70), +INFO: 173 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: seed corpus: files: 173 min: 32b max: 61b total: 6039b rss: 83Mb +#4 pulse cov: 4848 ft: 10214 corp: 1/32b exec/s: 2 rss: 313Mb +#8 pulse cov: 8507 ft: 11743 corp: 4/128b exec/s: 4 rss: 315Mb +#16 pulse cov: 8512 ft: 12393 corp: 10/320b exec/s: 8 rss: 319Mb +thread '' panicked at 'assertion failed: claimable_balance.amount > 0', fuzz_targets/fuzz_target_1.rs:130:13 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +==6102== ERROR: libFuzzer: deadly signal + #0 0x561f6ae3a431 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1c80431) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #1 0x561f6e3855b0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51cb5b0) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #2 0x561f6e35c08a (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51a208a) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #3 0x7fce05f5e08f (/lib/x86_64-linux-gnu/libc.so.6+0x4308f) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + #4 0x7fce05f5e00a (/lib/x86_64-linux-gnu/libc.so.6+0x4300a) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + #5 0x7fce05f3d858 (/lib/x86_64-linux-gnu/libc.so.6+0x22858) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + ... + #27 0x561f6e3847b9 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x51ca7b9) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #28 0x561f6ad98346 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde346) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #29 0x7fce05f3f082 (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + #30 0x561f6ad9837d (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + +NOTE: libFuzzer has rudimentary signal handlers. + Combine libFuzzer with AddressSanitizer or similar for better crash reports. +SUMMARY: libFuzzer: deadly signal +MS: 0 ; base unit: 0000000000000000000000000000000000000000 +0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x5d,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff,0x5f,0x5f,0x52,0xff, +\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000]\000\000\000\000\000\000\000\000\377__R\377 +artifact_prefix='/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/'; Test unit written to /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 +Base64: AAAAAAAAAAAAAAAAAAAAAAAAXQAAAAAAAAAA/19fUv8= + +──────────────────────────────────────────────────────────────────────────────── + +Failing input: + + fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 + +Output of `std::fmt::Debug`: + + Input { + deposit_amount: 0, + claim_amount: -901525218878596739118967460911579136, + } + +Reproduce with: + + cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 + +Minimize test case with: + + cargo fuzz tmin fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 + +──────────────────────────────────────────────────────────────────────────────── + +Error: Fuzz target exited with exit status: 77 +``` + +This is a fuzzing failure, indicating a bug in either the fuzzer or the program. The details will be different. + +Here is the same output, with less important lines trimmed: + +``` +thread '' panicked at 'assertion failed: claimable_balance.amount > 0', fuzz_targets/fuzz_target_1.rs:130:13 +... +Failing input: + + fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 + +Output of `std::fmt::Debug`: + + Input { + deposit_amount: 0, + claim_amount: -901525218878596739118967460911579136, + } + +Reproduce with: + + cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 + +Minimize test case with: + + cargo fuzz tmin fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 +``` + +The first line here is printed by our Rust program, and indicates exactly where the fuzzer panicked. The later lines indicate how to reproduce this failing case. + +The first thing to do when you get a fuzzing failure is copy the command to reproduce the failure, so that you can use it to debug: + +```bash +cargo +nightly fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-04704b1542f61a21a4649e39023ec57ff502f627 +``` + +Notice though that we need to tell `cargo` to use the nightly toolchain with the `+nightly` flag, something that `cargo-fuzz` doesn't print in its version of the command. + +Another thing to notice is that by default, `cargo-fuzz` / `libfuzzer` does not print names of functions in its output, as in the stack trace: + +``` +==6102== ERROR: libFuzzer: deadly signal + #0 0x561f6ae3a431 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1c80431) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + ... + #28 0x561f6ad98346 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde346) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) + #29 0x7fce05f3f082 (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + #30 0x561f6ad9837d (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) +``` + +Depending on how your system is set up, you may or may not have this problem. In order to print stack traces, `libfuzzer` needs the `llvm-symbolizer` program. On Ubuntu-based systems this can be installed with the `llvm-dev` package: + +```bash +$ sudo apt install llvm-dev +``` + +After which `libfuzzer` will print demangled function names instead of addresses: + +``` +==6323== ERROR: libFuzzer: deadly signal + #0 0x557c9da6a431 in __sanitizer_print_stack_trace /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_stack.cpp:87:3 + #1 0x557ca0fb55b0 in fuzzer::PrintStackTrace() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerUtil.cpp:210:38 + #2 0x557ca0f8c08a in fuzzer::Fuzzer::CrashCallback() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:233:18 + #3 0x557ca0f8c08a in fuzzer::Fuzzer::CrashCallback() /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:228:6 + #4 0x7ff19e84d08f (/lib/x86_64-linux-gnu/libc.so.6+0x4308f) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee) + #5 0x7ff19e84d00a in __libc_signal_restore_set /build/glibc-SzIz7B/glibc-2.31/signal/../sysdeps/unix/sysv/linux/internal-signals.h:86:3 + #6 0x7ff19e84d00a in raise /build/glibc-SzIz7B/glibc-2.31/signal/../sysdeps/unix/sysv/linux/raise.c:48:3 + #7 0x7ff19e82c858 in abort /build/glibc-SzIz7B/glibc-2.31/stdlib/abort.c:79:7 + ... + #23 0x557c9daee89a in fuzz_target_1::assert_invariants::hd6d4f9549b01c31c /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs:103:5 + #24 0x557c9daee89a in fuzz_target_1::_::run::hac1117cb3dfecb2b /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/fuzz_targets/fuzz_target_1.rs:69:9 + #25 0x557c9daecea6 in rust_fuzzer_test_input /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/src/lib.rs:297:60 + ... + #37 0x557c9d9c8346 in main /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libfuzzer-sys-0.4.5/libfuzzer/FuzzerMain.cpp:20:30 + #38 0x7ff19e82e082 in __libc_start_main /build/glibc-SzIz7B/glibc-2.31/csu/../csu/libc-start.c:308:16 + #39 0x557c9d9c837d in _start (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1+0x1bde37d) (BuildId: 6a95a932984a405ebab8171dddc9f812fdf16846) +``` + +To continue, our program has a bug that should be easy to fix by inspecting the error and making a slight modification to the source. + +Once the bug is fixed, the fuzzer will run continuously, producing output that looks like + +``` +$ cargo +nightly fuzz run fuzz_target_1 + Compiling soroban-fuzzing-contract v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing) + Compiling soroban-fuzzing-contract-fuzzer v0.0.0 (/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz) + Finished release [optimized + debuginfo] target(s) in 24.91s + Finished release [optimized + debuginfo] target(s) in 0.08s + Running `fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_target_1 -artifact_prefix=/home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/artifacts/fuzz_target_1/ /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1` +INFO: Running with entropic power schedule (0xFF, 100). +INFO: Seed: 1619748028 +INFO: Loaded 1 modules (1122061 inline 8-bit counters): 1122061 [0x5647a55b9080, 0x5647a56caf8d), +INFO: Loaded 1 PC tables (1122061 PCs): 1122061 [0x5647a56caf90,0x5647a67ea060), +INFO: 173 files found in /home/azureuser/data/stellar/soroban-examples/fuzzing/fuzz/corpus/fuzz_target_1 +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes +INFO: seed corpus: files: 173 min: 32b max: 61b total: 6039b rss: 85Mb +#2 pulse ft: 8067 exec/s: 1 rss: 312Mb +#4 pulse cov: 8068 ft: 10709 corp: 1/32b exec/s: 2 rss: 315Mb +#8 pulse cov: 8476 ft: 11498 corp: 5/160b exec/s: 4 rss: 317Mb +#16 pulse cov: 8512 ft: 12362 corp: 9/288b exec/s: 8 rss: 320Mb +#32 pulse cov: 8516 ft: 13290 corp: 19/608b exec/s: 10 rss: 326Mb +#64 pulse cov: 8516 ft: 13311 corp: 27/864b exec/s: 21 rss: 340Mb +#128 pulse cov: 8540 ft: 13536 corp: 37/1196b exec/s: 25 rss: 365Mb +#175 INITED cov: 8540 ft: 13580 corp: 42/1387b exec/s: 29 rss: 382Mb +#177 NEW cov: 8545 ft: 13821 corp: 43/1419b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeASCIIInt- +#178 NEW cov: 8545 ft: 13824 corp: 44/1451b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeBinInt- +#229 NEW cov: 8545 ft: 13826 corp: 45/1483b lim: 48 exec/s: 38 rss: 401Mb L: 32/48 MS: 1 ChangeByte- +#256 pulse cov: 8545 ft: 13826 corp: 45/1483b lim: 48 exec/s: 36 rss: 410Mb +#361 NEW cov: 8545 ft: 13830 corp: 46/1521b lim: 48 exec/s: 40 rss: 451Mb L: 38/48 MS: 5 ShuffleBytes-CMP-EraseBytes-CopyPart-ChangeBinInt- DE: "\005\000\000\000"- + NEW_FUNC[1/1]: 0x5647a2964640 in rand::rngs::adapter::reseeding::ReseedingCore$LT$R$C$Rsdr$GT$::reseed_and_generate::ha760ded93293681c /home/azureuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/rand-0.7.3/src/rngs/adapter/reseeding.rs:235 +#368 NEW cov: 8557 ft: 13842 corp: 47/1566b lim: 48 exec/s: 40 rss: 454Mb L: 45/48 MS: 2 CrossOver-InsertRepeatedBytes- +#512 pulse cov: 8557 ft: 13842 corp: 47/1566b lim: 48 exec/s: 46 rss: 502Mb +#850 NEW cov: 8557 ft: 13843 corp: 48/1610b lim: 48 exec/s: 53 rss: 591Mb L: 44/48 MS: 2 CopyPart-ChangeBit- +#1024 pulse cov: 8557 ft: 13843 corp: 48/1610b lim: 48 exec/s: 56 rss: 645Mb +#1796 NEW cov: 8557 ft: 13863 corp: 49/1642b lim: 53 exec/s: 71 rss: 669Mb L: 32/48 MS: 1 ChangeBinInt- +#1913 NEW cov: 8557 ft: 13864 corp: 50/1675b lim: 53 exec/s: 73 rss: 669Mb L: 33/48 MS: 2 ShuffleBytes-InsertByte- +#3749 REDUCE cov: 8557 ft: 13864 corp: 50/1670b lim: 68 exec/s: 98 rss: 669Mb L: 39/48 MS: 1 EraseBytes- +... +``` + +And this output will continue until the fuzzer is killed with `Ctrl-C`. + +Next, let's look at a single line of fuzzer output: + +```bash +#177 NEW cov: 8545 ft: 13821 corp: 43/1419b lim: 48 exec/s: 29 rss: 384Mb L: 32/48 MS: 1 ChangeASCIIInt- +``` + +The most important column here is `cov`. This is a cumulative measure of branches covered by the fuzzer. When this number stops increasing the fuzzer has probably explored as much of the program as it can. The other columns are described in the [`libfuzzer` documentation][lfout]. + +[lfout]: https://llvm.org/docs/LibFuzzer.html#output + +Finally, lets look at this warning: + +```bash +INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes. +``` + +By default, `libfuzzer` only generates input up to 4096 bytes. In a lot of cases, this is probably reasonable, but `cargo-fuzz` can increase the `max_len` by appending the argument after `--`: + +```bash +$ cargo +nightly fuzz run fuzz_target_1 -- -max_len=20000 +``` + +All the options to libfuzzer can be listed with + +```bash +$ cargo +nightly fuzz run fuzz_target_1 -- -help=1 +``` + +See the [`libfuzzer` documentation] for more. + +[`libfuzzer` documentation]: https://llvm.org/docs/LibFuzzer.html#output + +## Accepting Soroban Types as Input with the `SorobanArbitrary` Trait + +Inputs to the `fuzz_target!` macro must implement the [`Arbitrary`] trait, which accepts bytes from the fuzzer driver and converts them to Rust values. Soroban types though are managed by the host environment, and so must be created from an [`Env`] value, which is not available to the fuzzer driver. The [`SorobanArbitrary`] trait, implemented for all Soroban contract types, exists to bridge this gap: it defines a _prototype_ pattern whereby the `fuzz_target` macro creates prototype values that the fuzz program can convert to contract values with the standard soroban conversion traits, [`FromVal`] or [`IntoVal`]. + +[`sorobanarbitrary`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/arbitrary/trait.SorobanArbitrary.html +[`fromval`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/trait.FromVal.html +[`intoval`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/trait.IntoVal.html + +The types of prototypes are identified by the associated type, `SorobanArbitrary::Prototype`: + +```rust +pub trait SorobanArbitrary: + TryFromVal + IntoVal + TryFromVal +{ + type Prototype: for <'a> Arbitrary<'a>; +} +``` + +Types that implement `SorobanArbitrary` include: + +- `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, [`I256`], [`U256`], `()`, and `bool`, +- [`Error`], +- [`Bytes`], [`BytesN`], [`Vec`], [`Map`], +- [`Address`], [`Symbol`], +- [`Val`], + +[`i256`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.I256.html +[`u256`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.U256.html +[`error`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Error.html +[`bytes`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Bytes.html +[`bytesn`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.BytesN.html +[`vec`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Vec.html +[`map`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Map.html +[`address`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Address.html +[`symbol`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Symbol.html +[`val`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Val.html + +All user-defined contract types, those with the [`contracttype`] attribute, automatically derive `SorobanArbitrary`. Note that `SorobanArbitrary` is only derived when the "testutils" Cargo feature is active. This implies that, in general, to make a Soroban contract fuzzable, the contract crate must define a "testutils" Cargo feature, that feature should turn on the "soroban-sdk/testutils" feature, and the fuzz test, which is its own crate, must turn that feature on. + +## A More Complex Fuzz Test + +The [`fuzz_target_2.rs`] example, demonstrates the use of `SorobanArbitrary`, the advancement of time, and more advanced fuzzing techniques. + +[`fuzz_target_2.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing/fuzz/fuzz_targets/fuzz_target_2.rs + +This fuzz test takes a much more complex input, where some of the values are user-defined types exported from the contract under test. This test is structured as a simple interpreter, where the fuzzing harness provides arbitrarily-generated "steps", where each step is either a `deposit` command or a `claim` command. The test then treats each of these steps as a separate transaction: it maintains a snapshot of the blockchain state, and for each step creates a fresh environment in which to execute the contract call, simulating the advancement of time between each step. As in the previous example, assertions are made after each step. + +The input to the fuzzer looks, in part, like: + +```rust +#[derive(Arbitrary, Debug)] +struct Input { + addresses: [
::Prototype; NUM_ADDRESSES], + #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=i128::MAX))] + token_mint: i128, + steps: RustVec, +} + +#[derive(Arbitrary, Debug)] +struct Step { + #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(1..=u64::MAX))] + advance_time: u64, + command: Command, // `Command` not shown here - see the full source. +} +``` + +This shows how to use the `SorobanArbitrary::Prototype` associated type to define inputs to the fuzzer. A Soroban [`Address`] can only be created with an [`Env`], so cannot be generated directly by the `Arbitrary` trait. Instead we use the fully-qualified name of the `Address` prototype, `
::Prototype`, to ask for `Address`'s prototype instead. Then when our fuzzer needs the `Address` we instantiate it with the [`FromVal`] trait: + +```rust +let depositor_address = Address::from_val(&env, &input.addresses[cmd.depositor_index]); +``` + +--- + +The contract we are fuzzing is a _timelock_ contract, where calculation of time is crucial for correctness. So our testing must account for the advancement of time. + +The contract defines a `TimeBound` type and accepts it in the `deposit` method: + +```rust +#[derive(Clone, Debug)] +#[contracttype] +pub struct TimeBound { + pub kind: TimeBoundKind, + pub timestamp: u64, +} + +#[contractimpl] +impl ClaimableBalanceContract { + pub fn deposit( + env: Env, + from: Address, + token: Address, + amount: i128, + claimants: Vec
, + time_bound: TimeBound, + ) { + ... + } +} +``` + +In our fuzzer, one of the possible commands issued each step is a `DepositCommand`: + +```rust +#[derive(Arbitrary, Debug)] +struct DepositCommand { + #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=NUM_ADDRESSES - 1))] + depositor_index: usize, + amount: i128, + // This is an ugly way to get a vector of integers in range + #[arbitrary(with = |u: &mut Unstructured| { + u.arbitrary_len::().map(|len| { + (0..len).map(|_| { + u.int_in_range(0..=NUM_ADDRESSES - 1) + }).collect::, _>>() + }).and_then(|inner_result| inner_result) + })] + claimant_indexes: RustVec, + time_bound: ::Prototype, +} +``` + +Notice that this command again uses the `SorobanArbitrary::Prototype` associated type to accept a `TimeBound` as input. + +To advance time we maintain a [`LedgerSnapshot`], defined in the [`soroban-ledger-snapshot`] crate. For each step we call [`Env::from_snapshot`] to create a fresh environment to execute the step, then [`Env::to_snapshot`] to create a new snapshot to use in the following step. + +[`ledgersnapshot`]: https://docs.rs/soroban-ledger-snapshot/latest/soroban_ledger_snapshot/struct.LedgerSnapshot.html +[`soroban-ledger-snapshot`]: https://docs.rs/soroban-ledger-snapshot +[`env::from_snapshot`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html#method.from_snapshot +[`env::to_snapshot`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html#method.to_snapshot + +Here is a simplified outline of how this works. See the full source code for details. + +```rust +let init_snapshot = { + let init_ledger = LedgerInfo { + timestamp: 12345, + protocol_version: 1, + sequence_number: 10, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: u32::MAX, + min_persistent_entry_ttl: u32::MAX, + }; + + LedgerSnapshot::from(init_ledger, None) +}; + +let mut prev_env = Env::from_snapshot(init_snapshot); + +for step in &config.input.steps { + // Advance time and create a new env from snapshot. + let curr_env = { + let mut snapshot = prev_env.to_snapshot(); + snapshot.sequence_number += 1; + snapshot.timestamp = snapshot.timestamp.saturating_add(step.advance_time); + let env = Env::from_snapshot(snapshot); + env.budget().reset_unlimited(); + env + }; + + step.command.exec(&config, &curr_env); + prev_env = curr_env; +} +``` + +## Converting a Fuzz Test to a Property Test + +In addition to fuzz testing, Soroban supports property testing in the style of quickcheck, by using the [`proptest`] and [`proptest-arbitrary-interop`] crates in conjunction with the `SorobanArbitrary` trait. + +Property tests are similar to fuzz tests in that they generate randomized input. Property tests though do not instrument their test cases or mutate their input based on feedback from previous tests. Thus they are a weaker form of test. + +The great benefit of property tests though is that they can be included in standard Rust test suites and require no extra tooling to execute. One might take advantage of this by interactively fuzzing to discover deep bugs, then convert fuzz tests to property tests to help prevent regressions. + +The [`proptest.rs`] file is a translation of `fuzz_target_1.rs` to a property test. + +[`proptest.rs`]: https://github.com/stellar/soroban-examples/tree/v20.0.0/fuzzing/src/proptest.rs diff --git a/docs/smart-contracts/example-contracts/liquidity-pool.mdx b/docs/smart-contracts/example-contracts/liquidity-pool.mdx new file mode 100644 index 000000000..78cd28791 --- /dev/null +++ b/docs/smart-contracts/example-contracts/liquidity-pool.mdx @@ -0,0 +1,900 @@ +--- +title: Liquidity Pool +description: Write a constant-product liquidity pool contract. +sidebar_position: 130 +--- + + + Write a constant-product liquidity pool contract. + + + + + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +The [liquidity pool example] demonstrates how to write a constant product liquidity pool contract. A liquidity pool is an automated way to add liquidity for a set of tokens that will facilitate asset conversion between them. Users can deposit some amount of each token into the pool, receiving a proportional number of "token shares." The user will then receive a portion of the accrued conversion fees when they ultimately "trade in" their token shares to receive their original tokens back. + +Soroban liquidity pools are exclusive to Soroban and cannot interact with built-in Stellar AMM liquidity pools. + +:::caution + +Implementing a custom liquidity pool should be done cautiously. User funds are involved, so great care should be taken to ensure safety and transparency. The example here should _not_ be considered a ready-to-go contract. Please use it as a reference only. + +The Stellar network already has liquidity pool functionality built right in to the core protocol. [Learn more here.](https://developers.stellar.org/docs/encyclopedia/liquidity-on-stellar-sdex-liquidity-pools) + +::: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 +[liquidity pool example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/liquidity_pool +[source code]: https://github.com/stellar/soroban-examples/blob/v20.0.0/liquidity_pool/src/lib.rs#L143 + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +```bash +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `liquidity_pool` directory, and use `cargo test`. + +```bash +cd liquidity_pool +cargo test +``` + +You should see the output: + +```bash +running 1 test +test test::test ... ok +``` + +[setup]: ../getting-started/setup.mdx + +## Code + +:::info + +Since our liquidity pool will be issuing its own token to establish the nuber of shares in the pool the address has, we have created a `token.rs` module in this project to hold the logic controlling the token contract for those shares. + +::: + + + + +```rust title=liquidity_pool/src/lib.rs +#![no_std] + +mod test; +mod token; + +use num_integer::Roots; +use soroban_sdk::{ + contract, contractimpl, contractmeta, Address, BytesN, ConversionError, Env, IntoVal, + TryFromVal, Val, +}; +use token::create_contract; + +#[derive(Clone, Copy)] +#[repr(u32)] +pub enum DataKey { + TokenA = 0, + TokenB = 1, + TokenShare = 2, + TotalShares = 3, + ReserveA = 4, + ReserveB = 5, +} + +impl TryFromVal for Val { + type Error = ConversionError; + + fn try_from_val(_env: &Env, v: &DataKey) -> Result { + Ok((*v as u32).into()) + } +} + +fn get_token_a(e: &Env) -> Address { + e.storage().instance().get(&DataKey::TokenA).unwrap() +} + +fn get_token_b(e: &Env) -> Address { + e.storage().instance().get(&DataKey::TokenB).unwrap() +} + +fn get_token_share(e: &Env) -> Address { + e.storage().instance().get(&DataKey::TokenShare).unwrap() +} + +fn get_total_shares(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::TotalShares).unwrap() +} + +fn get_reserve_a(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::ReserveA).unwrap() +} + +fn get_reserve_b(e: &Env) -> i128 { + e.storage().instance().get(&DataKey::ReserveB).unwrap() +} + +fn get_balance(e: &Env, contract: Address) -> i128 { + token::Client::new(e, &contract).balance(&e.current_contract_address()) +} + +fn get_balance_a(e: &Env) -> i128 { + get_balance(e, get_token_a(e)) +} + +fn get_balance_b(e: &Env) -> i128 { + get_balance(e, get_token_b(e)) +} + +fn get_balance_shares(e: &Env) -> i128 { + get_balance(e, get_token_share(e)) +} + +fn put_token_a(e: &Env, contract: Address) { + e.storage().instance().set(&DataKey::TokenA, &contract); +} + +fn put_token_b(e: &Env, contract: Address) { + e.storage().instance().set(&DataKey::TokenB, &contract); +} + +fn put_token_share(e: &Env, contract: Address) { + e.storage().instance().set(&DataKey::TokenShare, &contract); +} + +fn put_total_shares(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::TotalShares, &amount) +} + +fn put_reserve_a(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::ReserveA, &amount) +} + +fn put_reserve_b(e: &Env, amount: i128) { + e.storage().instance().set(&DataKey::ReserveB, &amount) +} + +fn burn_shares(e: &Env, amount: i128) { + let total = get_total_shares(e); + let share_contract = get_token_share(e); + + token::Client::new(e, &share_contract).burn(&e.current_contract_address(), &amount); + put_total_shares(e, total - amount); +} + +fn mint_shares(e: &Env, to: Address, amount: i128) { + let total = get_total_shares(e); + let share_contract_id = get_token_share(e); + + token::Client::new(e, &share_contract_id).mint(&to, &amount); + + put_total_shares(e, total + amount); +} + +fn transfer(e: &Env, token: Address, to: Address, amount: i128) { + token::Client::new(e, &token).transfer(&e.current_contract_address(), &to, &amount); +} + +fn transfer_a(e: &Env, to: Address, amount: i128) { + transfer(e, get_token_a(e), to, amount); +} + +fn transfer_b(e: &Env, to: Address, amount: i128) { + transfer(e, get_token_b(e), to, amount); +} + +fn get_deposit_amounts( + desired_a: i128, + min_a: i128, + desired_b: i128, + min_b: i128, + reserve_a: i128, + reserve_b: i128, +) -> (i128, i128) { + if reserve_a == 0 && reserve_b == 0 { + return (desired_a, desired_b); + } + + let amount_b = desired_a * reserve_b / reserve_a; + if amount_b <= desired_b { + if amount_b < min_b { + panic!("amount_b less than min") + } + (desired_a, amount_b) + } else { + let amount_a = desired_b * reserve_a / reserve_b; + if amount_a > desired_a || desired_a < min_a { + panic!("amount_a invalid") + } + (amount_a, desired_b) + } +} + +// Metadata that is added on to the WASM custom section +contractmeta!( + key = "Description", + val = "Constant product AMM with a .3% swap fee" +); + +pub trait LiquidityPoolTrait { + // Sets the token contract addresses for this pool + fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address); + + // Returns the token contract address for the pool share token + fn share_id(e: Env) -> Address; + + // Deposits token_a and token_b. Also mints pool shares for the "to" Identifier. The amount minted + // is determined based on the difference between the reserves stored by this contract, and + // the actual balance of token_a and token_b for this contract. + fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128); + + // If "buy_a" is true, the swap will buy token_a and sell token_b. This is flipped if "buy_a" is false. + // "out" is the amount being bought, with in_max being a safety to make sure you receive at least that amount. + // swap will transfer the selling token "to" to this contract, and then the contract will transfer the buying token to "to". + fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128); + + // transfers share_amount of pool share tokens to this contract, burns all pools share tokens in this contracts, and sends the + // corresponding amount of token_a and token_b to "to". + // Returns amount of both tokens withdrawn + fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128); + + fn get_rsrvs(e: Env) -> (i128, i128); +} + +#[contract] +struct LiquidityPool; + +#[contractimpl] +impl LiquidityPoolTrait for LiquidityPool { + fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address) { + if token_a >= token_b { + panic!("token_a must be less than token_b"); + } + + let share_contract = create_contract(&e, token_wasm_hash, &token_a, &token_b); + token::Client::new(&e, &share_contract).initialize( + &e.current_contract_address(), + &7u32, + &"Pool Share Token".into_val(&e), + &"POOL".into_val(&e), + ); + + put_token_a(&e, token_a); + put_token_b(&e, token_b); + put_token_share(&e, share_contract.try_into().unwrap()); + put_total_shares(&e, 0); + put_reserve_a(&e, 0); + put_reserve_b(&e, 0); + } + + fn share_id(e: Env) -> Address { + get_token_share(&e) + } + + fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) { + // Depositor needs to authorize the deposit + to.require_auth(); + + let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e)); + + // Calculate deposit amounts + let amounts = get_deposit_amounts(desired_a, min_a, desired_b, min_b, reserve_a, reserve_b); + + let token_a_client = token::Client::new(&e, &get_token_a(&e)); + let token_b_client = token::Client::new(&e, &get_token_b(&e)); + + token_a_client.transfer(&to, &e.current_contract_address(), &amounts.0); + token_b_client.transfer(&to, &e.current_contract_address(), &amounts.1); + + // Now calculate how many new pool shares to mint + let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); + let total_shares = get_total_shares(&e); + + let zero = 0; + let new_total_shares = if reserve_a > zero && reserve_b > zero { + let shares_a = (balance_a * total_shares) / reserve_a; + let shares_b = (balance_b * total_shares) / reserve_b; + shares_a.min(shares_b) + } else { + (balance_a * balance_b).sqrt() + }; + + mint_shares(&e, to, new_total_shares - total_shares); + put_reserve_a(&e, balance_a); + put_reserve_b(&e, balance_b); + } + + fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) { + to.require_auth(); + + let (reserve_a, reserve_b) = (get_reserve_a(&e), get_reserve_b(&e)); + let (reserve_sell, reserve_buy) = if buy_a { + (reserve_b, reserve_a) + } else { + (reserve_a, reserve_b) + }; + + // First calculate how much needs to be sold to buy amount out from the pool + let n = reserve_sell * out * 1000; + let d = (reserve_buy - out) * 997; + let sell_amount = (n / d) + 1; + if sell_amount > in_max { + panic!("in amount is over max") + } + + // Transfer the amount being sold to the contract + let sell_token = if buy_a { + get_token_b(&e) + } else { + get_token_a(&e) + }; + let sell_token_client = token::Client::new(&e, &sell_token); + sell_token_client.transfer(&to, &e.current_contract_address(), &sell_amount); + + let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); + + // residue_numerator and residue_denominator are the amount that the invariant considers after + // deducting the fee, scaled up by 1000 to avoid fractions + let residue_numerator = 997; + let residue_denominator = 1000; + let zero = 0; + + let new_invariant_factor = |balance: i128, reserve: i128, out: i128| { + let delta = balance - reserve - out; + let adj_delta = if delta > zero { + residue_numerator * delta + } else { + residue_denominator * delta + }; + residue_denominator * reserve + adj_delta + }; + + let (out_a, out_b) = if buy_a { (out, 0) } else { (0, out) }; + + let new_inv_a = new_invariant_factor(balance_a, reserve_a, out_a); + let new_inv_b = new_invariant_factor(balance_b, reserve_b, out_b); + let old_inv_a = residue_denominator * reserve_a; + let old_inv_b = residue_denominator * reserve_b; + + if new_inv_a * new_inv_b < old_inv_a * old_inv_b { + panic!("constant product invariant does not hold"); + } + + if buy_a { + transfer_a(&e, to, out_a); + } else { + transfer_b(&e, to, out_b); + } + + put_reserve_a(&e, balance_a - out_a); + put_reserve_b(&e, balance_b - out_b); + } + + fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128) { + to.require_auth(); + + // First transfer the pool shares that need to be redeemed + let share_token_client = token::Client::new(&e, &get_token_share(&e)); + share_token_client.transfer(&to, &e.current_contract_address(), &share_amount); + + let (balance_a, balance_b) = (get_balance_a(&e), get_balance_b(&e)); + let balance_shares = get_balance_shares(&e); + + let total_shares = get_total_shares(&e); + + // Now calculate the withdraw amounts + let out_a = (balance_a * balance_shares) / total_shares; + let out_b = (balance_b * balance_shares) / total_shares; + + if out_a < min_a || out_b (i128, i128) { + (get_reserve_a(&e), get_reserve_b(&e)) + } +} +``` + + + + +```rust title=liquidity_pool/src/token.rs +#![allow(unused)] +use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env}; + +soroban_sdk::contractimport!( + file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" +); + +pub fn create_contract( + e: &Env, + token_wasm_hash: BytesN<32>, + token_a: &Address, + token_b: &Address, +) -> Address { + let mut salt = Bytes::new(e); + salt.append(&token_a.to_xdr(e)); + salt.append(&token_b.to_xdr(e)); + let salt = e.crypto().sha256(&salt); + e.deployer() + .with_current_contract(salt) + .deploy(token_wasm_hash) +} +``` + + + + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/liquidity_pool + +## How it Works + +Every asset created on Stellar starts with zero liquidity. The same is true of tokens created on Soroban (unless a Stellar asset with existing liquidity token is "wrapped" for use in Soroban). In simple terms, "liquidity" means how much of an asset in a market is available to be bough or sold. In the "old days," you could generate liquidity in a market by creating buy/sell orders on an order book. + +Liquidity pools automate this process by substituting the orders with math. Depositors into the liquidity pool earn fees from `swap` transactions. No orders required! + +Open the `liquidity_pool/src/lib.rs` file or see the code above to follow along. + +### Initialize the Contract + +When this contract is first deployed, it could create a liquidity pool for _any_ pair of tokens available on Soroban. It must first be initialized with the following information: + +- **`token_wasm_hash`:** The contract will end up [creating its own `POOL` token] as well as [interacting with contracts for `token_a` and `token_b`]. The way this example works is by using the [`token` example contract] for both of these jobs. When our liquidity pool contract is initialized it wants us to pass the wasm hash of the **already [installed]** token contract. It will then deploy a contract that will run the WASM bytecode stored at that hash as a new token contract for the `POOL` tokens. +- **`token_a`:** The contract `Address` for an **already deployed** (or wrapped) token that will be held in reserve by the liquidity pool. +- **`token_b`:** The contract `Address` for an **already deployed** (or wrapped) token that will be held in reserve by the liquidity pool. + +Bear in mind that which token is `token_a` and which is `token_b` is **not** an arbitrary distinction. In line with the Built-in Stellar liquidity pools, this contract can only make a single liquidity pool for a given set of tokens. So, the token addresses must be provided in [lexicographical order] at the time of initialization. + +```rust title=liquidity_pool/src/lib.rs +fn initialize(e: Env, token_wasm_hash: BytesN<32>, taken_a: Address, token_b: Address) { + if token_a >= token_b { + panic!("token_a must be less than token_b"); + } + + // The initialization function also stores important information in the contract's instance storage + put_token_a(&e, token_a); + put_token_b(&e, token_b); + put_token_share(&e, share_contract.try_into().unwrap()); + put_total_shares(&e, 0); + put_reserve_a(&e, 0); + put_reserve_b(&e, 0); +} +``` + +[creating its own `pool` token]: #creating-a-custom-pool-token-for-lp-shares +[interacting with contracts for `token_a` and `token_b`]: #token-transfers-tofrom-the-lp-contract +[`token` example contract]: ./tokens.mdx +[installed]: ../getting-started/deploy-to-testnet.mdx#two-step-deployment +[lexicographical order]: https://developers.stellar.org/docs/encyclopedia/liquidity-on-stellar-sdex-liquidity-pools#liquidity-pool-participation + +### A "Constant Product" Liquidity Pool + +The _type_ of liquidity pool this example contract implements is called a "constant product" liquidity pool. While this isn't the only type of liquidity pool out there, it is the most common variety. These liquidity pools are designed to keep the _total_ value of each asset in _relative_ equilibrium. The "product" in the constant product (also called an "invariant") will change every time the liquidity pool is interacted with (deposit, withdraw, or token swaps). However, the invariant **must** only increase with every interaction. + +During a swap, what must be kept in mind is that for every withdrawal from the `token_a` side, you must "refill" the `token_b` side with a sufficient amount to keep the liquidity pool's price balanced. The math is predictable, but it is not linear. The more you take from one side, the more you must give on the opposite site _exponentially_. + +Inside the `swap` function, the math is done like this (this is a simplified version, however): + +```rust title=liquidity_pool/src/lib.rs +fn swap(e: Env, to: Address, buy_a: bool, out: i128, in_max: i128) { + // Get the current balances of both tokens in the liquidity pool + let (reserve_sell, reserve_buy) = (get_reserve_a(&e), get_reserve_b(&e)); + + // Calculate how much needs to be + let n = reserve_sell * out * 1000; + let d = (reserve_buy - out) * 997; + let sell_amount = (n / d) + 1; +} +``` + +We have much more in-depth information about how this kind of liquidity pool works is available in [Stellar Quest: Series 3, Quest 5]. This is a really useful, interactive way to learn more about how the built-in Stellar liquidity pools work. Much of the knowledge you might gain from there will easily translate to this example contract. + +[stellar quest: series 3, quest 5]: https://quest.stellar.org/learn/series/3/quest/5 + +### Interacting with Token Contracts in Another Contract + +This liquidity pool contract will operate with a total of three different Soroban tokens: + +- **`POOL`:** This token is a unique token that is given to asset depositors in exchange for their deposit. These tokens are "traded in" by the user when they withdraw some amount of their original deposit (plus any earned swap fees). This example contract implements the same [`token` example contract] for this token. +- **`token_a`** and **`token_b`**: Will be the two "reserve tokens" that users will deposit into the pool. These could be "wrapped" tokens from pre-existing Stellar assets, or they could be Soroban-native tokens. This contract doesn't really care, as long as the functions it needs from the common [Token Interface] are available in the token contract. + +[token interface]: ../tokens/token-interface.mdx + +#### Creating a Custom `POOL` Token for LP Shares + +We are utilizing the compiled `token` example contract as our asset contract for the `POOL` token. This means it follows all the conventions of the [Token Interface], and can be treated just like any other token. They could be transferred, burned, minted, etc. It also means the LP developer _could_ take advantage of the administrative features such as clawbacks, authorization, and more. + +The `token.rs` file contains a `create_contract` function that we will use to deploy this particular token contract. + +```rust title="src/token.rs" +pub fn create_contract( + e: &Env, + token_wasm_hash: BytesN<32>, + token_a: &Address, + token_b: &Address, +) -> Address { + let mut salt = Bytes::new(e); + salt.append(&token_a.to_xdr(e)); + salt.append(&token_b.to_xdr(e)); + let salt = e.crypto().sha256(&salt); + e.deployer() + .with_current_contract(salt) + .deploy(token_wasm_hash) +} +``` + +This `POOL` token contract is then created within the `initialize` function. + +```rust title=liquidity_pool/src/lib.rs +fn initialize(e: Env, token_wasm_hash: BytesN<32>, token_a: Address, token_b: Address) { + let share_contract = create_contract(&e, token_wasm_hash, &token_a, &token_b); + token::Client::new(&e, &share_contract).initialize( + &e.current_contract_address(), + &7u32, + &"Pool Share Token".into_val(&e), + &"POOL".into_val(&e), + ); +} +``` + +Then, during a `deposit`, a calculated amount of `POOL` tokens are `mint`ed to the depositing address. + +```rust title=liquidity_pool/src/lib.rs +fn mint_shares(e: &Env, to: Address, amount: i128) { + let total = get_total_shares(e); + let share_contract_id = get_token_share(e); + + token::Client::new(e, &share_contract_id).mint(&to, &amount); + + put_total_shares(e, total + amount); +} +``` + +How is that number of shares calculated, you ask? Excellent question! If it's the very first deposit (see above), it's just the square root of the product of the quantities of `token_a` and `token_b` deposited. Very simple. + +However, if there have already been deposits into the liquidity pool, and the user is just adding more tokens into the pool, there's a bit more math. However, the main point is that each depositor receives the same ratio of `POOL` tokens for their deposit as every other depositor. + +```rust title=liquidity_pool/src/lib.rs +fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) { + let zero = 0; + let new_total_shares = if reserve_a > zero && reserve_b > zero { + // Note balance_a and balance_b at this point in the function include + // the tokens the user is currently depositing, whereas reserve_a and + // reserve_b do not yet. + let shares_a = (balance_a * total_shares) / reserve_a; + let shares_b = (balance_b * total_shares) / reserve_b; + shares_a.min(shares_b) + } else { + (balance_a * balance_b).sqrt() + }; +} +``` + +#### Token Transfers to/from the LP Contract + +As we've already discussed, the liquidity pool contract will make use of the [Token Interface] available in the token contracts that were supplied as `token_a` and `token_b` arguments at the time of initialization. Throughout the rest of the contract, the liquidity pool will make use of that interface to make transfers of those tokens to/from itself. + +What's happening is that as a user deposits tokens into the pool, and the contract invokes the `transfer` function to move the tokens from the `to` address (the depositor) to be held by the contract address. `POOL` tokens are then minted to depositor (see previous section). Pretty simple, right!? + +```rust title=liquidity_pool/src/lib.rs +fn deposit(e: Env, to: Address, desired_a: i128, min_a: i128, desired_b: i128, min_b: i128) { + // Depositor needs to authorize the deposit + to.require_auth(); + + let token_a_client = token::Client::new(&e, &get_token_a(&e)); + let token_b_client = token::Client::new(&e, &get_token_b(&e)); + + token_a_client.transfer(&to, &e.current_contract_address(), &amounts.0); + token_b_client.transfer(&to, &e.current_contract_address(), &amounts.1); + + mint_shares(&e, to, new_total_shares - total_shares); +} +``` + +In contrast, when a user withdraws their deposited tokens, It's about more involved, and the following procedure happens. + +1. Some amount of the `POOL` token is transferred from the depositor to the contract address. This is a temporary way to track how many `POOL` tokens are being redeemed. The contract will not hold this balance of `POOL` for long. +2. The withdraw amounts for the reserve tokens are calculated based on the contract's current balance of `POOL` tokens. +3. The `POOL` tokens are burned now that the withdraw amounts have been calculated, and they are no longer needed. +4. The respective amounts of `token_a` and `token_b` are transferred _from_ the contract address into the `to` address (the depositor). + +```rust title=liquidity_pool/src/lib.rs +fn withdraw(e: Env, to: Address, share_amount: i128, min_a: i128, min_b: i128) -> (i128, i128) { + to.require_auth(); + + // First transfer the pool shares that need to be redeemed + let share_token_client = token::Client::new(&e, &get_token_share(&e)); + share_token_client.transfer(&to, &e.current_contract_address(), &share_amount); + + // Now calculate the withdraw amounts + let out_a = (balance_a * balance_shares) / total_shares; + let out_b = (balance_b * balance_shares) / total_shares; + + burn_shares(&e, balance_shares); + transfer_a(&e, to.clone(), out_a); + transfer_b(&e, to, out_b); +} +``` + +You'll notice that by holding the balance of `token_a` and `token_b` on the liquidity pool contract itself it makes, it very easy for us to perform any of the [Token Interface] actions inside the contract. As a bonus, any outside observer could query the balances of `token_a` or `token_b` held by the contract to verify the reserves are actually in line with the values the contract reports when its own `get_rsvs` function is invoked. + +## Tests + +Open the [`liquidity_pool/src/test.rs`] file to follow along. + +```rust title=liquidity_pool/src/test.rs +#![cfg(test)] +extern crate std; + +use crate::{token, LiquidityPoolClient}; + +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + Address, BytesN, Env, IntoVal, +}; + +fn create_token_contract<'a>(e: &Env, admin: &Address) -> token::Client<'a> { + token::Client::new(e, &e.register_stellar_asset_contract(admin.clone())) +} + +fn create_liqpool_contract<'a>( + e: &Env, + token_wasm_hash: &BytesN<32>, + token_a: &Address, + token_b: &Address, +) -> LiquidityPoolClient<'a> { + let liqpool = LiquidityPoolClient::new(e, &e.register_contract(None, crate::LiquidityPool {})); + liqpool.initialize(token_wasm_hash, token_a, token_b); + liqpool +} + +fn install_token_wasm(e: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" + ); + e.deployer().upload_contract_wasm(WASM) +} + +#[test] +fn test() { + let e = Env::default(); + e.mock_all_auths(); + + let mut admin1 = Address::random(&e); + let mut admin2 = Address::random(&e); + + let mut token1 = create_token_contract(&e, &admin1); + let mut token2 = create_token_contract(&e, &admin2); + if &token2.address < &token1.address { + std::mem::swap(&mut token1, &mut token2); + std::mem::swap(&mut admin1, &mut admin2); + } + let user1 = Address::random(&e); + let liqpool = create_liqpool_contract( + &e, + &install_token_wasm(&e), + &token1.address, + &token2.address, + ); + + let token_share = token::Client::new(&e, &liqpool.share_id()); + + token1.mint(&user1, &1000); + assert_eq!(token1.balance(&user1), 1000); + + token2.mint(&user1, &1000); + assert_eq!(token2.balance(&user1), 1000); + + liqpool.deposit(&user1, &100, &100, &100, &100); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + liqpool.address.clone(), + symbol_short!("deposit"), + (&user1, 100_i128, 100_i128, 100_i128, 100_i128).into_val(&e) + )), + sub_invocations: std::vec![ + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token1.address.clone(), + symbol_short!("transfer"), + (&user1, &liqpool.address, 100_i128).into_val(&e) + )), + sub_invocations: std::vec![] + }, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token2.address.clone(), + symbol_short!("transfer"), + (&user1, &liqpool.address, 100_i128).into_val(&e) + )), + sub_invocations: std::vec![] + } + ] + } + )] + ); + + assert_eq!(token_share.balance(&user1), 100); + assert_eq!(token_share.balance(&liqpool.address), 0); + assert_eq!(token1.balance(&user1), 900); + assert_eq!(token1.balance(&liqpool.address), 100); + assert_eq!(token2.balance(&user1), 900); + assert_eq!(token2.balance(&liqpool.address), 100); + + liqpool.swap(&user1, &false, &49, &100); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + liqpool.address.clone(), + symbol_short!("swap"), + (&user1, false, 49_i128, 100_i128).into_val(&e) + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token1.address.clone(), + symbol_short!("transfer"), + (&user1, &liqpool.address, 97_i128).into_val(&e) + )), + sub_invocations: std::vec![] + }] + } + )] + ); + + assert_eq!(token1.balance(&user1), 803); + assert_eq!(token1.balance(&liqpool.address), 197); + assert_eq!(token2.balance(&user1), 949); + assert_eq!(token2.balance(&liqpool.address), 51); + + e.budget().reset_unlimited(); + liqpool.withdraw(&user1, &100, &197, &51); + + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + liqpool.address.clone(), + symbol_short!("withdraw"), + (&user1, 100_i128, 197_i128, 51_i128).into_val(&e) + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token_share.address.clone(), + symbol_short!("transfer"), + (&user1, &liqpool.address, 100_i128).into_val(&e) + )), + sub_invocations: std::vec![] + }] + } + )] + ); + + assert_eq!(token1.balance(&user1), 1000); + assert_eq!(token2.balance(&user1), 1000); + assert_eq!(token_share.balance(&user1), 0); + assert_eq!(token1.balance(&liqpool.address), 0); + assert_eq!(token2.balance(&liqpool.address), 0); + assert_eq!(token_share.balance(&liqpool.address), 0); +} +``` + +[`liquidity_pool/src/test.rs`]: https://github.com/stellar/soroban-examples/blob/v20.0.0/liquidity_pool/src/test.rs + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust title=liquidity_pool/src/test.rs +let e = Env::default(); +``` + +We mock authentication checks in the tests, which allows the tests to proceed as if all users/addresses/contracts/etc. had successfully authenticated. + +```rust title=liquidity_pool/src/test.rs +e.mock_all_auths(); +``` + +We have abstracted into a few functions the tasks of creating token contracts, deploying a liquidity pool contract, and installing the token example WASM bytecode into our test environment. Each are then used within the test. + +```rust title=liquidity_pool/src/test.rs +fn create_token_contract<'a>(e: &Env, admin: &Address) -> token::Client<'a> { + token::Client::new(e, &e.register_stellar_asset_contract(admin.clone())) +} + +fn create_liqpool_contract<'a>( + e: &Env, + token_wasm_hash: &BytesN<32>, + token_a: &Address, + token_b: &Address, +) -> LiquidityPoolClient<'a> { + let liqpool = LiquidityPoolClient::new(e, &e.register_contract(None, crate::LiquidityPool {})); + liqpool.initialize(token_wasm_hash, token_a, token_b); + liqpool +} + +fn install_token_wasm(e: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../token/target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" + ); + e.deployer().upload_contract_wasm(WASM) +} +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `LiquidityPool`, and the client is named `LiquidityPoolClient`. + +These tests examine the "typical" use-case of a liquidity pool, ensuring that the balances, returns, etc. are appropriate at various points during the test. + +1. First, the test sets everything up with an `Env`, two admin addresses, two reserve tokens, a randomly generated address to act as the user of the liquidity pool, the liquidity pool itself, a pool token shares contract, and mints the reserve assets to the user address. +2. The user then deposits some of each asset into the liquidity pool. At this time, the following checks are done: + - appropriate authorizations for deposits and transfers exist, + - balances are checked for each token (`token_a`, `token_b`, and `POOL`) from both the user's perspective and the `liqpool` contract's perspective +3. The user performs a swap, buying `token_b` in exchange for `token_a`. The same checks as the previous step are made now, excepting the balances of `POOL`, since a swap has no effect on `POOL` tokens. +4. The user then withdraws all of the deposits it made, trading all of its `POOL` tokens in the process. The same checks are made here as were made in the `deposit` step. + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +```bash +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory: + +```bash +target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions using it. + +```bash +soroban contract invoke \ + --wasm target/wasm32-unknown-unknown/release/soroban_liquidity_pool_contract.wasm \ + --id 1 \ + -- \ + deposit \ + --to GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR \ + --desired_a 100 \ + --min_a 98 \ + --desired_be 200 \ + --min_b 196 \ +``` + +[`soroban-cli`]: https://developers.stellar.org/docs/tools/developer-tools#soroban-cli diff --git a/docs/smart-contracts/example-contracts/logging.mdx b/docs/smart-contracts/example-contracts/logging.mdx new file mode 100644 index 000000000..c28e62aba --- /dev/null +++ b/docs/smart-contracts/example-contracts/logging.mdx @@ -0,0 +1,256 @@ +--- +title: Logging +description: Debug a smart contract with logs. +sidebar_position: 40 +--- + + + Debug a smart contract with logs. + + + + + +The [logging example] demonstrates how to log for the purpose of debugging. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +Logs in contracts are only visible in tests, or when executing contracts using [`soroban-cli`]. Logs are only compiled into the contract if the `debug-assertions` Rust compiler option is enabled. + +[logging example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/hello_world + +:::tip + +Logs are not a substitute for step-through debugging. Rust tests for Soroban can be step-through debugged in your Rust-enabled IDE. See [testing] for more details. + +::: + +:::caution + +Logs are not accessible by dapps and other applications. See the [events example] for how to produce structured events. + +::: + +[testing]: ../getting-started/hello-world.mdx#testing +[events example]: events.mdx + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +[setup]: ../getting-started/setup.mdx + +``` +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `logging` directory, and use `cargo test`. + +``` +cd logging +cargo test -- --nocapture +``` + +You should see the output: + +``` +running 1 test +Hello Symbol(Dev) +test test::test ... ok +``` + +## Code + +```toml title="Cargo.toml" +[profile.release-with-logs] +inherits = "release" +debug-assertions = true +``` + +```rust title="logging/src/lib.rs" +#![no_std] +use soroban_sdk::{contractimpl, log, Env, Symbol}; + +#[contract] +pub struct Contract; + +#[contractimpl] +impl Contract { + pub fn hello(env: Env, value: Symbol) { + log!(&env, "Hello {}", value); + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/logging + +## How it Works + +The [`log!`] macro logs a string. Any logs that occur during execution are outputted to stdout in [`soroban-cli`] and available for tests to assert on or print. + +Logs are only outputted if the contract is built with the `debug-assertions` compiler option enabled. This makes them efficient to leave in code permanently since a regular `release` build will omit them. + +Logs are only recorded in Soroban environments that have logging enabled. The only Soroban environments where logging is enabled is in Rust tests, and in the [`soroban-cli`]. + +Open the files above to follow along. + +### `Cargo.toml` Profile + +Logs are only outputted if the contract is built with the `debug-assertions` compiler option enabled. + +The `test` profile that is activated when running `cargo test` has `debug-assertions` enabled, so when running tests logs are enabled by default. + +A new `release-with-logs` profile is added to `Cargo.toml` that inherits from the `release` profile, and enables `debug-assertions`. It can be used to build a `.wasm` file that has logs enabled. + +```toml +[profile.release-with-logs] +inherits = "release" +debug-assertions = true +``` + +To build without logs use the `--release` or `--profile release` option. + +To build with logs use the `--profile release-with-logs` option. + +### Using the `log!` Macro + +The [`log!`] macro builds a string from the format string, and a list of arguments. Arguments are substituted wherever the `{}` value appears in the format string. + +```rust +log!(&env, "Hello {}", value); +``` + +The above log will render as follows if `value` is a `Symbol` containing `"Dev"`. + +``` +Hello Symbol(Dev) +``` + +:::caution + +The values outputted are currently relatively limited. While primitive values like `u32`, `u64`, `bool`, and `Symbol`s will render clearly in the log output, `Bytes`, `Vec`, `Map`, and custom types will render only their handle number. Logging capabilities are in early development. + +::: + +## Tests + +Open the `logging/src/test.rs` file to follow along. + +```rust title="logging/src/test.rs" +extern crate std; + +#[test] +fn test() { + let env = Env::default(); + let contract_id = env.register_contract(None, Contract); + let client = ContractClient::new(&env, &contract_id); + + client.hello(&symbol_short!("Dev")); + + let logs = env.logs().all(); + assert_eq!(logs, std::vec!["Hello Symbol(Dev)"]); + std::println!("{}", logs.join("\n")); +} +``` + +The `std` crate, which contains the Rust standard library, is imported so that the test can use the `std::vec!` and `std::println!` macros. Since contracts are required to use `#![no_std]`, tests in contracts must manually import `std` to use std functionality like printing to stdout. + +```rust +extern crate std; +``` + +In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let env = Env::default(); +``` + +The contract is registered with the environment using the contract type. + +```rust +let contract_id = env.register_contract(None, HelloContract); +``` + +All public functions within an `impl` block that is annotated with the `#[contractimpl]` attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract the contract type is `HelloContract`, and the client is named `HelloContractClient`. + +```rust +let client = HelloContractClient::new(&env, &contract_id); +let words = client.hello(&symbol_short!("Dev")); +``` + +Logs are available in tests via the environment. + +```rust +let logs = env.logs().all(); +``` + +They can asserted on like any other value. + +```rust +assert_eq!(logs, std::vec!["Hello Symbol(Dev)"]); +``` + +They can be printed to stdout. + +```rust +std::println!("{}", logs.join("\n")); +``` + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +### Without Logs + +To build the contract without logs, use the `--release` option. + +```sh +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory, in the `release` subdirectory: + +``` +target/wasm32-unknown-unknown/release/soroban_logging_contract.wasm +``` + +### With Logs + +To build the contract with logs, use the `--profile release-with-logs` option. + +```sh +soroban contract build --profile release-with-logs +``` + +A `.wasm` file should be outputted in the `target` directory, in the `release-with-logs` subdirectory: + +``` +target/wasm32-unknown-unknown/release-with-logs/soroban_logging_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions in the using it. Specify the `-v` option to enable verbose logs. + +```sh +soroban -v contract invoke \ + --wasm target/wasm32-unknown-unknown/release-with-logs/soroban_logging_contract.wasm \ + --id 1 \ + -- \ + hello \ + --value friend +``` + +The output should include the following line. + +``` +soroban_cli::log::event::contract_log: log="Hello Symbol(me)" +``` + +[`log!`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/macro.log.html +[`soroban-cli`]: ../getting-started/setup.mdx#install-the-soroban-cli diff --git a/docs/smart-contracts/example-contracts/single-offer-sale.mdx b/docs/smart-contracts/example-contracts/single-offer-sale.mdx new file mode 100644 index 000000000..dcbb85fe2 --- /dev/null +++ b/docs/smart-contracts/example-contracts/single-offer-sale.mdx @@ -0,0 +1,27 @@ +--- +title: Single Offer Sale +description: Make a standing offer to sell a token in exchange for another token. +sidebar_position: 120 +--- + + + + Make a standing offer to sell a token in exchange for another token. + + + + + + +The [single offer sale example] demonstrates how to write a contract that allows a seller to set up an offer to sell token A for token B to multiple buyers. The comments in the [source code] explain how the contract should be used. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +[single offer sale example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/single_offer +[source code]: https://github.com/stellar/soroban-examples/blob/v20.0.0/single_offer/src/lib.rs diff --git a/docs/smart-contracts/example-contracts/timelock.mdx b/docs/smart-contracts/example-contracts/timelock.mdx new file mode 100644 index 000000000..04db7bc54 --- /dev/null +++ b/docs/smart-contracts/example-contracts/timelock.mdx @@ -0,0 +1,29 @@ +--- +title: Timelock +description: Lockup some token to be claimed by another user under set conditions. +sidebar_position: 110 +--- + + + + Lockup some token to be claimed by another user under set conditions + + + + + + +The [timelock example] demonstrates how to write a timelock and implements a greatly simplified claimable balance similar to the [claimable balance] feature available on Stellar. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] [oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 + +The contract accepts deposits of an amount of a token, and allows other users to claim it before or after a time point. + +[timelock example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/timelock +[claimable balance]: https://developers.stellar.org/docs/glossary/claimable-balance diff --git a/docs/smart-contracts/example-contracts/tokens.mdx b/docs/smart-contracts/example-contracts/tokens.mdx new file mode 100644 index 000000000..85f9aea71 --- /dev/null +++ b/docs/smart-contracts/example-contracts/tokens.mdx @@ -0,0 +1,1002 @@ +--- +title: Tokens +description: Write a CAP-46-6 compliant token contract. +sidebar_position: 140 +--- + + + Write a CAP-46-6 compliant token contract + + + + + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +The [token example] demonstrates how to write a token contract that implements the [Token Interface]. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][oigp] + +[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/v20.0.0 +[token example]: https://github.com/stellar/soroban-examples/tree/v20.0.0/token +[token interface]: ../tokens/token-interface.mdx + +## Run the Example + +First go through the [Setup] process to get your development environment configured, then clone the `v20.0.0` tag of `soroban-examples` repository: + +```bash +git clone -b v20.0.0 https://github.com/stellar/soroban-examples +``` + +Or, skip the development environment setup and open this example in [Gitpod][oigp]. + +To run the tests for the example, navigate to the `hello_world` directory, and use `cargo test`. + +```bash +cd token +cargo test +``` + +You should see the output: + +```bash +running 8 tests +test test::initialize_already_initialized - should panic ... ok +test test::transfer_spend_deauthorized - should panic ... ok +test test::decimal_is_over_max - should panic ... ok +test test::test_burn ... ok +test test::transfer_receive_deauthorized - should panic ... ok +test test::transfer_from_insufficient_allowance - should panic ... ok +test test::transfer_insufficient_balance - should panic ... ok +test test::test ... ok +``` + +[setup]: ../getting-started/setup.mdx + +## Code + +:::note + +The source code for this [token example] is broken into several smaller modules. This is a common design pattern for more complex smart contracts. + +::: + + + + +```rust title="token/src/lib.rs" +#![no_std] + +mod admin; +mod allowance; +mod balance; +mod contract; +mod event; +mod metadata; +mod storage_types; +mod test; + +pub use crate::contract::TokenClient; +``` + + + + +```rust title="token/src/admin.rs" +use crate::storage_types::DataKey; +use soroban_sdk::{Address, Env, symbol_short}; + +pub fn has_administrator(e: &Env) -> bool { + let key = DataKey::Admin; + e.storage().instance().has(&key) +} + +pub fn read_administrator(e: &Env) -> Address { + let key = DataKey::Admin; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_administrator(e: &Env, id: &Address) { + let key = DataKey::Admin; + e.storage().instance().set(&key, id); +} +``` + + + + +```rust title="token/src/allowance.rs" +use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; +use soroban_sdk::{Address, Env}; + +pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + if let Some(allowance) = e.storage().temporary().get::<_, AllowanceValue>(&key) { + if allowance.expiration_ledger < e.ledger().sequence() { + AllowanceValue { + amount: 0, + expiration_ledger: allowance.expiration_ledger, + } + } else { + allowance + } + } else { + AllowanceValue { + amount: 0, + expiration_ledger: 0, + } + } +} + +pub fn write_allowance( + e: &Env, + from: Address, + spender: Address, + amount: i128, + expiration_ledger: u32, +) { + let allowance = AllowanceValue { + amount, + expiration_ledger, + }; + + if amount > 0 && expiration_ledger < e.ledger().sequence() { + panic!("expiration_ledger is less than ledger seq when amount > 0") + } + + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + e.storage().temporary().set(&key.clone(), &allowance); + + if amount > 0 { + e.storage().temporary().bump( + &key, + expiration_ledger + .checked_sub(e.ledger().sequence()) + .unwrap(), + ) + } +} + +pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { + let allowance = read_allowance(e, from.clone(), spender.clone()); + if allowance.amount < amount { + panic!("insufficient allowance"); + } + write_allowance( + e, + from, + spender, + allowance.amount - amount, + allowance.expiration_ledger, + ); +} +``` + + + + +```rust title="token/src/balance.rs" +use crate::storage_types::DataKey; +use soroban_sdk::{Address, Env}; + +pub fn read_balance(e: &Env, addr: Address) -> i128 { + let key = DataKey::Balance(addr); + if let Some(balance) = e.storage().persistent().get::(&key) { + balance + } else { + 0 + } +} + +fn write_balance(e: &Env, addr: Address, amount: i128) { + let key = DataKey::Balance(addr); + e.storage().persistent().set(&key, &amount); +} + +pub fn receive_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + if !is_authorized(e, addr.clone()) { + panic!("can't receive when deauthorized"); + } + write_balance(e, addr, balance + amount); +} + +pub fn spend_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + if !is_authorized(e, addr.clone()) { + panic!("can't spend when deauthorized"); + } + if balance < amount { + panic!("insufficient balance"); + } + write_balance(e, addr, balance - amount); +} + +pub fn is_authorized(e: &Env, addr: Address) -> bool { + let key = DataKey::State(addr); + if let Some(state) = e.storage().persistent().get::(&key) { + state + } else { + true + } +} + +pub fn write_authorization(e: &Env, addr: Address, is_authorized: bool) { + let key = DataKey::State(addr); + e.storage().persistent().set(&key, &is_authorized); +} +``` + + + + +```rust title="token/src/contract.rs" +//! This contract demonstrates a sample implementation of the Soroban token +//! interface. +use crate::admin::{has_administrator, read_administrator, write_administrator}; +use crate::allowance::{read_allowance, spend_allowance, write_allowance}; +use crate::balance::{is_authorized, write_authorization}; +use crate::balance::{read_balance, receive_balance, spend_balance}; +use crate::event; +use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; +use soroban_sdk::{contractimpl, Address, String, Env}; +use soroban_token_sdk::TokenMetadata; + +pub trait TokenTrait { + fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String); + + fn allowance(e: Env, from: Address, spender: Address) -> i128; + + fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32); + + fn balance(e: Env, id: Address) -> i128; + + fn spendable_balance(e: Env, id: Address) -> i128; + + fn authorized(e: Env, id: Address) -> bool; + + fn transfer(e: Env, from: Address, to: Address, amount: i128); + + fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128); + + fn burn(e: Env, from: Address, amount: i128); + + fn burn_from(e: Env, spender: Address, from: Address, amount: i128); + + fn clawback(e: Env, from: Address, amount: i128); + + fn set_authorized(e: Env, id: Address, authorize: bool); + + fn mint(e: Env, to: Address, amount: i128); + + fn set_admin(e: Env, new_admin: Address); + + fn decimals(e: Env) -> u32; + + fn name(e: Env) -> String; + + fn symbol(e: Env) -> String; +} + +fn check_nonnegative_amount(amount: i128) { + if amount < 0 { + panic!("negative amount is not allowed: {}", amount) + } +} + +#[contract] +pub struct Token; + +#[contractimpl] +impl TokenTrait for Token { + fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { + if has_administrator(&e) { + panic!("already initialized") + } + write_administrator(&e, &admin); + if decimal > u8::MAX.into() { + panic!("Decimal must fit in a u8"); + } + + write_metadata( + &e, + TokenMetadata { + decimal, + name, + symbol, + }, + ) + } + + fn allowance(e: Env, from: Address, spender: Address) -> i128 { + read_allowance(&e, from, spender) + } + + fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) { + from.require_auth(); + + check_nonnegative_amount(amount); + + write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger); + event::approve(&e, from, spender, amount, expiration_ledger); + } + + fn balance(e: Env, id: Address) -> i128 { + read_balance(&e, id) + } + + fn spendable_balance(e: Env, id: Address) -> i128 { + read_balance(&e, id) + } + + fn authorized(e: Env, id: Address) -> bool { + is_authorized(&e, id) + } + + fn transfer(e: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + event::transfer(&e, from, to, amount); + } + + fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { + spender.require_auth(); + + check_nonnegative_amount(amount); + spend_allowance(&e, from.clone(), spender, amount); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + event::transfer(&e, from, to, amount) + } + + fn burn(e: Env, from: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + spend_balance(&e, from.clone(), amount); + event::burn(&e, from, amount); + } + + fn burn_from(e: Env, spender: Address, from: Address, amount: i128) { + spender.require_auth(); + + check_nonnegative_amount(amount); + spend_allowance(&e, from.clone(), spender, amount); + spend_balance(&e, from.clone(), amount); + event::burn(&e, from, amount) + } + + fn clawback(e: Env, from: Address, amount: i128) { + check_nonnegative_amount(amount); + let admin = read_administrator(&e); + admin.require_auth(); + spend_balance(&e, from.clone(), amount); + event::clawback(&e, admin, from, amount); + } + + fn set_authorized(e: Env, id: Address, authorize: bool) { + let admin = read_administrator(&e); + admin.require_auth(); + write_authorization(&e, id.clone(), authorize); + event::set_authorized(&e, admin, id, authorize); + } + + fn mint(e: Env, to: Address, amount: i128) { + check_nonnegative_amount(amount); + let admin = read_administrator(&e); + admin.require_auth(); + receive_balance(&e, to.clone(), amount); + event::mint(&e, admin, to, amount); + } + + fn set_admin(e: Env, new_admin: Address) { + let admin = read_administrator(&e); + admin.require_auth(); + write_administrator(&e, &new_admin); + event::set_admin(&e, admin, new_admin); + } + + fn decimals(e: Env) -> u32 { + read_decimal(&e) + } + + fn name(e: Env) -> Bytes { + read_name(&e) + } + + fn symbol(e: Env) -> Bytes { + read_symbol(&e) + } +} +``` + + + + +```rust title="token/src/event.rs" +use soroban_sdk::{Address, Env, Symbol, symbol_short}; + +pub(crate) fn approve(e: &Env, from: Address, to: Address, amount: i128, expiration_ledger: u32) { + let topics = (Symbol::new(e, "approve"), from, to); + e.events().publish(topics, (amount, expiration_ledger)); +} + +pub(crate) fn transfer(e: &Env, from: Address, to: Address, amount: i128) { + let topics = (symbol_short!("transfer"), from, to); + e.events().publish(topics, amount); +} + +pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) { + let topics = (symbol_short!("mint"), admin, to); + e.events().publish(topics, amount); +} + +pub(crate) fn clawback(e: &Env, admin: Address, from: Address, amount: i128) { + let topics = (symbol_short!("clawback"), admin, from); + e.events().publish(topics, amount); +} + +pub(crate) fn set_authorized(e: &Env, admin: Address, id: Address, authorize: bool) { + let topics = (Symbol::new(e, "set_authorized"), admin, id); + e.events().publish(topics, authorize); +} + +pub(crate) fn set_admin(e: &Env, admin: Address, new_admin: Address) { + let topics = (symbol_short!("set_admin"), admin); + e.events().publish(topics, new_admin); +} + +pub(crate) fn burn(e: &Env, from: Address, amount: i128) { + let topics = (symbol_short!("burn"), from); + e.events().publish(topics, amount); +} +``` + + + + +```rust title="token/src/metadata.rs" +use soroban_sdk::{Bytes, Env}; +use soroban_token_sdk::{TokenMetadata, TokenUtils}; + +pub fn read_decimal(e: &Env) -> u32 { + let util = TokenUtils::new(e); + util.get_metadata_unchecked().unwrap().decimal +} + +pub fn read_name(e: &Env) -> Bytes { + let util = TokenUtils::new(e); + util.get_metadata_unchecked().unwrap().name +} + +pub fn read_symbol(e: &Env) -> Bytes { + let util = TokenUtils::new(e); + util.get_metadata_unchecked().unwrap().symbol +} + +pub fn write_metadata(e: &Env, metadata: TokenMetadata) { + let util = TokenUtils::new(e); + util.set_metadata(&metadata); +} +``` + + + + +```rust title="token/src/storage_types.rs" +use soroban_sdk::{contracttype, Address}; + +#[derive(Clone)] +#[contracttype] +pub struct AllowanceDataKey { + pub from: Address, + pub spender: Address, +} + +#[contracttype] +pub struct AllowanceValue { + pub amount: i128, + pub expiration_ledger: u32, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Allowance(AllowanceDataKey), + Balance(Address), + Nonce(Address), + State(Address), + Admin, +} +``` + + + + +Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/token + +## How it Works + +Tokens created on a smart contract platform can take many different forms, include a variety of different functionalities, and meet very different needs or use-cases. While each token can fulfill a unique niche, there are some "normal" features that almost all tokens will need to make use of (e.g., payments, transfers, balance queries, etc.). In an effort to minimize repetition and streamline token deployments, Soroban implements the [Token Interface], which provides a uniform, predictable interface for developers and users. + +Creating a Soroban token compatible contract from an existing Stellar asset is very easy, it requires deploying the built-in [Stellar Asset Contract]. + +This example contract, however, demonstrates how a smart contract token might be constructed that doesn't take advantage of the Stellar Asset Contract, but does still satisfy the commonly used Token Interface to maximize interoperability. + +[stellar asset contract]: ../tokens/stellar-asset-contract.mdx + +### Separation of Functionality + +You have likely noticed that this example contract is broken into discrete modules, with each one responsible for a siloed set of functionality. This common practice helps to organize the code and make it more maintainable. + +For example, most of the token logic exists in the `contract.rs` module. Functions like `mint`, `burn`, `transfer`, etc. are written and programmed in that file. The Token Interface describes how some of these functions should emit events when they occur. However, keeping all that event-emitting logic bundled in with the rest of the contract code could make it harder to track what is happening in the code, and that confusion could ultimately lead to errors. + +Instead, we have a separate `events.rs` module that takes away all the headache of emitting events when other functions run. Here is the function to emit an event whenever the token is minted: + +```rust +pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) { + let topics = (symbol_short!("mint"), admin, to); + e.events().publish(topics, amount); +} +``` + +Admittedly, this is a simple example, but constructing the contract this way makes it very clear to the developer what is happening and where. This function is then used by the `contract.rs` module whenever the `mint` function is invoked: + +```rust +// earlier in `contract.rs` +use crate::event; + +fn mint(e: Env, to: Address, amount: i128) { + check_nonnegative_amount(amount); + let admin = read_administrator(&e); + admin.require_auth(); + receive_balance(&e, to.clone(), amount); + // highlight-next-line + event::mint(&e, admin, to, amount); +} +``` + +This same convention is used to separate from the "main" contract code the metadata for the token, the storage type definitions, etc. + +### Standardized Interface, Customized Behavior + +This example contract follows the standardized [Token Interface], implementing all of the same functions as the [Stellar Asset Contract]. This gives wallets, users, developers, etc. a predictable interface to interact with the token. Even though we are implementing the same _interface_ of functions, that doesn't mean we have to implement the same _behavior_ inside those functions. While this example contract doesn't actually modify any of the functions that would be present in a deployed instance of the Stellar Asset Contract, that possibility remains open to the contract developer. + +By way of example, perhaps you have an NFT project, and the artist wants to have a small royalty paid every time their token transfers hands: + +```rust +// This is mainly the `transfer` function from `src/contract.rs` +fn transfer(e: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + spend_balance(&e, from.clone(), amount); + // highlight-start + // We calculate some new amounts for payment and royalty + let payment = (amount * 997) / 1000; + let royalty = amount - payment + receive_balance(&e, artist.clone(), royalty); + // highlight-end + receive_balance(&e, to.clone(), payment); + event::transfer(&e, from, to, amount); +} +``` + +The `transfer` interface is still in use, and is still the same as other tokens, but we've customized the behavior to address a specific need. Another use-case might be a tightly controlled token that requires authentication from an admin before any `transfer`, `allowance`, etc. function could be invoked. + +:::tip + +Of course, you will want your token to behave in an _intuitive_ and _transparent_ manner. If a user is invoking a `transfer`, they will expect tokens to move. If an asset issuer needs to invoke a `clawback` they will likely _require_ the right kind of behavior to take place. + +::: + +## Tests + +Open the `token/src/test.rs` file to follow along. + +```rust title="token/src/test.rs" +#![cfg(test)] +extern crate std; + +use crate::{contract::Token, TokenClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal, Symbol}; + +fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + let token = TokenClient::new(e, &e.register_contract(None, Token {})); + token.initialize(admin, &7, &"name".into_val(e), &"symbol".into_val(e)); + token +} + +#[test] +fn test() { + let e = Env::default(); + e.mock_all_auths(); + + let admin1 = Address::random(&e); + let admin2 = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let user3 = Address::random(&e); + let token = create_token(&e, &admin1); + + token.mint(&user1, &1000); + assert_eq!( + e.auths(), + std::vec![( + admin1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("mint"), + (&user1, 1000_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user2, &user3, &500, &200); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("approve"), + (&user2, &user3, 500_i128, 200_u32).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.allowance(&user2, &user3), 500); + + token.transfer(&user1, &user2, &600); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("transfer"), + (&user1, &user2, 600_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 400); + assert_eq!(token.balance(&user2), 600); + + token.transfer_from(&user3, &user2, &user1, &400); + assert_eq!( + e.auths(), + std::vec![( + user3.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + Symbol::new(&e, "transfer_from"), + (&user3, &user2, &user1, 400_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 800); + assert_eq!(token.balance(&user2), 200); + + token.transfer(&user1, &user3, &300); + assert_eq!(token.balance(&user1), 500); + assert_eq!(token.balance(&user3), 300); + + token.set_admin(&admin2); + assert_eq!( + e.auths(), + std::vec![( + admin1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("set_admin"), + (&admin2,).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + token.set_authorized(&user2, &false); + assert_eq!( + e.auths(), + std::vec![( + admin2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + Symbol::new(&e, "set_authorized"), + (&user2, false).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.authorized(&user2), false); + + token.set_authorized(&user3, &true); + assert_eq!(token.authorized(&user3), true); + + token.clawback(&user3, &100); + assert_eq!( + e.auths(), + std::vec![( + admin2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("clawback"), + (&user3, 100_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user3), 200); + + // Set allowance to 500 + token.approve(&user2, &user3, &500, &200); + assert_eq!(token.allowance(&user2, &user3), 500); + token.approve(&user2, &user3, &0, &200); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("approve"), + (&user2, &user3, 0_i128, 200_u32).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.allowance(&user2, &user3), 0); +} + +#[test] +fn test_burn() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user1, &user2, &500, &200); + assert_eq!(token.allowance(&user1, &user2), 500); + + token.burn_from(&user2, &user1, &500); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("burn_from"), + (&user2, &user1, 500_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + assert_eq!(token.allowance(&user1, &user2), 0); + assert_eq!(token.balance(&user1), 500); + assert_eq!(token.balance(&user2), 0); + + token.burn(&user1, &500); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("burn"), + (&user1, 500_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + assert_eq!(token.balance(&user1), 0); + assert_eq!(token.balance(&user2), 0); +} + +#[test] +#[should_panic(expected = "insufficient balance")] +fn transfer_insufficient_balance() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.transfer(&user1, &user2, &1001); +} + +#[test] +#[should_panic(expected = "can't receive when deauthorized")] +fn transfer_receive_deauthorized() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.set_authorized(&user2, &false); + token.transfer(&user1, &user2, &1); +} + +#[test] +#[should_panic(expected = "can't spend when deauthorized")] +fn transfer_spend_deauthorized() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.set_authorized(&user1, &false); + token.transfer(&user1, &user2, &1); +} + +#[test] +#[should_panic(expected = "insufficient allowance")] +fn transfer_from_insufficient_allowance() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::random(&e); + let user1 = Address::random(&e); + let user2 = Address::random(&e); + let user3 = Address::random(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user1, &user3, &100, &200); + assert_eq!(token.allowance(&user1, &user3), 100); + + token.transfer_from(&user3, &user1, &user2, &101); +} + +#[test] +#[should_panic(expected = "already initialized")] +fn initialize_already_initialized() { + let e = Env::default(); + let admin = Address::random(&e); + let token = create_token(&e, &admin); + + token.initialize(&admin, &10, &"name".into_val(&e), &"symbol".into_val(&e)); +} + +#[test] +#[should_panic(expected = "Decimal must fit in a u8")] +fn decimal_is_over_max() { + let e = Env::default(); + let admin = Address::random(&e); + let token = TokenClient::new(&e, &e.register_contract(None, Token {})); + token.initialize( + &admin, + &(u32::from(u8::MAX) + 1), + &"name".into_val(&e), + &"symbol".into_val(&e), + ); +} +``` + +The token example implements eight different tests to cover a wide array of potential behaviors and problems. However, all of the tests start with a few common pieces. In any test, the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run in. + +```rust +let e = Env::default(); +``` + +We mock authentication checks in the tests, which allows the tests to proceed as if all users/addresses/contracts/etc. had successfully authenticated. + +```rust +e.mock_all_auths(); +``` + +We're also using a `create_token` function to ease the repetition of having to register and initialize our token contract. The resulting `token` client is then used to invoke the contract during each test. + +```rust +// It is defined at the top of the file... +fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + let token = TokenClient::new(e, &e.register_contract(None, Token {})); + token.initialize(admin, &7, &"name".into_val(e), &"symbol".into_val(e)); + token +} + +// ... and it is used inside each test +let token = create_token(&e, &admin); +``` + +All public functions within an `impl` block that has been annotated with the `#[contractimpl]` attribute will have a corresponding function in the test's generated client type. The client type will be named the same as the contract type with `Client` appended. For example, in our contract, the contract type is named `Token`, and the client type is named `TokenClient`. + +The eight tests created for this example contract test a range of possible conditions and ensure the contract responds appropriately to each one: + +- **`test()`** - This function makes use of a variety of the built-in token functions to test the "predictable" way an asset might be interacted with by a user, as well as an administrator. +- **`test_burn()`** - This function ensures a `burn()` invocation decreases a user's balance, and that a `burn_from()` invocation decreases a user's balance as well as consuming another user's allowance of that balance. +- **`transfer_insufficient_balance()`** - This function ensures a `transfer()` invocation panics when the `from` user doesn't have the balance to cover it. +- **`transfer_receive_deauthorized()`** - This function ensures a user who is specifically de-authorized to hold the token cannot be the beneficiary of a `transfer()` invocation. +- **`transfer_spend_deauthorized()`** - This function ensures a user with a token balance, who is subsequently de-authorized cannot be the source of a `transfer()` invocation. +- **`transfer_from_insufficient_allowance()`** - This function ensures a user with an existing allowance for someone else's balance cannot make a `transfer()` greater than that allowance. +- **`initialize_already_initialized()`** - This function checks that the contract cannot have it's `initialize()` function invoked a second time. +- **`decimal_is_over_max()`** - This function tests that invoking `initialize()` with too high of a decimal precision will not succeed. + +## Build the Contract + +To build the contract, use the `soroban contract build` command. + +```bash +soroban contract build +``` + +A `.wasm` file should be outputted in the `target` directory: + +```bash +target/wasm32-unknown-unknown/release/soroban_token_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions using it. + +```bash +soroban contract invoke \ + --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm \ + --id 1 \ + -- \ + balance \ + --id GBZV3NONYSUDVTEHATQO4BCJVFXJO3XQU5K32X3XREVZKSMMOZFO4ZXR +``` + +[`soroban-cli`]: https://developers.stellar.org/docs/tools/developer-tools#soroban-cli diff --git a/docs/smart-contracts/getting-started/README.mdx b/docs/smart-contracts/getting-started/README.mdx new file mode 100644 index 000000000..a9d1d6569 --- /dev/null +++ b/docs/smart-contracts/getting-started/README.mdx @@ -0,0 +1,10 @@ +--- +title: Getting Started +sidebar_position: 20 +--- + +import DocCardList from "@theme/DocCardList"; + +Dive into smart contract development with our "Getting Started" tutorial. + + diff --git a/docs/smart-contracts/getting-started/create-an-app.mdx b/docs/smart-contracts/getting-started/create-an-app.mdx new file mode 100644 index 000000000..1d9e8e8c5 --- /dev/null +++ b/docs/smart-contracts/getting-started/create-an-app.mdx @@ -0,0 +1,437 @@ +--- +sidebar_position: 50 +title: 5. Create an App +description: Make a frontend web app that interacts with your smart contracts. +--- + +With two smart contracts deployed to a public network, you can now create a web app that interacts with them via RPC calls. Let's get started. + +## Initialize a frontend toolchain + +You can build a Soroban app with any frontend toolchain or integrate it into any existing full-stack app. For this tutorial, we're going to use [Astro](https://astro.build/). Astro works with React, Vue, Svelte, any other UI library, or no UI library at all. In this tutorial, we're not using a UI library. The Soroban-specific parts of this tutorial will be similar no matter what frontend toolchain you use. + +If you're new to frontend, don't worry. We won't go too deep. But it will be useful for you to see and experience the frontend development process used by Soroban apps. We'll cover the relevant bits of JavaScript and Astro, but teaching all of frontend development and Astro is beyond the scope of this tutorial. + +Let's get started. + +You're going to need [Node.js](https://nodejs.org/en/download/package-manager/) v18.14.1 or greater. If you haven't yet, install it now. + +We want to initialize our current project as an Astro project. To do this, we can again turn to the `soroban contract init` command, which has a `--frontend-template` flag that allows us to pass the url of a frontend template repository. As we learned in [Storing Data](storing-data.mdx#adding-the-increment-contract), `soroban contract init` will not overwrite existing files, and is safe to use to add to an existing project. + +From our `getting-started-tutorial` directory, run the following command to add the Astro template files. + +```sh +soroban contract init ./ \ + --frontend-template https://github.com/AhaLabs/soroban-astro-template +``` + +This will add the following to your project, which we'll go over in more detail below. + +```bash +├── CONTRIBUTING.md +├── initialize.js +├── package-lock.json +├── package.json +├── packages +├── public +│   └── favicon.svg +├── src +│   ├── components +│   │   └── Card.astro +│   ├── env.d.ts +│   ├── layouts +│   │   └── Layout.astro +│   └── pages +│   └── index.astro +└── tsconfig.json +``` + +## Generate an NPM package for the Hello World contract + +Before we open the new frontend files, let's generate an NPM package for the Hello World contract. This is our suggested way to interact with contracts from frontends. These generated libraries work with any JavaScript project (not a specific UI like React), and make it easy to work with some of the trickiest bits of Soroban, like encoding [XDR](https://soroban.stellar.org/docs/fundamentals-and-concepts/fully-typed-contracts). + +This is going to use the CLI command `soroban contract bindings typescript`: + +```bash +soroban contract bindings typescript \ + --network testnet \ + --contract-id $(cat .soroban/contract-ids/soroban_hello_world_contract.txt) \ + --output-dir packages/hello_world +``` + +This project is set up as an NPM Workspace, and so the `hello_world` client library was generated in the `packages` directory at `packages/hello_world`. + +We attempt to keep the code in these generated libraries readable, so go ahead and look around. Open up the new `packages/hello_world` directory in your editor. If you've built or contributed to Node projects, it will all look familiar. You'll see a `package.json` file, a `src` directory, a `tsconfig.json`, and even a README. + +## Generate an NPM package for the Increment contract + +Though we can run `soroban contract bindings typescript` for each of our contracts individually, the [soroban-astro-template](https://github.com/AhaLabs/soroban-astro-template) that we used as our template includes a very handy `initialize.js` script that will handle this for all of the contracts in our `contracts` directory. + +In addition to generating the NPM packages, `initialize.js` will also: + +- Generate and fund our Stellar account +- Build all of the contracts in the `contracts` dir +- Deploy our contracts +- Create handy contract clients for each contract + +We have already taken care of the first three bullet points in earlier steps of this tutorial, so those tasks will be noops when we run `initialize.js`. + +### Configure initialize.js + +We need to make sure that `initialize.js` has all of the environment variables it needs before we do anything else. Copy the `.env.example` file over to `.env`. The environment variables set in `.env` are used by the `initialize.js` script. + +```bash +cp .env.example .env +``` + +Let's take a look at the contents of the `.env` file: + +``` +# Prefix with "PUBLIC_" to make available in Astro frontend files +PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017" +PUBLIC_SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc" + +SOROBAN_ACCOUNT="me" +SOROBAN_NETWORK="standalone" + +# env vars that begin with PUBLIC_ will be available to the client +PUBLIC_SOROBAN_RPC_URL=$SOROBAN_RPC_URL +``` + +This `.env` file defaults to connecting to a locally running network, but we want to configure our project to communicate with Testnet, since that is where we deployed our contracts. To do that, let's update the `.env` file to look like this: + +```diff +# Prefix with "PUBLIC_" to make available in Astro frontend files +-PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017" ++PUBLIC_SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +-PUBLIC_SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc" ++PUBLIC_SOROBAN_RPC_URL="https://soroban-testnet.stellar.org:443" + +-SOROBAN_ACCOUNT="me" ++SOROBAN_ACCOUNT="alice" +-SOROBAN_NETWORK="standalone" ++SOROBAN_NETWORK="testnet" +``` + +:::info + +This `.env` file is used in the `initialize.js` script. When using the CLI, we can still use the network configuration we set up in the [Setup](./setup.mdx) step, or by passing the `--rpc-url` and `--network-passphrase` flags. + +::: + +### Run `initialize.js` + +First let's install the Javascript dependencies: + +```bash +npm install +``` + +And then let's run `initialize.js`: + +```bash +npm run init +``` + +As mentioned above, this script attempts to build and deploy our contracts, which we have already done. The script is smart enough to check if a step has already been taken care of, and is a no-op in that case, so it is safe to run more than once. + +### Call the contract from the frontend + +Now let's open up `src/pages/index.astro` and take a look at how the frontend code integrates with the NPM package we created for our contracts. + +Here we can see that we're importing our generated `helloWorld` client from `../contracts/soroban_hello_world_contract`. We're then invoking the `hello` method and adding the result to the page. + +```ts title="src/pages/index.astro" +--- +import Layout from "../layouts/Layout.astro"; +import Card from "../components/Card.astro"; +import helloWorld from "../contracts/soroban_hello_world_contract"; +const { result } = await helloWorld.hello({ to: "you" }); +const greeting = result.join(" "); +--- + + ... + +

{greeting}

+``` + +Let's see it in action! Start the dev server: + +```bash +npm run dev +``` + +And open [http://localhost:4321](http://localhost:4321) in your browser. You should see the greeting from the contract! + +You can try updating the `{ to: 'Soroban' }` argument. When you save the file, the page will automatically update. + +:::info + +When you start up the dev server with `npm run dev`, you will see similar output in your terminal as when you ran `npm run init`. This is because the `dev` script in package.json is set up to run `npm run init` and `astro dev`, so that you can ensure that your deployed contract and your generated NPM pacakage are always in sync. If you want to just start the dev server without the initialize.js script, you can run `npm run astro dev`. + +::: + +### What's happening here? + +If you inspect the page (right-click, inspect) and refresh, you'll see a couple interesting things: + +- The "Network" tab shows that there are no Fetch/XHR requests made. But RPC calls happen via Fetch/XHR! So how is the frontend calling the contract? +- There's no JavaScript on the page. But we just wrote some JavaScript! How is it working? + +This is part of Astro's philosophy: the frontend should ship with as few assets as possible. Preferably zero JavaScript. When you put JavaScript in the [frontmatter](https://docs.astro.build/en/core-concepts/astro-components/), Astro will run it at build time, and then replace anything in the `{...}` curly brackets with the output. + +When using the development server with `npm run dev`, it runs the frontmatter code on the server, and injects the resulting values into the page on the client. + +You can try building to see this more dramatically: + +```bash +npm run build +``` + +Then check the `dist` folder. You'll see that it built an HTML and CSS file, but no JavaScript. And if you look at the HTML file, you'll see a static "Hello Soroban" in the `

`. + +During the build, Astro made a single call to your contract, then injected the static result into the page. This is great for contract methods that don't change, but probably won't work for most contract methods. Let's integrate with the `incrementor` contract to see how to handle interactive methods in Astro. --> + +## Call the incrementor contract from the frontend + +While `hello` is a simple view-only/read method, `increment` changes on-chain state. This means that someone needs to sign the transaction. So we'll need to add transaction-signing capabilities to the frontend. + +The way signing works in a browser is with a _wallet_. Wallets can be web apps, browser extensions, standalone apps, or even separate hardware devices. + +### Install Freighter Extension + +Right now, the wallet that best supports Soroban is [Freighter](../guides/freighter/README.mdx). It is available as a Firefox Add-on, as well as extensions for Chrome and Brave. Go ahead and [install it now](https://freighter.app). + +Once it's installed, open it up by clicking the extension icon. If this is your first time using Freighter, you will need to create a new wallet. Go through the prompts to create a password and save your recovery passphrase. + +Go to Settings (the gear icon) → Preferences and toggle the switch to Enable Experimental Mode. Then go back to its home screen and select "Test Net" from the top-right dropdown. Finally, if it shows the message that your Stellar address is not funded, go ahead and click the "Fund with Friendbot" button. + +Now you're all set up to use Freighter as a user, and you can add it to your app. + +### Add Freighter + +We're going to add a "Connect" button to the page that opens Freighter and prompts the user to give your web page permission to use Freighter. Once they grant this permission, the "Connect" button will be replaced with a message saying, "Signed in as [their public key]". + +First, add [@stellar/freighter-api](https://www.npmjs.com/package/@stellar/freighter-api) as a dependency: + +```bash +npm install @stellar/freighter-api +``` + +Now let's add a new component to the `src/components` directory called `ConnectFreighter.astro` with the following contents: + +```html title="src/components/ConnectFreighter.astro" +
+
+ +
+
+ + + + +``` + +Some of this may look surprising. `