Write Anchor-compatible Solana programs in Python

Overview

seahorse: Write Solana programs in Python

The ease of Python with the safety of Rust.

Seahorse lets you write Solana programs in Python. It is a community-led project built on Anchor.

Developers gain Python's ease-of-use, while still having the same safety guarantees of every Rust program on the Solana chain. Low-level memory problems are handled by default, letting you worry about the important stuff.

Features

  • Compile-time type safety
  • Fully interoperable with Rust code
  • Compatibility with Anchor

The Seahorse compiler generates intermediate Rust artifacts and uses Anchor to do some of the heavy lifting.

Seahorse is beta software. Many features are unimplemented and it's not production-ready.

Get started

Installation

Examples

Example: FizzBuzz

Here's a very simple program that does something similar to the classic FizzBuzz problem.

# fizzbuzz
# Built with Seahorse v0.1.0
#
# On-chain, persistent FizzBuzz!

from seahorse.prelude import *

declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS')

class FizzBuzz(Account):
  fizz: bool
  buzz: bool
  n: u64

@instruction
def init(owner: Signer, fizzbuzz: Empty[FizzBuzz]):
  fizzbuzz.init(payer = owner, seeds = ['fizzbuzz', owner])

@instruction
def do_fizzbuzz(fizzbuzz: FizzBuzz, n: u64):
  fizzbuzz.fizz = n % 3 == 0
  fizzbuzz.buzz = n % 5 == 0
  if not fizzbuzz.fizz and not fizzbuzz.buzz:
    fizzbuzz.n = n
  else:
    fizzbuzz.n = 0

This shows some basic Seahorse functionality, like account initialization and creating instructions. For more, check out Calculator: Your first Seahorse program or other examples here.

Comments
  • Pubkey.find_program_address()

    Pubkey.find_program_address()

    I think it's all there. Thought this would be harder to add but I was able to copy-paste my way to victory.

    I just added the functionality as directly as I could, copying Solana's existing API. Not sure if that's what was wanted or not, but we can have a discussion now that the PR is here.

    Closes #41

    opened by orvioconge 7
  • Need support for passing Pubkeys and Arrays as seeds

    Need support for passing Pubkeys and Arrays as seeds

    Currently, passing pubkeys and/or byte arrays as seeds is not supported by Seahorse compiler as it throws an error like seen in the screenshot, the same happens with an array of u8s or any other integer type Screenshot from 2022-10-27 21-31-34

    opened by harsh4786 6
  • Issue with variable assignment within different scopes

    Issue with variable assignment within different scopes

    Variables are not correctly assigned if they are defined in a certain level and their value is changed in a different one. For example, consider the following instruction:

    @instruction
    def test(signer: Signer, number: f64):
       myboolean=True
       newnumber=1.0
       if number > 0.0:
          myboolean=False
          newnumber=10.0
          print(f'Your boolean is {myboolean}. Extra number: {newnumber}.')
       print(f'Your number is {number}.')
       print(f'Your boolean is {myboolean}. Extra number: {newnumber}.')
    

    When the previous instruction is executed with the number 5.0 as an input, the output is

    Program log: Instruction: Test
    Program log: Your boolean is false. Extra number: 10.
    Program log: Your number is 5.
    Program log: Your boolean is true. Extra number: 1.
    

    which is clearly an undesired behaviour.

    opened by miguel-ot 4
  • Support strings in events by calling .clone()

    Support strings in events by calling .clone()

    Since strings in Rust don't implement Copy, we need to call .clone() when we read these fields of an event. This PR adds logic to add .clone() for only strings.

    I've also added an event example which shows various different field types in events. I found that lists and arrays also don't work, but will also need further work, so I've opened a separate issue for them: #64

    Closes #61

    opened by mcintyre94 4
  • Refactor intermediate datatypes to make the compiler output stable

    Refactor intermediate datatypes to make the compiler output stable

    This means that if we compile the same code repeatedly the output is identical. The motivation for this is to allow adding lightweight infrastructure to detect whether/how a PR changes the compile output for the example Python files. Having a stable output is a prerequisite for this to avoid false positives where eg. functions are just rearranged.

    Where we've used HashMap and HashSet data structures the sort order is undefined, and in practice if you compile multiple times you'll get different outputs.

    See suggestion in Discord: https://discordapp.com/channels/1005658120548270224/1006027721127776388/1042345155522474034

    To test this I compiled the examples 20 times each:

    # examples/
    for f in ./*.py
    do
        for i in {1..20}
        do
            ../target/debug/seahorse compile $f > compiled/$f$i.rs
        done
    done
    

    And then verified with md5 compiled/* that all instances of the same file are identical

    Intentionally didn't update changelog BTW! :D

    opened by mcintyre94 4
  • Class methods

    Class methods

    Adds methods (instance and static) for struct-type classes (a.k.a., non-enums). Accounts may not define constructors, since Solana needs to know they exist instead of just existing transiently in memory.

    Example usage:

    class Point:
        x: i32
        y: i32
    
        # Constructor defined like in Python
        # Caveat: structs now derive `Default`, and have all of their fields set to
        # their default values
        def __init__(self, x: i32, y: i32):
            self.x = x
            self.y = y
    
        # Giving a method a `self` parameter makes it an instance method
        def manhattan_dist(self) -> i32:
            return self.x + self.y
    
        # No self = static method - no @staticmethod needed
        def origin() -> Point:
            return Point(0, 0)
    
    # ...
    
    @instruction
    def ix(...):
        p = Point(2, 3)
        q = Point.origin()
    
        print(p.manhattan_dist(), q.manhattan_dist())
    
    opened by ameliatastic 4
  • Add CI workflow: compile example files + check for any changes

    Add CI workflow: compile example files + check for any changes

    This PR is intended to improve our robustness, by requiring that any changes to the compiled output of the example Seahorse files is know about and reviewed. This should mostly avoid me introducing more bugs :p

    I've tested it on my own fork:

    • Success: https://github.com/mcintyre94/seahorse-lang/actions/runs/3550019740
    • Error (caused by a change in the compiler): https://github.com/mcintyre94/seahorse-lang/actions/runs/3550014723

    It hasn't run on this PR, I assume that's a github security limitation for new workflows.

    Note that currently the examples are quite limited, there are many areas they don't cover. Improving this in future PRs would be a valuable contribution, and double as extra documentation! But in particular because they're all single files being compiled with seahorse compile, the Python imports feature can't currently be tested with it. We might want to add some examples using seahorse build in a full-fledged seahorse project going forward. But I think this is a good start and that most things will be better served by simple single file tests.

    opened by mcintyre94 3
  • Change len(str) to be char count, add size(str) for string size in bytes

    Change len(str) to be char count, add size(str) for string size in bytes

    • Changes the behaviour of len(str) to match Python and return a character count. Eg len("ƒoo") is 3. This is a change from the current behaviour which uses Rust's str.len() and would return 4.

    • Adds a new function size(str) which gets the size in bytes of a string. This uses Rust's str.len().

    This is intended to be a step toward supporting strings on accounts by allowing custom account sizing. Code like assert len(content) < 280, "tweet is too long!" will now behave the same as in Python, allowing up to 280 unicode characters. Asserting on size is also possible if required.

    And the extra space would be defined as 4 + size(content) when we have that capability.

    I split this into a separate PR because it's a breaking change of len.

    opened by mcintyre94 3
  • Allow compiling to WASM

    Allow compiling to WASM

    • Make rustfmt-wrapper a conditional dependency, exclude for wasm
    • Conditionally exclude code that uses rustfmt when compiling for wasm

    Pretty much the same as what I did before, except the beautify function makes it a bit cleaner!

    Should make no change for non-wasm targets, while allowing cargo build --target wasm32-unknown-unknown to work again!

    Closes #20

    opened by mcintyre94 3
  • Add support for strings 🧵

    Add support for strings 🧵

    This PR adds support for strings! The end result is that we can now compile code such as:

    class Tweet(Account):
      owner: Pubkey
      tweet: String[280]
    
    @instruction
    def new_tweet(author: Signer, tweet_account: Empty[Tweet], tweet: String[280]):
      tweet_account.init(payer=author, seeds = ['tweet', author])
      tweet_account.owner = author.key()
    
      assert len(tweet) <= 280, "Tweet must be no more than 280 characters"
      tweet_account.tweet = tweet
    

    This involves the following changes:

    • A new String[N] type is added to the compiler. In the seahorse AST this is stored as Ty::StringOfLength(len), so Seahorse always knows the maximum length of a string. In the generated Rust it is simply a String
    • When we initialize a new account, we add the length of any StringOfLength(n) fields on it to the size. This is necessary because Rust doesn't know the length of strings and std::mem::size_of::<String> is a small constant
    • The new String[N]type is added to the prelude
    • Python's len function is updated to support strings. This works slightly differently to the existing implementation for arrays, which returns the defined length. For strings we generate Rust code that computes the actual length, eg. tweet.chars().count(). This means that you can assert on the length of a string as in the example above

    I've also updated the way we put string constants in Rust to add the .to_string() so you can also write tweet_account.tweet = "hello world" and it'll compile to tweet_account.tweet = "hello world!".to_string();

    Also note that this will slightly over-estimate the space required for the string. I've added a comment, but because we're already doing std::mem::size_of::<Account> this includes a small number of bytes (24 in Rust playground) for the String. I'm not subtracting this from the number I calculate because I'm not sure how constant it is.

    opened by mcintyre94 3
  • Support array of U8 in seeds

    Support array of U8 in seeds

    Anchor supports a (reference to an) array of U8 values as a seed value. Using this allows the client-side code to be more straightforward than having each value as its own seed, since we can pack the array into a single buffer: Buffer.from([x, y]).

    This PR adds support for this to Seahorse, such that this becomes valid:

    @instruction
    def create_pixel(
      pixel: Empty[Pixel],
      user: Signer,
      pos_x: u8,
      pos_y: u8
    ):
      # Initialise pixel account
      pixel_account = pixel.init(
        payer = user,
        seeds = ["pixel", [pos_x, pos_y]]
      )
    

    The generated accounts context has seeds:

    #[derive(Accounts)]
    # [instruction (pos_x : u8 , pos_y : u8)]
    pub struct CreatePixel<'info> {
        #[account(
            init,
            payer = user,
            seeds = ["pixel".as_bytes().as_ref(), [pos_x, pos_y].as_ref()],
            bump,
            space = 8 + std::mem::size_of::<Pixel>()
        )]
        pub pixel: Box<Account<'info, Pixel>>,
        #[account(mut)]
        pub user: Signer<'info>,
        pub system_program: Program<'info, System>,
    }
    

    The PDA can then be generated in JS:

    const [pixelPublicKey] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("pixel"), Buffer.from([x, y])],
      program.programId,
    )
    

    From experimenting with Anchor I think only an array of U8 needs to be supported. A string/pubkey/longer number would take multiple bytes and then you'd have a nested list which Anchor doesn't seem to like. So I think it's only Array of U8 that should be supported.

    Closes #4

    opened by mcintyre94 3
  • Update pyth-sdk-solana to 0.7.0 for compatibility with Anchor 0.26.0

    Update pyth-sdk-solana to 0.7.0 for compatibility with Anchor 0.26.0

    • Seahorse actually uses whichever version of Anchor you have installed/active, so no need to explicitly bump it
    • But there was a dependency clash between pyth-sdk-solana 0.6.1 and Anchor 0.26.0 (on solana-program-library) causing a compile error on seahorse build
    • Bumping to 0.7.0 works with both Anchor 0.25.0 and Anchor 0.26.0
    • Also tested with all examples on Anchor 0.26.0

    Closes #80

    opened by mcintyre94 0
  • Possible bug with string type

    Possible bug with string type

    When compiling the code

    # Built with Seahorse v0.2.5
    
    from seahorse.prelude import *
    
    declare_id('2U2z3rSoeJYEAkLoStCn676EunGSXXt9HVQCcp7d9PB7')
    
    def mystring() -> str:
      return 'abc'
    

    I obtain the following error message:

    ✗ Compiling test... Error: anchor build -p test failed:

    This is most likely a bug in the Seahorse compiler!

    If you want to help the project, you can report this:

    • on the Seahorse Discord (http://discord.gg/4sFzH5pus8)
    • or as a Github issue (https://github.com/ameliatastic/seahorse-lang/issues).

    Thanks!

    Compiling test v0.1.0 (/home/miguel/Aldrin/Programming/Seahorse/test/programs/test) error[E0308]: mismatched types --> programs/test/src/dot/program.rs:10:12 | 9 | pub fn mystring() -> String { | ------ expected String because of return type 10 | return "abc"; | ^^^^^- help: try using a conversion method: .to_string() | | | expected struct String, found &str

    For more information about this error, try rustc --explain E0308. error: could not compile test due to previous error

    If I replace return 'abc' with return str('abc'), the code compiles without errors.

    opened by miguel-ot 1
  • Bump to Anchor 0.26.0

    Bump to Anchor 0.26.0

    Released yesterday, changelog: https://github.com/coral-xyz/anchor/blob/master/CHANGELOG.md

    • Includes the upstream fix for the issue this PR was trying to address in seahorse: https://github.com/ameliatastic/seahorse-lang/pull/56
    • Has some breaking changes we'll need to check seahorse against
    opened by mcintyre94 0
  • Can't use account fields in seed array

    Can't use account fields in seed array

    Context

    I wanted to have a tokenMint signed for by a program, so it uses a protocol as the mint and freeze authority

    Issue

    When trying to pass in the protocol and its bump as signer, I got an error message

    # random
    # Built with Seahorse v0.2.5
    
    from seahorse.prelude import *
    
    declare_id("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS")
    
    
    class Protocol(Account):
        bump: u8
    
    
    @instruction
    def init_protocol(signer: Signer, protocol: Empty[Protocol]):
        protocol = protocol.init(payer=signer, seeds=["protocol"])
        bump = Pubkey.find_program_address(["protocol"])[1]
    
        protocol.bump = bump
    
    
    @instruction
    def init_mint(signer: Signer, mint: Empty[TokenMint], protocol: Protocol):
        mint = mint.init(payer=signer, authority=protocol, decimals=u8(6))
    
    
    @instruction
    def use_bump(
        signer: Signer,
        protocol: Protocol,
        mint: TokenMint,
        destination_token_account: TokenAccount,
    ):
    
        bump = protocol.bump
        mint.mint(
            authority=protocol,
            to=destination_token_account,
            amount=u64(100000),
            signer=["protocol", protocol.bump],
        )
    
    

    output

    Error: anchor build -p bugreport failed:
    
    This is most likely a bug in the Seahorse compiler!
    
    If you want to help the project, you can report this:
      - on the Seahorse Discord (http://discord.gg/4sFzH5pus8)
      - or as a Github issue (https://github.com/ameliatastic/seahorse-lang/issues).
    
    Thanks!
    
       Compiling bugreport v0.1.0 (/Users/purpleparakeet/work/bugreport/programs/bugreport)
    error[E0609]: no field `bump` on type `seahorse_util::Mutable<LoadedProtocol<'info, '_>>`
      --> programs/bugreport/src/dot/program.rs:84:26
       |
    84 |                 protocol.bump.to_le_bytes().as_ref(),
       |                          ^^^^ unknown field
    
    For more information about this error, try `rustc --explain E0609`.
    error: could not compile `bugreport` due to previous error
    

    but when I assign protocol.bump to a variable and pass it in the same way, I get no such message

    # random
    # Built with Seahorse v0.2.5
    
    from seahorse.prelude import *
    
    declare_id("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS")
    
    
    class Protocol(Account):
        bump: u8
    
    
    @instruction
    def init_protocol(signer: Signer, protocol: Empty[Protocol]):
        protocol = protocol.init(payer=signer, seeds=["protocol"])
        bump = Pubkey.find_program_address(["protocol"])[1]
    
        protocol.bump = bump
    
    
    @instruction
    def init_mint(signer: Signer, mint: Empty[TokenMint], protocol: Protocol):
        mint = mint.init(payer=signer, authority=protocol, decimals=u8(6))
    
    
    @instruction
    def use_bump(
        signer: Signer,
        protocol: Protocol,
        mint: TokenMint,
        destination_token_account: TokenAccount,
    ):
    
        bump = protocol.bump
        mint.mint(
            authority=protocol,
            to=destination_token_account,
            amount=u64(100000),
            signer=["protocol", bump],
        )
    
    
    

    I think this behavior is surprising, so I'm filing it as a bug.

    opened by firefly-sol 0
  • Simple project won't compile -- no error message.

    Simple project won't compile -- no error message.

    I was trying to use a bump as a signer. When trying to recreate the issue with the minimum reproducible code, I noticed that this program that I would expect to compile, wouldn't

    Source code:

    # bugreport
    # Built with Seahorse v0.2.5
    
    from seahorse.prelude import *
    
    declare_id("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS")
    
    
    class Protocol:
        bump: u8
    
    
    @instruction
    def init_protocol(signer: Signer, protocol: Empty[Protocol]) -> Program:
        protocol = protocol.init(payer=signer, seeds=["protocol"])
        protocol.bump = u8(0)
    

    Error message on seahorse build

    seahorse build ⠋ Compiling bugreport...thread 'main' panicked at 'explicit panic', src/core/compile/build/mod.rs:248:18 note: run with RUST_BACKTRACE=1 environment variable to display a backtrace ⠙ Compiling bugreport...%

    opened by firefly-sol 1
  • Expression context

    Expression context

    Putting some thoughts into a branch for how to tackle this problem. Context.

    Basically, the compiler needs a way for context to be passed down to expressions. When the build stage is run and a Rust-like AST is generated, the entire tree is built from the leaves up. This means that expressions like a = f(x, y) will first build the syntax tree component for a, x, then y, then combine those to make f(x, y), then combine those to make a = f(x, y).

    This introduces a small problem: the innermost expressions have no way of knowing the context that they're being created in. An example that's been resolved in a somewhat hacky way: a very simple assignment statement like

    x.a = y.b
    

    needs to be turned into something like

    x.borrow_mut().a = y.borrow().b;
    

    due to how mutability is handled. Even though syntactically, the (Python) expressions on the left and right are exactly the same, they need to generated different (Rust) expressions! Right now this is handled by just traversing the LHS a second time and making all of the borrows mutable borrows, but this could easily lead to unforeseen problems. It would be much cleaner to just generate the right code the first time.

    However, getting there is a challenge, because this involves actually changing the compiler, and maybe changing it significantly. On this PR I'll be looking trying to figure out a good way of solving the problem. Any outside help would be greatly appreciated. Thanks!

    enhancement help wanted 
    opened by ameliatastic 3
Owner
✨ amelia chen ✨
✨ amelia chen ✨
🕶 Assorted checks and validations for writing safer Solana programs.

vipers ?? Assorted checks and validations for writing safer Solana programs. Motivation Solana's fee mechanism is unlike Ethereum's, in that the numbe

Saber 131 Sep 14, 2022
Visualization for Timely Dataflow and Differential Dataflow programs

DDShow Visualization for Timely Dataflow and Differential Dataflow programs Getting started with ddshow First, install ddshow via cargo. As of now dds

Chase Wilson 61 Nov 25, 2022
Rust programs written entirely in Rust

mustang Programs written entirely in Rust Mustang is a system for building programs built entirely in Rust, meaning they do not depend on any part of

Dan Gohman 561 Dec 26, 2022
Complete code for the larger example programs from the book.

Code Examples for Programming Rust This repository contains complete code for the larger example programs from the book “Programming Rust”, by Jim Bla

Programming Rust 670 Jan 1, 2023
Cogo is a high-performance library for programming stackful coroutines with which you can easily develop and maintain massive concurrent programs.

Cogo is a high-performance library for programming stackful coroutines with which you can easily develop and maintain massive concurrent programs.

co-rs 47 Nov 17, 2022
Small programs written in Rust. Warm up for the upcoming Selenium Manager

Rust Examples This repository contains several example programs written in Rust. Selenium Manager These examples are used as warm up for the upcoming

Boni García 5 Dec 30, 2022
A additional Rust compiler pass to detect memory safe bugs of Rust programs.

SafeDrop A additional Rust compiler pass to detect memory safe bugs of Rust programs. SafeDrop performs path-sensitive and field-sensitive inter-proce

Artisan-Lab  (Fn*) 5 Nov 25, 2022
Gain intuition about the goings-on of your multithreaded/multicomponent programs

Intuition: a super simple profiler with a terminal ui based on tui-rs. Gain intuition about the goings-on of your multithreaded/multicomponent program

GenesysGo 9 Mar 2, 2023
Rust library for compiling and running other programs.

Exers ?? Exers is a rust library for compiling and running code in different languages and runtimes. Usage example fn main() { // Imports...

olix3001 5 Jun 10, 2023
a frontier based evm compatible chain template

Substrate Frontier Node Template A FRAME-based Substrate node with the Ethereum RPC support, ready for hacking ?? Generation & Upstream This template

zero network 2 Oct 6, 2021
A SCALE-compatible collection of bits

scale-bits · This small utility crate provides two separate things: A Bits type that can be SCALE encoded and decoded, and is fully SCALE compatible w

Parity Technologies 3 Sep 25, 2022
An asynchronous runtime compatible with WebAssembly and non-WebAssembly targets.

Promise x Tokio = Prokio An asynchronous runtime compatible with WebAssembly and non-WebAssembly targets. Rationale When designing components and libr

Yew Stack 29 Feb 6, 2023
OP-Up is a hive tool for testing OP-Stack-compatible software modules

op-up Warning This is a work in progress. OP-Up is a hive tool for testing OP-Stack-compatible software modules. This project was born out of the need

nicolas 20 Jun 13, 2023
A cli tool to write your idea in terminal

Ideas ideas is a cli tools to write your idea in your terminal. Demo Features tagged idea, contains tips, idea, todo status switch ascii icon write yo

王祎 12 Jun 22, 2022
Write Cloudflare Workers in 100% Rust via WebAssembly

Work-in-progress ergonomic Rust bindings to Cloudflare Workers environment. Write your entire worker in Rust! Read the Notes and FAQ Example Usage use

Cloudflare 1.3k Dec 29, 2022
Learn to write Rust procedural macros [Rust Latam conference, Montevideo Uruguay, March 2019]

Rust Latam: procedural macros workshop This repo contains a selection of projects designed to learn to write Rust procedural macros — Rust code that g

David Tolnay 2.5k Dec 29, 2022
Thread-safe clone-on-write container for fast concurrent writing and reading.

sync_cow Thread-safe clone-on-write container for fast concurrent writing and reading. SyncCow is a container for concurrent writing and reading of da

null 40 Jan 16, 2023
Compact, clone-on-write vector and string.

ecow Compact, clone-on-write vector and string. Types An EcoVec is a reference-counted clone-on-write vector. It takes up two words of space (= 2 usiz

Typst 78 Apr 18, 2023
A simple heads or tails (cross and pile) on Solana

cross-pile (Coin flip) A heads or tails (cross and pile) on Solana. It's meant to serve as an example of how to use solrand, a randomness Oracle: http

Evan Marshall 22 Nov 19, 2022