diff --git a/src/generator/template-sources.ts b/src/generator/template-sources.ts index 6690d5e..387ffac 100644 --- a/src/generator/template-sources.ts +++ b/src/generator/template-sources.ts @@ -4,6 +4,7 @@ import { ValidationCliError } from '../errors/errors.js'; const BUNDLED_TEMPLATE_DIRECTORIES = { 'basic-app': 'basic-app', 'full-stack': 'full-stack', + 'smart-contract': 'smart-contract', } as const; type BundledTemplateId = keyof typeof BUNDLED_TEMPLATE_DIRECTORIES; diff --git a/templates/smart-contract/.gitignore b/templates/smart-contract/.gitignore new file mode 100644 index 0000000..64d73d3 --- /dev/null +++ b/templates/smart-contract/.gitignore @@ -0,0 +1,4 @@ +target/ +.env +.env.* +.DS_Store diff --git a/templates/smart-contract/Cargo.toml b/templates/smart-contract/Cargo.toml new file mode 100644 index 0000000..4ba755e --- /dev/null +++ b/templates/smart-contract/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +resolver = "2" +members = ["contracts/*"] + +[workspace.dependencies] +soroban-sdk = "26" + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true diff --git a/templates/smart-contract/README.md b/templates/smart-contract/README.md new file mode 100644 index 0000000..0ded9a2 --- /dev/null +++ b/templates/smart-contract/README.md @@ -0,0 +1,57 @@ +# {{projectName}} + +Generated with the StellarForge `smart-contract` template. + +This is an intentionally small, non-custodial Soroban starter. It does not manage assets, balances, authorization, or deployment credentials. + +## Requirements + +- Rust 1.84 or newer +- `wasm32v1-none` target +- current `stellar` CLI + +Install the Rust target when needed: + +```bash +rustup target add wasm32v1-none +``` + +## Test + +```bash +cargo test +``` + +## Build + +Use the current Stellar CLI contract workflow: + +```bash +stellar contract build +``` + +The CLI builds contract crates for the `wasm32v1-none` target using the release profile. + +## Testnet deployment + +Build first, then deploy the generated Wasm with an identity you created outside this repository. Choose an alias that is valid for your local Stellar configuration; `my-contract` is used below only as an example. + +```bash +stellar contract deploy \ + --wasm \ + --source \ + --network testnet \ + --alias my-contract +``` + +Invoke the starter after deployment: + +```bash +stellar contract invoke \ + --id my-contract \ + --source \ + --network testnet \ + -- hello --name StellarForge +``` + +Do not commit private keys, secret seeds, signing credentials, or production deployment material. Use locally managed Stellar identities or your approved secret-management workflow. diff --git a/templates/smart-contract/contracts/hello/Cargo.toml b/templates/smart-contract/contracts/hello/Cargo.toml new file mode 100644 index 0000000..f9c8172 --- /dev/null +++ b/templates/smart-contract/contracts/hello/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "stellarforge-starter-contract" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/templates/smart-contract/contracts/hello/src/lib.rs b/templates/smart-contract/contracts/hello/src/lib.rs new file mode 100644 index 0000000..c9325cd --- /dev/null +++ b/templates/smart-contract/contracts/hello/src/lib.rs @@ -0,0 +1,15 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, String}; + +#[contract] +pub struct Contract; + +#[contractimpl] +impl Contract { + pub fn hello(name: String) -> String { + name + } +} + +mod test; diff --git a/templates/smart-contract/contracts/hello/src/test.rs b/templates/smart-contract/contracts/hello/src/test.rs new file mode 100644 index 0000000..e8b87f5 --- /dev/null +++ b/templates/smart-contract/contracts/hello/src/test.rs @@ -0,0 +1,14 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{Env, String}; + +#[test] +fn returns_the_supplied_greeting_target() { + let env = Env::default(); + let contract_id = env.register(Contract, ()); + let client = ContractClient::new(&env, &contract_id); + let name = String::from_str(&env, "StellarForge"); + + assert_eq!(client.hello(&name), name); +} diff --git a/tests/smart-contract-template.test.ts b/tests/smart-contract-template.test.ts new file mode 100644 index 0000000..0780c0e --- /dev/null +++ b/tests/smart-contract-template.test.ts @@ -0,0 +1,74 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createNewCommand } from '../src/commands/new.js'; +import { + createCapturedTerminalOutput, + withTempDirectory, +} from './helpers/index.js'; + +describe('Smart Contract template', () => { + it('generates a valid Cargo workspace with current Stellar contract guidance and no secrets', () => + withTempDirectory((root) => { + const captured = createCapturedTerminalOutput(); + const command = createNewCommand({ + cwd: () => root, + output: captured.output, + }); + + command.parse([ + 'node', + 'new', + 'demo.contract', + '--template', + 'smart-contract', + ]); + + const projectRoot = join(root, 'demo.contract'); + const rootManifest = readFileSync( + join(projectRoot, 'Cargo.toml'), + 'utf8', + ); + const contractManifest = readFileSync( + join(projectRoot, 'contracts', 'hello', 'Cargo.toml'), + 'utf8', + ); + const readme = readFileSync(join(projectRoot, 'README.md'), 'utf8'); + + expect( + existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'lib.rs')), + ).toBe(true); + expect( + existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'test.rs')), + ).toBe(true); + expect(rootManifest).toContain('soroban-sdk = "26"'); + expect(rootManifest).not.toContain('wasm32v1-none'); + expect(contractManifest).toContain( + 'name = "stellarforge-starter-contract"', + ); + expect(contractManifest).not.toContain('demo.contract'); + expect(readme).toContain('# demo.contract'); + expect(readme).toContain('stellar contract build'); + expect(readme).toContain('stellar contract deploy'); + expect(readme).toContain('stellar contract invoke'); + expect(readme).toContain('--network testnet'); + expect(readme).not.toMatch(/S[A-Z0-9]{55}/); + expect(captured.stdoutText()).toContain( + 'Created demo.contract from smart-contract', + ); + + const metadata = spawnSync( + 'cargo', + ['metadata', '--no-deps', '--format-version=1'], + { + cwd: projectRoot, + encoding: 'utf8', + shell: false, + }, + ); + + expect(metadata.status, metadata.stderr).toBe(0); + expect(metadata.stdout).toContain('stellarforge-starter-contract'); + })); +});