Skip to content

Commit

Permalink
token: Add Zig implementation (#15)
Browse files Browse the repository at this point in the history
* token: Add Zig implementation

#### Problem

There's only a Rust implementation of SPL Token in Rosetta, but we can
write a token program in many other languages.

#### Summary of changes

Add a simple SPL Token clone in Zig. It does not have multisig support
just yet, but it has most of the functionality needed.

CU usage is much lower than the Rust version. We might be able to
improve it more if we can make pubkey comparisons cheaper. Each one
seems to use ~30 CUs currently.

* Run zig fmt
  • Loading branch information
joncinque authored Nov 15, 2024
1 parent 2817544 commit 2451bde
Show file tree
Hide file tree
Showing 11 changed files with 1,377 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
name: Run tests against Zig implementations
strategy:
matrix:
program: [helloworld, transfer-lamports, cpi]
program: [helloworld, transfer-lamports, cpi, token]
fail-fast: false
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,33 +197,39 @@ Token program.
| Language | CU Usage |
| --- | --- |
| Rust | 1115 |
| Zig | 165 |

* Initialize Account

| Language | CU Usage |
| --- | --- |
| Rust | 2071 |
| Zig | 189 |

* Mint To

| Language | CU Usage |
| --- | --- |
| Rust | 2189 |
| Zig | 215 |

* Transfer

| Language | CU Usage |
| --- | --- |
| Rust | 2208 |
| Zig | 205 |

* Burn

| Language | CU Usage |
| --- | --- |
| Rust | 2045 |
| Zig | 175 |

* Close Account

| Language | CU Usage |
| --- | --- |
| Rust | 1483 |
| Zig | 291 |
5 changes: 3 additions & 2 deletions test-zig.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash

PROGRAM_NAME="$1"
ZIG="$2"
#ZIG="$2"
PARAMS=("$@")
ROOT_DIR="$(cd "$(dirname "$0")"; pwd)"
if [[ -z "$ZIG" ]]; then
ZIG="$ROOT_DIR/solana-zig/zig"
Expand All @@ -11,4 +12,4 @@ set -e
PROGRAM_DIR=$ROOT_DIR/$PROGRAM_NAME
cd $PROGRAM_DIR/zig
$ZIG build --summary all -freference-trace --verbose
SBF_OUT_DIR="$PROGRAM_DIR/zig/zig-out/lib" cargo test --manifest-path "$PROGRAM_DIR/Cargo.toml"
SBF_OUT_DIR="$PROGRAM_DIR/zig/zig-out/lib" cargo test --manifest-path "$PROGRAM_DIR/Cargo.toml" "${PARAMS[@]:2}"
2 changes: 0 additions & 2 deletions token/tests/assert_instruction_count.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(feature = "test-sbf")]

mod action;
use {
solana_program_test::{processor, tokio, ProgramTest},
Expand Down
24 changes: 24 additions & 0 deletions token/zig/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const std = @import("std");
const solana = @import("solana-program-sdk");

pub fn build(b: *std.Build) !void {
const target = b.resolveTargetQuery(solana.sbf_target);
const optimize = .ReleaseFast;

//const dep_opts = .{ .target = target, .optimize = optimize };
//const solana_lib_dep = b.dependency("solana-program-library", dep_opts);
//const solana_lib_mod = solana_lib_dep.module("solana-program-library");

const program = b.addSharedLibrary(.{
.name = "spl_token",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});

//program.root_module.addImport("solana-program-library", solana_lib_mod);

_ = solana.buildProgram(b, program, target, optimize);

b.installArtifact(program);
}
38 changes: 38 additions & 0 deletions token/zig/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.{
.name = "solana-program-rosetta-token-zig",
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.13.0",

// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
.minimum_zig_version = "0.13.0",

// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.@"solana-program-sdk" = .{
.url = "https://github.com/joncinque/solana-program-sdk-zig/archive/refs/tags/v0.15.0.tar.gz",
.hash = "1220c255d7d80a59251d901da4d2982eb660d099680c1207b14f51078987c655c979",
},
},

// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package.
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
// For example...
"build.zig",
"build.zig.zon",
"src",
"../../LICENSE",
"../../README.md",
},
}
140 changes: 140 additions & 0 deletions token/zig/src/error.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const sol = @import("solana-program-sdk");

pub const TokenError = error{
NotRentExempt,
InsufficientFunds,
InvalidMint,
MintMismatch,
OwnerMismatch,
FixedSupply,
AlreadyInUse,
InvalidNumberOfProvidedSigners,
InvalidNumberOfRequiredSigners,
UninitializedState,
NativeNotSupported,
NonNativeHasBalance,
InvalidInstruction,
InvalidState,
Overflow,
AuthorityTypeNotSupported,
MintCannotFreeze,
AccountFrozen,
MintDecimalsMismatch,
NonNativeNotSupported,
// generic program errors
InvalidArgument,
InvalidInstructionData,
InvalidAccountData,
AccountDataTooSmall,
//InsufficientFunds,
IncorrectProgramId,
MissingRequiredSignature,
AccountAlreadyInitialized,
UninitializedAccount,
NotEnoughAccountKeys,
AccountBorrowFailed,
MaxSeedLengthExceeded,
InvalidSeeds,
BorshIoError,
AccountNotRentExempt,
UnsupportedSysvar,
IllegalOwner,
MaxAccountsDataAllocationsExceeded,
InvalidRealloc,
MaxInstructionTraceLengthExceeded,
BuiltinProgramsMustConsumeComputeUnits,
InvalidAccountOwner,
ArithmeticOverflow,
Immutable,
IncorrectAuthority,
};

pub fn logError(e: TokenError) void {
switch (e) {
TokenError.NotRentExempt => {
sol.log("Error: Lamport balance below rent-exempt threshold");
},
TokenError.InsufficientFunds => {
sol.log("Error: insufficient funds");
},
TokenError.InvalidMint => {
sol.log("Error: Invalid Mint");
},
TokenError.MintMismatch => {
sol.log("Error: Account not associated with this Mint");
},
TokenError.OwnerMismatch => {
sol.log("Error: owner does not match");
},
TokenError.FixedSupply => {
sol.log("Error: the total supply of this token is fixed");
},
TokenError.AlreadyInUse => {
sol.log("Error: account or token already in use");
},
TokenError.InvalidNumberOfProvidedSigners => {
sol.log("Error: Invalid number of provided signers");
},
TokenError.InvalidNumberOfRequiredSigners => {
sol.log("Error: Invalid number of required signers");
},
TokenError.UninitializedState => {
sol.log("Error: State is uninitialized");
},
TokenError.NativeNotSupported => {
sol.log("Error: Instruction does not support native tokens");
},
TokenError.NonNativeHasBalance => {
sol.log("Error: Non-native account can only be closed if its balance is zero");
},
TokenError.InvalidInstruction => {
sol.log("Error: Invalid instruction");
},
TokenError.InvalidState => {
sol.log("Error: Invalid account state for operation");
},
TokenError.Overflow => {
sol.log("Error: Operation overflowed");
},
TokenError.AuthorityTypeNotSupported => {
sol.log("Error: Account does not support specified authority type");
},
TokenError.MintCannotFreeze => {
sol.log("Error: This token mint cannot freeze accounts");
},
TokenError.AccountFrozen => {
sol.log("Error: Account is frozen");
},
TokenError.MintDecimalsMismatch => {
sol.log("Error: decimals different from the Mint decimals");
},
TokenError.NonNativeNotSupported => {
sol.log("Error: Instruction does not support non-native tokens");
},
TokenError.InvalidArgument => {},
TokenError.InvalidInstructionData => {},
TokenError.InvalidAccountData => {},
TokenError.AccountDataTooSmall => {},
TokenError.InsufficientFunds => {},
TokenError.IncorrectProgramId => {},
TokenError.MissingRequiredSignature => {},
TokenError.AccountAlreadyInitialized => {},
TokenError.UninitializedAccount => {},
TokenError.NotEnoughAccountKeys => {},
TokenError.AccountBorrowFailed => {},
TokenError.MaxSeedLengthExceeded => {},
TokenError.InvalidSeeds => {},
TokenError.BorshIoError => {},
TokenError.AccountNotRentExempt => {},
TokenError.UnsupportedSysvar => {},
TokenError.IllegalOwner => {},
TokenError.MaxAccountsDataAllocationsExceeded => {},
TokenError.InvalidRealloc => {},
TokenError.MaxInstructionTraceLengthExceeded => {},
TokenError.BuiltinProgramsMustConsumeComputeUnits => {},
TokenError.InvalidAccountOwner => {},
TokenError.ArithmeticOverflow => {},
TokenError.Immutable => {},
TokenError.IncorrectAuthority => {},
}
}
4 changes: 4 additions & 0 deletions token/zig/src/id.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const PublicKey = @import("solana-program-sdk").PublicKey;
pub const id = PublicKey.comptimeFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
pub const native_mint_id = PublicKey.comptimeFromBase58("So11111111111111111111111111111111111111112");
pub const system_program_id = PublicKey.comptimeFromBase58("11111111111111111111111111111111");
Loading

0 comments on commit 2451bde

Please sign in to comment.