Skip to main content

February prototype status

Time for a post on the state of the prototype!

First note is that syntax is not final. This is all Rust meant to compile to WASM similar to what the result of the Starstream language compiled to WASM would be. This makes prototyping easy and hopefully helps illustrate the semantics more concretely. The prototype runs the WASM within a scheduler written in Node.js, but it's compatible with browsers and WASM zkVMs. ABI glue is left out of the post.

The coordination script is where Starstream's design shines. The design naturally permits UTXO call chaining, including conditionals. Let's mint NFTs up to a specific supply amount then stop:

pub fn star_nft_mint_up_to(
// This parameter's type is a handle to a UTXO object.
// The scheduler routes calls to the correct sleeping UTXO code instance.
nft_contract: example_contract::StarNftMint,
desired_supply: u64,
owner: PublicKey,
) {
while nft_contract.get_supply() < desired_supply {
// In this example, we create many UTXOs with one NFT each. We could
// just as easily create one UTXO containing all NFTs minted by this
// call.
example_contract::PayToPublicKeyHash::new(owner)
.attach(nft_contract.prepare_to_mint());
}
}

As an example NFT we'll introduce StarNft. Tokens in Starstream are mainly defined by their conversion functions to/from their "intermediates". Intermediates are linear types, enforced by the Starstream compiler and scheduler to either be minted or properly burned rather than forgotten.

struct StarNftIntermediate {
pub id: u64,
}

starstream::token_export! {
for StarNftIntermediate;
mint fn starstream_mint_StarNft(this: Self) -> TokenStorage {
// Example of common assertion: only sanctioned coordination code
// can mint this NFT. This indirectly enforces that only intermediates
// produced by calls to `StarNftMint::prepare_to_mint` are minted.
assert!(starstream::coordination_code() == starstream::this_code());
TokenStorage { id: this.id, amount: 1 }
}
burn fn starstream_burn_StarNft(storage: TokenStorage) -> Self {
assert!(starstream::coordination_code() == starstream::this_code());
assert!(storage.amount == 1);
StarNftIntermediate { id: storage.id }
}
}

But where do StarNfts come from? A mint contract, of course:

pub struct StarNftMint {
supply: u64,
}

impl StarNftMint {
// `sleep` is supplied by the scheduler and suspends execution.
pub fn new(max_supply: u64, sleep: fn(&mut Self)) {
let mut this = StarNftMint { supply: 0 };
while this.supply < max_supply {
// While sleeping, get_supply and prepare_to_mint can be called.
sleep(&mut this);
// Code here can control the .resume() function of the UTXO, which
// can mutate it, equivalent to consuming the UTXO and producing
// the "next" UTXO state.
}
// When we hit here, the lifetime of the StarNftMint UTXO ends, but the
// StarNft tokens attached to PayToPublicKeyHash UTXOs remain valid.
}

// Some methods can query UTXOs but not step them or edit their memory.
pub fn get_supply(&self) -> u64 {
self.supply
}

// Some methods can mutate the UTXO, also equivalent to consuming/remaking
pub fn prepare_to_mint(&mut self) -> StarNftIntermediate {
// Note: TX will fail if StarNftIntermediate is dropped without
// being mint()ed, so supply += 1 is kept accurate.
self.supply += 1;
StarNftIntermediate { id: self.supply }
}
// ^ One &mut self function can also be 'promoted' to .resume() above,
// but it does make it less convenient to return a value.
}

Finally, a peek at PayToPublicKeyHash, which would be a very common UTXO type provided by standard Starstream:

pub struct PayToPublicKeyHash {
owner: PublicKey,
}

impl PayToPublicKeyHash {
pub fn new(owner: PublicKey, sleep: fn(&mut Self)) {
// It's currently the TX where the UTXO is created.
let mut this = PayToPublicKeyHash { owner };
sleep(&mut this);
// Now it's the TX where someone has requested to spend this UTXO.
// They are allowed to do that if that TX is signed by the owner we
// started with.
starstream::assert_tx_signed_by(owner);
// When the UTXO's lifetime ends, all its tokens are freed up, and then
// the calling coordination script must either put them directly into
// another UTXO or burn them according to that token's code, or else
// the TX will fail.
}

pub fn get_owner(&self) -> PublicKey {
self.owner
}

// Any token can be attached to PayToPublicKeyHash.
pub fn attach<T: Token>(&mut self, i: T::Intermediate) {
T::mint(i);
}
}