├── .gitmodules ├── cli ├── .gitignore ├── rust-toolchain.toml ├── test.sh ├── LICENSE ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── program-test ├── .gitignore ├── rust-toolchain.toml ├── test.sh ├── Cargo.toml └── tests │ └── functional.rs ├── .gitignore ├── src ├── error.zig ├── state.zig ├── instruction.zig └── main.zig ├── install-rust-build-deps.sh ├── install-solana-zig.sh ├── README.md ├── .github └── workflows │ └── main.yml └── LICENSE /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /program-test/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /cli/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86.0" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | solana-zig 2 | zig-cache/ 3 | zig-out/ 4 | .zig-cache/ 5 | -------------------------------------------------------------------------------- /program-test/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86.0" 3 | -------------------------------------------------------------------------------- /src/error.zig: -------------------------------------------------------------------------------- 1 | pub const ProgramError = error{ 2 | AlreadyInUse, 3 | InvalidAccountType, 4 | Uninitialized, 5 | IncorrectSize, 6 | }; 7 | -------------------------------------------------------------------------------- /program-test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ZIG="$1" 4 | ROOT_DIR="$(cd "$(dirname "$0")"/..; pwd)" 5 | if [[ -z "$ZIG" ]]; then 6 | ZIG="$ROOT_DIR/solana-zig/zig" 7 | fi 8 | set -e 9 | $ZIG build --summary all --verbose 10 | SBF_OUT_DIR="$ROOT_DIR/zig-out/lib" cargo test --manifest-path "$ROOT_DIR/program-test/Cargo.toml" 11 | -------------------------------------------------------------------------------- /cli/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ZIG="$1" 4 | ROOT_DIR="$(cd "$(dirname "$0")"/..; pwd)" 5 | if [[ -z "$ZIG" ]]; then 6 | ZIG="$ROOT_DIR/solana-zig/zig" 7 | fi 8 | 9 | set -e 10 | $ZIG build --summary all --verbose 11 | SBF_OUT_DIR="$ROOT_DIR/zig-out/lib" cargo test --manifest-path "$ROOT_DIR/cli/Cargo.toml" -- --nocapture 12 | -------------------------------------------------------------------------------- /install-rust-build-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | case $(uname -s | cut -c1-7) in 4 | "Windows" | "MINGW64") 5 | # OpenSSL also needed 6 | choco install protoc 7 | export PROTOC='C:\ProgramData\chocolatey\lib\protoc\tools\bin\protoc.exe' 8 | ;; 9 | "Darwin") 10 | brew install protobuf 11 | ;; 12 | "Linux") 13 | sudo apt update 14 | sudo apt install pkg-config libudev-dev protobuf-compiler -y 15 | ;; 16 | *) 17 | echo "Unknown Operating System" 18 | exit 1 19 | ;; 20 | esac 21 | -------------------------------------------------------------------------------- /program-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Jon C "] 3 | description = "Solana Helloworld Zig Program Test" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | name = "solana-helloworld-zig-program-test" 7 | repository = "https://github.com/joncinque/solana-helloworld-zig/program-test" 8 | version = "0.0.1" 9 | 10 | [dev-dependencies] 11 | num_enum = "0.7" 12 | solana-program = "3.0.0" 13 | solana-program-test = "3.0.0" 14 | solana-sdk = "3.0.0" 15 | solana-system-interface = "2" 16 | test-case = "3" 17 | -------------------------------------------------------------------------------- /src/state.zig: -------------------------------------------------------------------------------- 1 | pub const AccountType = enum(u8) { 2 | Uninitialized, 3 | SmallInt, 4 | BigInt, 5 | 6 | pub fn sizeOfData(self: AccountType) usize { 7 | return switch (self) { 8 | AccountType.Uninitialized => 0, 9 | AccountType.SmallInt => @sizeOf(SmallIntData), 10 | AccountType.BigInt => @sizeOf(BigIntData), 11 | }; 12 | } 13 | }; 14 | 15 | pub const SmallIntData = packed struct { amount: u32 }; 16 | 17 | pub const BigIntData = packed struct { amount: u256 }; 18 | -------------------------------------------------------------------------------- /cli/LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli-template" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "WTFPL" 6 | publish = false 7 | 8 | [dependencies] 9 | clap = { version = "3", features = ["cargo"] } 10 | futures-util = "0.3.19" 11 | solana-clap-v3-utils = "3.0.0" 12 | solana-cli-config = "3.0.0" 13 | solana-client = "3.0.0" 14 | solana-commitment-config = "3.0.0" 15 | solana-logger = "3.0.0" 16 | solana-remote-wallet = "3.0.0" 17 | solana-sdk = "3.0.0" 18 | solana-system-interface = "2" 19 | tokio = { version = "1", features = ["full"] } 20 | 21 | [dev-dependencies] 22 | solana-sdk-ids = "3.0.0" 23 | solana-test-validator = "3.0.0" 24 | 25 | [workspace] 26 | -------------------------------------------------------------------------------- /src/instruction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const AccountType = @import("state.zig").AccountType; 3 | 4 | const testing = std.testing; 5 | 6 | pub const InstructionType = enum(u8) { 7 | init, 8 | increment, 9 | add, 10 | }; 11 | 12 | pub const InitData = packed struct { 13 | account_type: AccountType, 14 | }; 15 | 16 | pub const AddData = packed struct { 17 | amount: u32, 18 | }; 19 | 20 | test "instruction type cast" { 21 | const inc = InstructionType.increment; 22 | const add = InstructionType.add; 23 | 24 | // can work with a ptr, but brittle 25 | const inc_type = [_]u8{1}; 26 | const inc_ptr: *const InstructionType = @ptrCast(&inc_type); 27 | try testing.expectEqual(inc, inc_ptr.*); 28 | 29 | // preferred way, directly from the value 30 | const add_type = [_]u8{ 0, 2 }; 31 | const add_ptr: InstructionType = @enumFromInt(add_type[1]); 32 | try testing.expectEqual(add, add_ptr); 33 | } 34 | 35 | test "instruction data cast" { 36 | const add_type = [_]u8{ 2, 255, 255, 255, 255 }; 37 | const data_ptr = @intFromPtr(&add_type) + @sizeOf(InstructionType); 38 | const add_ptr: *align(1) const AddData = @ptrCast(@as([*]u8, @ptrFromInt(data_ptr))); 39 | try testing.expectEqual(add_ptr.amount, std.math.maxInt(u32)); 40 | } 41 | -------------------------------------------------------------------------------- /install-solana-zig.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n $SOLANA_ZIG_VERSION ]]; then 4 | solana_zig_version="$SOLANA_ZIG_VERSION" 5 | else 6 | solana_zig_version="v1.52.0" 7 | fi 8 | solana_zig_release_url="https://github.com/joncinque/solana-zig-bootstrap/releases/download/solana-$solana_zig_version" 9 | 10 | output_dir="$1" 11 | if [[ -z $output_dir ]]; then 12 | output_dir="solana-zig" 13 | fi 14 | output_dir="$(mkdir -p "$output_dir"; cd "$output_dir"; pwd)" 15 | cd $output_dir 16 | 17 | arch=$(uname -m) 18 | if [[ "$arch" == "arm64" ]]; then 19 | arch="aarch64" 20 | fi 21 | case $(uname -s | cut -c1-7) in 22 | "Linux") 23 | os="linux" 24 | abi="musl" 25 | ;; 26 | "Darwin") 27 | os="macos" 28 | abi="none" 29 | ;; 30 | "Windows" | "MINGW64") 31 | os="windows" 32 | abi="gnu" 33 | ;; 34 | *) 35 | echo "install-solana-zig.sh: Unknown OS $(uname -s)" >&2 36 | exit 1 37 | ;; 38 | esac 39 | 40 | solana_zig_tar=zig-$arch-$os-$abi.tar.bz2 41 | url="$solana_zig_release_url/$solana_zig_tar" 42 | echo "Downloading $url" 43 | curl --proto '=https' --tlsv1.2 -SfOL "$url" 44 | echo "Unpacking $solana_zig_tar" 45 | tar -xjf $solana_zig_tar 46 | rm $solana_zig_tar 47 | 48 | solana_zig_dir="zig-$arch-$os-$abi-baseline" 49 | mv "$solana_zig_dir"/* . 50 | rmdir $solana_zig_dir 51 | echo "solana-zig compiler available at $output_dir" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solana-helloworld-zig 2 | 3 | A simple hello world program for Solana in Zig. 4 | 5 | ## Getting started 6 | 7 | ### Compiler 8 | 9 | First, you need a zig compiler built with Solana's LLVM fork. See the README of 10 | [solana-zig-bootstrap](https://github.com/joncinque/solana-zig-bootstrap) 11 | on how to build it, or you can download it from the 12 | [GitHub releases page](https://github.com/joncinque/solana-zig-bootstrap/releases). 13 | 14 | There is also a helper script which will install it to the current directory: 15 | 16 | ```console 17 | ./install-solana-zig.sh 18 | ``` 19 | 20 | ### Dependencies 21 | 22 | This project opts for the zig package manager and the package declared at 23 | [solana-program-sdk-zig](https://github.com/joncinque/solana-program-sdk-zig). 24 | 25 | ```console 26 | zig fetch --save https://github.com/joncinque/base58-zig/archive/refs/tags/v0.15.0.tar.gz 27 | zig fetch --save https://github.com/joncinque/solana-sdk-zig/archive/refs/tags/v0.17.0.tar.gz 28 | ``` 29 | 30 | ### Build 31 | 32 | You can build the program by running: 33 | 34 | ```console 35 | ./solana-zig/zig build 36 | ``` 37 | 38 | ### Deploy 39 | 40 | With the Solana tools, run: 41 | 42 | ```console 43 | solana program deploy zig-out/lib/helloworld.so 44 | ``` 45 | 46 | ## Command-line Interface 47 | 48 | The repo has a simple CLI to send instructions to the program: 49 | 50 | ```console 51 | cd cli 52 | ./test.sh 53 | ``` 54 | 55 | The CLI requires a Rust compiler to run, and the test script requires the Solana 56 | CLI to startup a test validator. 57 | 58 | ## Program Tests 59 | 60 | There are also integration tests run against the Agave runtime using the 61 | [`solana-program-test` crate](https://crates.io/solana-program-test). 62 | 63 | You can run these tests using the `test.sh` script: 64 | 65 | ```console 66 | cd program-test/ 67 | ./test.sh 68 | ``` 69 | 70 | These tests require a Rust compiler along with the solana-zig compiler. 71 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | ## Quick start 2 | 1. Install Rust from https://rustup.rs/ 3 | 2. `cargo run` 4 | 5 | ## Want Batteries Included? 6 | 7 | This template is intentionally minimal. For a template that's more functional out of the box, try https://github.com/hashblock/solana-cli-program-template 8 | 9 | ## About 10 | `solana-cli-template` is a sample app demonstrating the creation of a minimal CLI application written in Rust to interact with Solana. 11 | It provides three pieces of functionality: 12 | 13 | - `ping`: Creates a transaction sending 0 SOL from the signer's account to the signer's account. Returns the signature of the transaction. 14 | - `balance`: Returns the account's balance. 15 | - `help`: Tips for using the app. This is an off-chain operation. 16 | 17 | ## Local step-by-step 18 | 1. Prepare account: 19 | - Start a local node: run `solana-test-validator`. 20 | - Generate a keypair: `solana-keygen new -o test.json`. 21 | - Add 100 SOL to the corresponding account `solana airdrop --url http://127.0.0.1:8899 --keypair test.json 100`. 22 | 23 | 2. Build app: `cargo run`. 24 | 25 | 3. Ping: 26 | ``` 27 | $ cargo run -- ping --url http://127.0.0.1:8899 --keypair test.json 28 | Signature: 2Y863JX96RTqbeGfcvQowVt1V91Dgs2LZfVgQ3mGJPmYu24sUTYmfkArHAAgj4uFqP75bm9GXU9DYjiMFxahQJUC 29 | ``` 30 | 31 | 4. Balance: 32 | ``` 33 | $ cargo run -- balance --url http://127.0.0.1:8899 --keypair test.json 34 | 3dSRGE3wYCcGWFrxAsQs5PaBqtJzzxdTzY2ypXNFUji9 has a balance of ◎99.999995000 // balance less than 100 because of ping operation above 35 | ``` 36 | 37 | 5. Run help for the complete list of options: 38 | ``` 39 | $ cargo run -- --help 40 | cli-template 0.1.0 41 | 42 | 43 | USAGE: 44 | cli-template [FLAGS] [OPTIONS] 45 | 46 | FLAGS: 47 | -h, --help Prints help information 48 | -V, --version Prints version information 49 | -v, --verbose Show additional information 50 | 51 | OPTIONS: 52 | -C, --config Configuration file to use [default: /Users/user/.config/solana/cli/config.yml] 53 | --url JSON RPC URL for the cluster [default: value from configuration file] 54 | --keypair Filepath or URL to a keypair [default: client keypair] 55 | 56 | SUBCOMMANDS: 57 | balance Get balance 58 | help Prints this message or the help of the given subcommand(s) 59 | ping Send a ping transaction 60 | ``` 61 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sol = @import("solana_program_sdk"); 3 | 4 | const ix = @import("instruction.zig"); 5 | const state = @import("state.zig"); 6 | const ProgramError = @import("error.zig").ProgramError; 7 | 8 | export fn entrypoint(input: [*]u8) u64 { 9 | var context = sol.context.Context.load(input) catch return 1; 10 | processInstruction(context.program_id, context.accounts[0..context.num_accounts], context.data) catch |err| return @intFromError(err); 11 | return 0; 12 | } 13 | 14 | fn processInstruction(program_id: *sol.public_key.PublicKey, accounts: []sol.account.Account, data: []const u8) ProgramError!void { 15 | sol.log.print("Hello zig program {f}", .{program_id}); 16 | for (accounts, 0..) |account, i| { 17 | sol.log.print("Hello Account {d}: {f}", .{ i, account.info().owner_id }); 18 | } 19 | sol.log.print("Hello Data: {any}", .{data}); 20 | const instruction_type: *const ix.InstructionType = @ptrCast(data); 21 | switch (instruction_type.*) { 22 | ix.InstructionType.init => { 23 | const init: *align(1) const ix.InitData = @ptrCast(data[1..]); 24 | sol.log.print("Hello init {d}", .{init.account_type}); 25 | const account = accounts[0]; 26 | const account_type: state.AccountType = @enumFromInt(account.data()[0]); 27 | if (account_type != state.AccountType.Uninitialized) { 28 | return ProgramError.AlreadyInUse; 29 | } 30 | if (init.account_type == state.AccountType.Uninitialized) { 31 | return ProgramError.InvalidAccountType; 32 | } 33 | if (account.data()[1..].len != init.account_type.sizeOfData()) { 34 | return ProgramError.IncorrectSize; 35 | } 36 | account.data()[0] = @intFromEnum(init.account_type); 37 | }, 38 | ix.InstructionType.increment => { 39 | sol.log.log("Hello increment"); 40 | const account = accounts[0]; 41 | const account_type: state.AccountType = @enumFromInt(account.data()[0]); 42 | switch (account_type) { 43 | state.AccountType.Uninitialized => return ProgramError.Uninitialized, 44 | state.AccountType.SmallInt => { 45 | var accountData: *align(1) state.SmallIntData = @ptrCast(account.data()[1..]); 46 | accountData.amount += 1; 47 | }, 48 | state.AccountType.BigInt => { 49 | var accountData: *align(1) state.BigIntData = @ptrCast(account.data()[1..]); 50 | accountData.amount += 1; 51 | }, 52 | } 53 | }, 54 | ix.InstructionType.add => { 55 | const add: *align(1) const ix.AddData = @ptrCast(data[1..]); 56 | sol.log.print("Hello add {d}", .{add.amount}); 57 | const account = accounts[0]; 58 | const account_type: state.AccountType = @enumFromInt(account.data()[0]); 59 | switch (account_type) { 60 | state.AccountType.Uninitialized => return ProgramError.Uninitialized, 61 | state.AccountType.SmallInt => { 62 | var accountData: *align(1) state.SmallIntData = @ptrCast(account.data()[1..]); 63 | accountData.amount += add.amount; 64 | }, 65 | state.AccountType.BigInt => { 66 | var accountData: *align(1) state.BigIntData = @ptrCast(account.data()[1..]); 67 | accountData.amount += add.amount; 68 | }, 69 | } 70 | }, 71 | } 72 | } 73 | 74 | test { 75 | std.testing.refAllDecls(@This()); 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | push: 8 | branches: 9 | - 'main' 10 | 11 | env: 12 | SOLANA_ZIG_VERSION: v1.52.0 13 | SOLANA_ZIG_DIR: solana-zig 14 | 15 | jobs: 16 | unit-test: 17 | name: Unit tests 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | fail-fast: false 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | 28 | - uses: actions/cache@v4 29 | with: 30 | path: | 31 | $SOLANA_ZIG_DIR 32 | key: solana-zig-${{ hashFiles('./program-test/Cargo.lock') }}-$SOLANA_ZIG_VERSION 33 | 34 | - name: Download solana-zig compiler 35 | shell: bash 36 | run: SOLANA_ZIG_VERSION=$SOLANA_ZIG_VERSION ./install-solana-zig.sh $SOLANA_ZIG_DIR 37 | 38 | - name: Test program 39 | shell: bash 40 | run: | 41 | $SOLANA_ZIG_DIR/zig build test --summary all --verbose 42 | 43 | integration-test: 44 | name: Integration tests 45 | strategy: 46 | matrix: 47 | os: [ubuntu-latest, macos-latest, windows-latest] 48 | fail-fast: false 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | submodules: recursive 54 | 55 | - uses: actions/cache@v4 56 | with: 57 | path: | 58 | ~/.cargo/registry 59 | ~/.cargo/git 60 | ~/.cargo/zig 61 | $SOLANA_ZIG_DIR 62 | key: solana-zig-${{ hashFiles('./program-test/Cargo.lock') }}-$SOLANA_ZIG_VERSION 63 | 64 | - name: Download solana-zig compiler 65 | shell: bash 66 | run: SOLANA_ZIG_VERSION=$SOLANA_ZIG_VERSION ./install-solana-zig.sh $SOLANA_ZIG_DIR 67 | 68 | - name: Install Rust 69 | uses: dtolnay/rust-toolchain@master 70 | with: 71 | toolchain: 1.86.0 72 | 73 | # took the workaround from https://github.com/sfackler/rust-openssl/issues/2149 74 | - name: Set Perl environment variables 75 | if: runner.os == 'Windows' 76 | run: | 77 | echo "PERL=$((where.exe perl)[0])" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 78 | echo "OPENSSL_SRC_PERL=$((where.exe perl)[0])" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 79 | 80 | - name: Setup openssl on Windows 81 | if: runner.os == 'Windows' 82 | shell: bash 83 | run: | 84 | cat > vcpkg.json <> $GITHUB_ENV 99 | echo "OPENSSL_INCLUDE_DIR=$PWD/vcpkg_installed/x64-windows-static-md/include" >> $GITHUB_ENV 100 | 101 | - name: Install build deps 102 | shell: bash 103 | run: ./install-rust-build-deps.sh 104 | 105 | - name: Build and test program 106 | shell: bash 107 | run: ./program-test/test.sh 108 | 109 | cli-test: 110 | name: CLI tests 111 | if: false 112 | strategy: 113 | matrix: 114 | os: [ubuntu-latest, macos-latest, windows-latest] 115 | fail-fast: false 116 | runs-on: ${{ matrix.os }} 117 | steps: 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | 122 | - uses: actions/cache@v4 123 | with: 124 | path: | 125 | ~/.cargo/registry 126 | ~/.cargo/git 127 | ~/.cargo/zig 128 | $SOLANA_ZIG_DIR 129 | key: solana-zig-${{ hashFiles('./cli/Cargo.lock') }}-$SOLANA_ZIG_VERSION 130 | 131 | - name: Download solana-zig compiler 132 | shell: bash 133 | run: SOLANA_ZIG_VERSION=$SOLANA_ZIG_VERSION ./install-solana-zig.sh $SOLANA_ZIG_DIR 134 | 135 | - name: Install Rust 136 | uses: dtolnay/rust-toolchain@master 137 | with: 138 | toolchain: 1.86.0 139 | 140 | - name: Install Rust build deps 141 | shell: bash 142 | run: ./install-rust-build-deps.sh 143 | 144 | - name: Build and test program 145 | shell: bash 146 | run: ./cli/test.sh 147 | -------------------------------------------------------------------------------- /program-test/tests/functional.rs: -------------------------------------------------------------------------------- 1 | use { 2 | num_enum::IntoPrimitive, 3 | solana_program::{ 4 | instruction::{AccountMeta, Instruction, InstructionError}, 5 | pubkey::Pubkey, 6 | }, 7 | solana_program_test::{tokio, ProgramTest, ProgramTestContext}, 8 | solana_sdk::{ 9 | signature::{Keypair, Signer}, 10 | transaction::{Transaction, TransactionError}, 11 | }, 12 | solana_system_interface::instruction as system_instruction, 13 | test_case::test_case, 14 | }; 15 | 16 | mod helloworld { 17 | solana_program::declare_id!("Zigc1Hc97L8Pebma74jDzYiyoUvdxxcj7Gxppg9VRxK"); 18 | } 19 | 20 | fn program_test() -> ProgramTest { 21 | ProgramTest::new("helloworld", helloworld::id(), None) 22 | } 23 | 24 | #[repr(u8)] 25 | #[derive(Clone, Copy, IntoPrimitive)] 26 | enum AccountType { 27 | Uninitialized, 28 | SmallInt, 29 | BigInt, 30 | } 31 | 32 | async fn create_and_init( 33 | context: &mut ProgramTestContext, 34 | account_type: AccountType, 35 | ) -> Result { 36 | let rent = context.banks_client.get_rent().await.unwrap(); 37 | let size = match account_type { 38 | AccountType::SmallInt => 4, 39 | AccountType::BigInt => 32, 40 | AccountType::Uninitialized => 1, 41 | }; 42 | let space = size + 1; 43 | let lamports = rent.minimum_balance(space); 44 | let new_account = Keypair::new(); 45 | let blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); 46 | 47 | let transaction = Transaction::new_signed_with_payer( 48 | &[ 49 | system_instruction::create_account( 50 | &context.payer.pubkey(), 51 | &new_account.pubkey(), 52 | lamports, 53 | space as u64, 54 | &helloworld::id(), 55 | ), 56 | Instruction { 57 | program_id: helloworld::id(), 58 | accounts: vec![AccountMeta::new(new_account.pubkey(), false)], 59 | data: vec![0, u8::from(account_type)], 60 | }, 61 | ], 62 | Some(&context.payer.pubkey()), 63 | &[&context.payer, &new_account], 64 | blockhash, 65 | ); 66 | context 67 | .banks_client 68 | .process_transaction(transaction) 69 | .await 70 | .map(|_| new_account.pubkey()) 71 | .map_err(|e| e.unwrap()) 72 | } 73 | 74 | #[tokio::test] 75 | async fn fail_init_uninitialized() { 76 | let pt = program_test(); 77 | let mut context = pt.start_with_context().await; 78 | let err = create_and_init(&mut context, AccountType::Uninitialized) 79 | .await 80 | .unwrap_err(); 81 | assert_eq!( 82 | err, 83 | TransactionError::InstructionError(1, InstructionError::Custom(2)) 84 | ); 85 | } 86 | 87 | #[test_case(AccountType::SmallInt; "small")] 88 | #[test_case(AccountType::BigInt; "big")] 89 | #[tokio::test] 90 | async fn init(account_type: AccountType) { 91 | let pt = program_test(); 92 | let mut context = pt.start_with_context().await; 93 | let account_pubkey = create_and_init(&mut context, account_type).await.unwrap(); 94 | 95 | let account = context 96 | .banks_client 97 | .get_account(account_pubkey) 98 | .await 99 | .unwrap() 100 | .unwrap(); 101 | let mut expected_data = vec![u8::from(account_type)]; 102 | match account_type { 103 | AccountType::SmallInt => expected_data.extend_from_slice(&[0; 4]), 104 | AccountType::BigInt => expected_data.extend_from_slice(&[0; 32]), 105 | AccountType::Uninitialized => unreachable!(), 106 | } 107 | assert_eq!(account.data, expected_data); 108 | } 109 | 110 | #[test_case(AccountType::SmallInt; "small")] 111 | #[test_case(AccountType::BigInt; "big")] 112 | #[tokio::test] 113 | async fn increment(account_type: AccountType) { 114 | let pt = program_test(); 115 | let mut context = pt.start_with_context().await; 116 | let account_pubkey = create_and_init(&mut context, account_type).await.unwrap(); 117 | 118 | let blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); 119 | let transaction = Transaction::new_signed_with_payer( 120 | &[Instruction { 121 | program_id: helloworld::id(), 122 | accounts: vec![AccountMeta::new(account_pubkey, false)], 123 | data: vec![1], 124 | }], 125 | Some(&context.payer.pubkey()), 126 | &[&context.payer], 127 | blockhash, 128 | ); 129 | context 130 | .banks_client 131 | .process_transaction(transaction) 132 | .await 133 | .unwrap(); 134 | 135 | let account = context 136 | .banks_client 137 | .get_account(account_pubkey) 138 | .await 139 | .unwrap() 140 | .unwrap(); 141 | let mut expected_data = vec![u8::from(account_type), 1]; 142 | match account_type { 143 | AccountType::SmallInt => expected_data.extend_from_slice(&[0; 3]), 144 | AccountType::BigInt => expected_data.extend_from_slice(&[0; 31]), 145 | AccountType::Uninitialized => unreachable!(), 146 | } 147 | assert_eq!(account.data, expected_data); 148 | } 149 | 150 | #[test_case(AccountType::SmallInt; "small")] 151 | #[test_case(AccountType::BigInt; "big")] 152 | #[tokio::test] 153 | async fn add(account_type: AccountType) { 154 | let pt = program_test(); 155 | let mut context = pt.start_with_context().await; 156 | let account_pubkey = create_and_init(&mut context, account_type).await.unwrap(); 157 | 158 | let blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); 159 | let transaction = Transaction::new_signed_with_payer( 160 | &[Instruction { 161 | program_id: helloworld::id(), 162 | accounts: vec![AccountMeta::new(account_pubkey, false)], 163 | data: vec![2, 255, 255, 255, 255], 164 | }], 165 | Some(&context.payer.pubkey()), 166 | &[&context.payer], 167 | blockhash, 168 | ); 169 | context 170 | .banks_client 171 | .process_transaction(transaction) 172 | .await 173 | .unwrap(); 174 | 175 | let account = context 176 | .banks_client 177 | .get_account(account_pubkey) 178 | .await 179 | .unwrap() 180 | .unwrap(); 181 | let mut expected_data = vec![u8::from(account_type), 255, 255, 255, 255]; 182 | match account_type { 183 | AccountType::SmallInt => expected_data.extend_from_slice(&[0; 0]), 184 | AccountType::BigInt => expected_data.extend_from_slice(&[0; 28]), 185 | AccountType::Uninitialized => unreachable!(), 186 | } 187 | assert_eq!(account.data, expected_data); 188 | } 189 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::{crate_description, crate_name, crate_version, Arg, Command}, 3 | futures_util::StreamExt, 4 | solana_clap_v3_utils::{ 5 | input_parsers::{ 6 | parse_url_or_moniker, 7 | signer::{SignerSource, SignerSourceParserBuilder}, 8 | }, 9 | input_validators::normalize_to_url_if_moniker, 10 | keypair::signer_from_path, 11 | }, 12 | solana_client::{ 13 | nonblocking::{pubsub_client::PubsubClient, rpc_client::RpcClient}, 14 | rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}, 15 | }, 16 | solana_commitment_config::CommitmentConfig, 17 | solana_remote_wallet::remote_wallet::RemoteWalletManager, 18 | solana_sdk::{ 19 | instruction::{AccountMeta, Instruction}, 20 | message::Message, 21 | pubkey::Pubkey, 22 | signature::{Keypair, Signature, Signer}, 23 | transaction::Transaction, 24 | }, 25 | solana_system_interface::instruction as system_instruction, 26 | std::{process::exit, rc::Rc}, 27 | }; 28 | 29 | struct Config { 30 | commitment_config: CommitmentConfig, 31 | default_signer: Box, 32 | json_rpc_url: String, 33 | verbose: bool, 34 | websocket_url: String, 35 | } 36 | 37 | async fn process_ping( 38 | rpc_client: &RpcClient, 39 | signer: &dyn Signer, 40 | program_id: &Pubkey, 41 | dry_run: bool, 42 | ) -> Result> { 43 | let new_account = Keypair::new(); 44 | let space = 33; 45 | let lamports = rpc_client 46 | .get_minimum_balance_for_rent_exemption(space.try_into()?) 47 | .await?; 48 | let mut transaction = Transaction::new_unsigned(Message::new( 49 | &[ 50 | system_instruction::create_account( 51 | &signer.pubkey(), 52 | &new_account.pubkey(), 53 | lamports, 54 | space, 55 | program_id, 56 | ), 57 | Instruction { 58 | program_id: *program_id, 59 | accounts: vec![AccountMeta::new(new_account.pubkey(), false)], 60 | data: vec![0, 2], 61 | }, 62 | Instruction { 63 | program_id: *program_id, 64 | accounts: vec![AccountMeta::new(new_account.pubkey(), false)], 65 | data: vec![1], 66 | }, 67 | Instruction { 68 | program_id: *program_id, 69 | accounts: vec![AccountMeta::new(new_account.pubkey(), false)], 70 | data: vec![2, 255, 255, 255, 255], 71 | }, 72 | ], 73 | Some(&signer.pubkey()), 74 | )); 75 | 76 | let blockhash = rpc_client 77 | .get_latest_blockhash() 78 | .await 79 | .map_err(|err| format!("error: unable to get latest blockhash: {err}"))?; 80 | 81 | transaction 82 | .try_sign(&vec![signer, &new_account], blockhash) 83 | .map_err(|err| format!("error: failed to sign transaction: {err}"))?; 84 | 85 | if dry_run { 86 | println!("4"); 87 | let result = rpc_client.simulate_transaction(&transaction).await.unwrap(); 88 | println!("{result:?}"); 89 | Ok(transaction.signatures[0]) 90 | } else { 91 | let signature = rpc_client 92 | .send_and_confirm_transaction_with_spinner(&transaction) 93 | .await 94 | .map_err(|err| format!("error: send transaction: {err}"))?; 95 | Ok(signature) 96 | } 97 | } 98 | 99 | async fn process_logs(websocket_url: &str) -> Result<(), Box> { 100 | let pubsub_client = PubsubClient::new(websocket_url).await?; 101 | 102 | let (mut logs, logs_unsubscribe) = pubsub_client 103 | .logs_subscribe( 104 | RpcTransactionLogsFilter::All, 105 | RpcTransactionLogsConfig { 106 | commitment: Some(CommitmentConfig::confirmed()), 107 | }, 108 | ) 109 | .await?; 110 | 111 | while let Some(log) = logs.next().await { 112 | println!("Transaction executed in slot {}:", log.context.slot); 113 | println!(" Signature: {}:", log.value.signature); 114 | println!( 115 | " Status: {}", 116 | log.value 117 | .err 118 | .map(|err| err.to_string()) 119 | .unwrap_or_else(|| "Success".into()) 120 | ); 121 | println!(" Log Messages:"); 122 | for msg in log.value.logs { 123 | println!(" {msg}"); 124 | } 125 | } 126 | logs_unsubscribe().await; 127 | Ok(()) 128 | } 129 | 130 | #[tokio::main] 131 | async fn main() -> Result<(), Box> { 132 | let app_matches = Command::new(crate_name!()) 133 | .about(crate_description!()) 134 | .version(crate_version!()) 135 | .subcommand_required(true) 136 | .arg_required_else_help(true) 137 | .arg({ 138 | let arg = Arg::new("config_file") 139 | .short('C') 140 | .long("config") 141 | .value_name("PATH") 142 | .takes_value(true) 143 | .global(true) 144 | .help("Configuration file to use"); 145 | if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { 146 | arg.default_value(config_file) 147 | } else { 148 | arg 149 | } 150 | }) 151 | .arg( 152 | Arg::new("keypair") 153 | .long("keypair") 154 | .value_name("KEYPAIR") 155 | .value_parser(SignerSourceParserBuilder::default().allow_all().build()) 156 | .takes_value(true) 157 | .global(true) 158 | .help("Filepath or URL to a keypair [default: client keypair]"), 159 | ) 160 | .arg( 161 | Arg::new("verbose") 162 | .long("verbose") 163 | .short('v') 164 | .takes_value(false) 165 | .global(true) 166 | .help("Show additional information"), 167 | ) 168 | .arg( 169 | Arg::new("json_rpc_url") 170 | .short('u') 171 | .long("url") 172 | .value_name("URL") 173 | .takes_value(true) 174 | .global(true) 175 | .value_parser(parse_url_or_moniker) 176 | .help("JSON RPC URL for the cluster [default: value from configuration file]"), 177 | ) 178 | .subcommand( 179 | Command::new("ping") 180 | .about("Send a ping transaction") 181 | .arg( 182 | Arg::new("address") 183 | .value_parser(SignerSourceParserBuilder::default().allow_all().build()) 184 | .value_name("ADDRESS") 185 | .takes_value(true) 186 | .index(1) 187 | .help("Address of the helloworld program to ping"), 188 | ) 189 | .arg( 190 | Arg::new("dry_run") 191 | .long("dry-run") 192 | .help("Simulate the transaction"), 193 | ), 194 | ) 195 | .subcommand(Command::new("logs").about("Stream transaction logs")) 196 | .get_matches(); 197 | 198 | let (command, matches) = app_matches.subcommand().unwrap(); 199 | let mut wallet_manager: Option> = None; 200 | 201 | let config = { 202 | let cli_config = if let Some(config_file) = matches.value_of("config_file") { 203 | solana_cli_config::Config::load(config_file).unwrap_or_default() 204 | } else { 205 | solana_cli_config::Config::default() 206 | }; 207 | 208 | let default_signer = if let Ok(Some((signer, _))) = 209 | SignerSource::try_get_signer(matches, "keypair", &mut wallet_manager) 210 | { 211 | Box::new(signer) 212 | } else { 213 | signer_from_path( 214 | matches, 215 | &cli_config.keypair_path, 216 | "keypair", 217 | &mut wallet_manager, 218 | )? 219 | }; 220 | 221 | let json_rpc_url = normalize_to_url_if_moniker( 222 | matches 223 | .value_of("json_rpc_url") 224 | .unwrap_or(&cli_config.json_rpc_url), 225 | ); 226 | 227 | let websocket_url = solana_cli_config::Config::compute_websocket_url(&json_rpc_url); 228 | Config { 229 | commitment_config: CommitmentConfig::confirmed(), 230 | default_signer, 231 | json_rpc_url, 232 | verbose: matches.is_present("verbose"), 233 | websocket_url, 234 | } 235 | }; 236 | solana_logger::setup_with_default("solana=info"); 237 | 238 | if config.verbose { 239 | println!("JSON RPC URL: {}", config.json_rpc_url); 240 | println!("Websocket URL: {}", config.websocket_url); 241 | } 242 | let rpc_client = 243 | RpcClient::new_with_commitment(config.json_rpc_url.clone(), config.commitment_config); 244 | 245 | match (command, matches) { 246 | ("logs", _arg_matches) => { 247 | process_logs(&config.websocket_url) 248 | .await 249 | .unwrap_or_else(|err| { 250 | eprintln!("error: {err}"); 251 | exit(1); 252 | }); 253 | } 254 | ("ping", arg_matches) => { 255 | let address = 256 | SignerSource::try_get_pubkey(arg_matches, "address", &mut wallet_manager)?.unwrap(); 257 | let dry_run = arg_matches.is_present("dry_run"); 258 | let signature = process_ping( 259 | &rpc_client, 260 | config.default_signer.as_ref(), 261 | &address, 262 | dry_run, 263 | ) 264 | .await 265 | .unwrap_or_else(|err| { 266 | eprintln!("error: send transaction: {err}"); 267 | exit(1); 268 | }); 269 | println!("Signature: {signature}"); 270 | } 271 | _ => unreachable!(), 272 | }; 273 | 274 | Ok(()) 275 | } 276 | 277 | #[cfg(test)] 278 | mod test { 279 | use { 280 | super::*, 281 | solana_sdk_ids::bpf_loader_upgradeable, 282 | solana_test_validator::*, 283 | std::{env, path::PathBuf}, 284 | }; 285 | 286 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 287 | async fn test_ping() { 288 | solana_logger::setup(); 289 | let mut test_validator_genesis = TestValidatorGenesis::default(); 290 | let sbf_out_dir = env::var("SBF_OUT_DIR").unwrap(); 291 | let mut program_path = PathBuf::from(sbf_out_dir); 292 | program_path.push("helloworld.so"); 293 | let program_id = Pubkey::new_unique(); 294 | test_validator_genesis.add_upgradeable_programs_with_path(&[UpgradeableProgramInfo { 295 | program_id, 296 | loader: bpf_loader_upgradeable::id(), 297 | program_path, 298 | upgrade_authority: Pubkey::new_unique(), 299 | }]); 300 | let (test_validator, payer) = test_validator_genesis.start_async().await; 301 | let rpc_client = test_validator.get_async_rpc_client(); 302 | 303 | process_ping(&rpc_client, &payer, &program_id, false).await.unwrap(); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------