diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fc90c9..8648436 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: diff --git a/README.md b/README.md index 0ee3071..5d7b98f 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/test-zig.sh b/test-zig.sh index 05338b6..30c753b 100755 --- a/test-zig.sh +++ b/test-zig.sh @@ -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" @@ -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}" diff --git a/token/tests/assert_instruction_count.rs b/token/tests/assert_instruction_count.rs index 009fb1e..618b2c4 100644 --- a/token/tests/assert_instruction_count.rs +++ b/token/tests/assert_instruction_count.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "test-sbf")] - mod action; use { solana_program_test::{processor, tokio, ProgramTest}, diff --git a/token/zig/build.zig b/token/zig/build.zig new file mode 100644 index 0000000..7733836 --- /dev/null +++ b/token/zig/build.zig @@ -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); +} diff --git a/token/zig/build.zig.zon b/token/zig/build.zig.zon new file mode 100644 index 0000000..bec40ec --- /dev/null +++ b/token/zig/build.zig.zon @@ -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", + }, +} diff --git a/token/zig/src/error.zig b/token/zig/src/error.zig new file mode 100644 index 0000000..356dcc9 --- /dev/null +++ b/token/zig/src/error.zig @@ -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 => {}, + } +} diff --git a/token/zig/src/id.zig b/token/zig/src/id.zig new file mode 100644 index 0000000..a43c505 --- /dev/null +++ b/token/zig/src/id.zig @@ -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"); diff --git a/token/zig/src/instruction.zig b/token/zig/src/instruction.zig new file mode 100644 index 0000000..430720d --- /dev/null +++ b/token/zig/src/instruction.zig @@ -0,0 +1,521 @@ +const std = @import("std"); +const COption = @import("state.zig").COption; +const PublicKey = @import("solana-program-sdk").PublicKey; + +pub const AuthorityType = enum(u8) { + /// Authority to mint new tokens + mint_tokens, + /// Authority to freeze any account associated with the Mint + freeze_account, + /// Owner of a given token account + account_owner, + /// Authority to close a token account + close_account, +}; + +pub const InstructionDiscriminant = enum(u8) { + initialize_mint, + initialize_account, + initialize_multisig, + transfer, + approve, + revoke, + set_authority, + mint_to, + burn, + close_account, + freeze_account, + thaw_account, + transfer_checked, + approve_checked, + mint_to_checked, + burn_checked, + initialize_account_2, + sync_native, + initialize_account_3, + initialize_multisig_2, + initialize_mint_2, + get_account_data_size, + initialize_immutable_owner, + amount_to_ui_amount, + ui_amount_to_amount, +}; + +pub fn IxOption(T: type) type { + return packed struct { + is_some: u8, + value: T, + const Self = @This(); + pub fn fromOptional(v: ?T) Self { + if (v) |value| { + return Self.fromValue(value); + } else { + return Self.fromNull(); + } + } + pub fn fromValue(value: T) Self { + return Self{ + .is_some = 1, + .value = value, + }; + } + pub fn fromNull() Self { + return Self{ + .is_some = 0, + .value = std.mem.zeroes(T), + }; + } + pub fn asOptional(self: *const Self) ?T { + if (self.is_some == 0) { + return null; + } else { + return self.value; + } + } + pub fn toCOption(self: Self) COption(T) { + return COption(T){ + .is_some = self.is_some, + .value = self.value, + }; + } + }; +} + +pub const InitializeMintData = packed struct { + /// Number of base 10 digits to the right of the decimal place. + decimals: u8, + /// The authority/multisignature to mint tokens. + mint_authority: PublicKey, + /// The freeze authority/multisignature of the mint. + freeze_authority: IxOption(PublicKey), +}; + +pub const AmountData = packed struct { + /// The amount of tokens to process. + amount: u64, +}; + +pub const Instruction = union(InstructionDiscriminant) { + /// Initializes a new mint and optionally deposits all the newly minted + /// tokens in an account. + /// + /// The `InitializeMint` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + /// 1. `[]` Rent sysvar + /// + initialize_mint: InitializeMintData, + /// Initializes a new account to hold tokens. If this account is associated + /// with the native mint then the token balance of the initialized account + /// will be equal to the amount of SOL in the account. If this account is + /// associated with another mint, that mint must be initialized before this + /// command can succeed. + /// + /// The `InitializeAccount` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + /// 2. `[]` The new account's owner/multisignature. + /// 3. `[]` Rent sysvar + initialize_account: void, + /// Initializes a multisignature account with N provided signers. + /// + /// Multisignature accounts can used in place of any single owner/delegate + /// accounts in any token instruction that require an owner/delegate to be + /// present. The variant field represents the number of signers (M) + /// required to validate this multisignature account. + /// + /// The `InitializeMultisig` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The multisignature account to initialize. + /// 1. `[]` Rent sysvar + /// 2. ..2+N. `[]` The signer accounts, must equal to N where 1 <= N <= + /// 11. + initialize_multisig: packed struct { + /// The number of signers (M) required to validate this multisignature + /// account. + m: u8, + }, + /// Transfers tokens from one account to another either directly or via a + /// delegate. If this account is associated with the native mint then equal + /// amounts of SOL and Tokens will be transferred to the destination + /// account. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[writable]` The destination account. + /// 2. `[signer]` The source account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[writable]` The destination account. + /// 2. `[]` The source account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + transfer: AmountData, + /// Approves a delegate. A delegate is given the authority over tokens on + /// behalf of the source account's owner. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[]` The delegate. + /// 2. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The delegate. + /// 2. `[]` The source account's multisignature owner. + /// 3. ..3+M `[signer]` M signer accounts + approve: AmountData, + /// Revokes the delegate's authority. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The source account's multisignature owner. + /// 2. ..2+M `[signer]` M signer accounts + revoke: void, + /// Sets a new authority of a mint or account. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint or account to change the authority of. + /// 1. `[signer]` The current authority of the mint or account. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint or account to change the authority of. + /// 1. `[]` The mint's or account's current multisignature authority. + /// 2. ..2+M `[signer]` M signer accounts + set_authority: packed struct { + /// The type of authority to update. + authority_type: AuthorityType, + /// The new authority. + new_authority_id: IxOption(PublicKey), + }, + /// Mints new tokens to an account. The native mint does not support + /// minting. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[signer]` The mint's minting authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[]` The mint's multisignature mint-tokens authority. + /// 3. ..3+M `[signer]` M signer accounts. + mint_to: AmountData, + /// Burns tokens by removing them from an account. `Burn` does not support + /// accounts associated with the native mint, use `CloseAccount` instead. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[signer]` The account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[]` The account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + burn: AmountData, + /// Close an account by transferring all its SOL to the destination account. + /// Non-native accounts may only be closed if its token amount is zero. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to close. + /// 1. `[writable]` The destination account. + /// 2. `[signer]` The account's owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to close. + /// 1. `[writable]` The destination account. + /// 2. `[]` The account's multisignature owner. + /// 3. ..3+M `[signer]` M signer accounts. + close_account: void, + /// Freeze an Initialized account using the Mint's freeze_authority (if + /// set). + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[signer]` The mint freeze authority. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[]` The mint's multisignature freeze authority. + /// 3. ..3+M `[signer]` M signer accounts. + freeze_account: void, + /// Thaw a Frozen account using the Mint's freeze_authority (if set). + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[signer]` The mint freeze authority. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[]` The mint's multisignature freeze authority. + /// 3. ..3+M `[signer]` M signer accounts. + thaw_account: void, + /// Transfers tokens from one account to another either directly or via a + /// delegate. If this account is associated with the native mint then equal + /// amounts of SOL and Tokens will be transferred to the destination + /// account. + /// + /// This instruction differs from Transfer in that the token mint and + /// decimals value is checked by the caller. This may be useful when + /// creating transactions offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[signer]` The source account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[]` The source account's multisignature owner/delegate. + /// 4. ..4+M `[signer]` M signer accounts. + transfer_checked: packed struct { + /// The amount of tokens to transfer. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + /// Approves a delegate. A delegate is given the authority over tokens on + /// behalf of the source account's owner. + /// + /// This instruction differs from Approve in that the token mint and + /// decimals value is checked by the caller. This may be useful when + /// creating transactions offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[]` The delegate. + /// 3. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[]` The delegate. + /// 3. `[]` The source account's multisignature owner. + /// 4. ..4+M `[signer]` M signer accounts + approve_checked: packed struct { + /// The amount of tokens the delegate is approved for. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + /// Mints new tokens to an account. The native mint does not support + /// minting. + /// + /// This instruction differs from MintTo in that the decimals value is + /// checked by the caller. This may be useful when creating transactions + /// offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[signer]` The mint's minting authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[]` The mint's multisignature mint-tokens authority. + /// 3. ..3+M `[signer]` M signer accounts. + mint_to_checked: packed struct { + /// The amount of new tokens to mint. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + /// Burns tokens by removing them from an account. `BurnChecked` does not + /// support accounts associated with the native mint, use `CloseAccount` + /// instead. + /// + /// This instruction differs from Burn in that the decimals value is checked + /// by the caller. This may be useful when creating transactions offline or + /// within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[signer]` The account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[]` The account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + burn_checked: packed struct { + /// The amount of tokens to burn. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + /// Like InitializeAccount, but the owner pubkey is passed via instruction data + /// rather than the accounts list. This variant may be preferable when using + /// Cross Program Invocation from an instruction that does not need the owner's + /// `AccountInfo` otherwise. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + /// 3. `[]` Rent sysvar + initialize_account_2: packed struct { + /// The new account's owner/multisignature. + owner_id: PublicKey, + }, + /// Given a wrapped / native token account (a token account containing SOL) + /// updates its amount field based on the account's underlying `lamports`. + /// This is useful if a non-wrapped SOL account uses `system_instruction::transfer` + /// to move lamports to a wrapped token account, and needs to have its token + /// `amount` field updated. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The native token account to sync with its underlying lamports. + sync_native: void, + /// Like InitializeAccount2, but does not require the Rent sysvar to be provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + initialize_account_3: packed struct { + /// The new account's owner/multisignature. + owner_id: PublicKey, + }, + /// Like InitializeMultisig, but does not require the Rent sysvar to be provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The multisignature account to initialize. + /// 1. ..1+N. `[]` The signer accounts, must equal to N where 1 <= N <= + /// 11. + initialize_multisig_2: packed struct { + /// The number of signers (M) required to validate this multisignature + /// account. + m: u8, + }, + /// Like InitializeMint, but does not require the Rent sysvar to be provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + /// + initialize_mint_2: packed struct { + /// Number of base 10 digits to the right of the decimal place. + decimals: u8, + /// The authority/multisignature to mint tokens. + mint_authority_id: PublicKey, + /// The freeze authority/multisignature of the mint. + freeze_authority_id: IxOption(PublicKey), + }, + /// Gets the required size of an account for the given mint as a little-endian + /// `u64`. + /// + /// Return data can be fetched using `sol_get_return_data` and deserializing + /// the return data as a little-endian `u64`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + get_account_data_size: void, + /// Initialize the Immutable Owner extension for the given token account + /// + /// Fails if the account has already been initialized, so must be called before + /// `InitializeAccount`. + /// + /// No-ops in this version of the program, but is included for compatibility + /// with the Associated Token Account program. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// + /// Data expected by this instruction: + /// None + initialize_immutable_owner: void, + /// Convert an Amount of tokens to a UiAmount `string`, using the given mint. + /// In this version of the program, the mint can only specify the number of decimals. + /// + /// Fails on an invalid mint. + /// + /// Return data can be fetched using `sol_get_return_data` and deserialized with + /// `String::from_utf8`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + amount_to_ui_amount: packed struct { + /// The amount of tokens to reformat. + amount: u64, + }, + /// Convert a UiAmount of tokens to a little-endian `u64` raw Amount, using the given mint. + /// In this version of the program, the mint can only specify the number of decimals. + /// + /// Return data can be fetched using `sol_get_return_data` and deserializing + /// the return data as a little-endian `u64`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + ui_amount_to_amount: packed struct { + /// The ui_amount of tokens to reformat. + ui_amount: [*:0]u8, + }, +}; diff --git a/token/zig/src/main.zig b/token/zig/src/main.zig new file mode 100644 index 0000000..16b3d34 --- /dev/null +++ b/token/zig/src/main.zig @@ -0,0 +1,522 @@ +const sol = @import("solana-program-sdk"); +const PublicKey = sol.PublicKey; +const Rent = sol.Rent; + +const ix = @import("instruction.zig"); +const state = @import("state.zig"); +const TokenError = @import("error.zig").TokenError; +const native_mint_id = @import("id.zig").native_mint_id; +const system_program_id = @import("id.zig").system_program_id; + +export fn entrypoint(input: [*]u8) u64 { + var context = sol.Context.load(input) catch return 1; + processInstruction(context.program_id, context.accounts[0..context.num_accounts], context.data) catch |err| return @intFromError(err); + return 0; +} + +fn processInstruction(program_id: *align(1) PublicKey, accounts: []sol.Account, data: []const u8) TokenError!void { + const instruction_type: *const ix.InstructionDiscriminant = @ptrCast(data); + switch (instruction_type.*) { + ix.InstructionDiscriminant.initialize_mint => { + //sol.log("Instruction: InitializeMint"); + if (accounts.len < 2) { + return TokenError.NotEnoughAccountKeys; + } + const init: *align(1) const ix.InitializeMintData = @ptrCast(data[1..]); + const mint_account = accounts[0]; + const rent_sysvar = accounts[1]; + if (!rent_sysvar.id().equals(Rent.id)) { + return TokenError.InvalidAccountData; + } + + if (mint_account.dataLen() != state.Mint.len) { + return TokenError.InvalidAccountData; + } + + var mint: *align(1) state.Mint = @ptrCast(mint_account.data()); + if (mint.is_initialized == 1) { + return TokenError.AlreadyInUse; + } + + const rent: *align(1) Rent.Data = @ptrCast(rent_sysvar.data()); + if (!rent.isExempt(mint_account.lamports().*, mint_account.dataLen())) { + return TokenError.NotRentExempt; + } + + mint.mint_authority = state.COption(PublicKey).fromValue(init.mint_authority); + mint.decimals = init.decimals; + mint.is_initialized = 1; + mint.freeze_authority = init.freeze_authority.toCOption(); + }, + ix.InstructionDiscriminant.initialize_account => { + //sol.log("Instruction: InitializeAccount"); + if (accounts.len < 4) { + return TokenError.NotEnoughAccountKeys; + } + const token_account = accounts[0]; + const mint_account = accounts[1]; + const owner = accounts[2]; + const rent_sysvar = accounts[3]; + const rent: *align(1) Rent.Data = @ptrCast(rent_sysvar.data()); + + if (token_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var account: *align(1) state.Account = @ptrCast(token_account.data()); + if (account.state != state.Account.State.uninitialized) { + return TokenError.AlreadyInUse; + } + + if (!rent.isExempt(token_account.lamports().*, token_account.dataLen())) { + return TokenError.NotRentExempt; + } + + account.mint = mint_account.id(); + account.owner = owner.id(); + //account.close_authority = state.COption(PublicKey).asNull(); + //account.delegate = state.COption(PublicKey).asNull(); + //account.delegated_amount = 0; + account.state = state.Account.State.initialized; + if (mint_account.id().equals(native_mint_id)) { + const rent_exempt_reserve = rent.getMinimumBalance(token_account.dataLen()); + account.is_native = state.COption(u64).fromValue(rent_exempt_reserve); + const amount = @subWithOverflow(token_account.lamports().*, rent_exempt_reserve); + if (amount[1] != 0) { + return TokenError.Overflow; + } + account.amount = amount[0]; + } else { + if (!mint_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + const mint: *align(1) state.Mint = @ptrCast(mint_account.data()); + if (mint_account.dataLen() != state.Mint.len) { + return TokenError.InvalidAccountData; + } + if (mint.is_initialized != 1) { + return TokenError.UninitializedState; + } + + //account.is_native = state.COption(u64).asNull(); + //account.amount = 0; + } + }, + ix.InstructionDiscriminant.initialize_multisig => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.transfer => { + //sol.log("Instruction: Transfer"); + if (accounts.len < 3) { + return TokenError.NotEnoughAccountKeys; + } + const transfer_data: *align(1) const ix.AmountData = @ptrCast(data[1..]); + const source_account = accounts[0]; + const destination_account = accounts[1]; + const authority_account = accounts[2]; + + if (source_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var source: *align(1) state.Account = @ptrCast(source_account.data()); + if (source.state == state.Account.State.uninitialized) { + return TokenError.UninitializedState; + } + if (source.state == state.Account.State.frozen) { + return TokenError.AccountFrozen; + } + + if (destination_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var destination: *align(1) state.Account = @ptrCast(destination_account.data()); + if (destination.state == state.Account.State.uninitialized) { + return TokenError.UninitializedState; + } + if (destination.state == state.Account.State.frozen) { + return TokenError.AccountFrozen; + } + + if (source.amount < transfer_data.amount) { + return TokenError.InsufficientFunds; + } + if (!source.mint.equals(destination.mint)) { + return TokenError.MintMismatch; + } + + const self_transfer = source_account.id().equals(destination_account.id()); + //match source_account.delegate { + // COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { + // Self::validate_owner( + // program_id, + // delegate, + // authority_info, + // account_info_iter.as_slice(), + // )?; + // if source_account.delegated_amount < amount { + // return Err(TokenError::InsufficientFunds.into()); + // } + // if !self_transfer { + // source_account.delegated_amount = source_account + // .delegated_amount + // .checked_sub(amount) + // .ok_or(TokenError::Overflow)?; + // if source_account.delegated_amount == 0 { + // source_account.delegate = COption::None; + // } + // } + // } + try validateOwner( + program_id, + &source.owner, + authority_account, + accounts[3..], + ); + + if (self_transfer or transfer_data.amount == 0) { + if (!source_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + if (!destination_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + return; + } + + source.amount -= transfer_data.amount; + destination.amount += transfer_data.amount; + + if (source.isNative()) { + source_account.lamports().* -= transfer_data.amount; + destination_account.lamports().* += transfer_data.amount; + } + }, + ix.InstructionDiscriminant.approve => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.revoke => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.set_authority => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.mint_to => { + //sol.log("Instruction: MintTo"); + if (accounts.len < 3) { + return TokenError.NotEnoughAccountKeys; + } + const ix_data: *align(1) const ix.AmountData = @ptrCast(data[1..]); + const mint_account = accounts[0]; + const destination_account = accounts[1]; + const authority_account = accounts[2]; + if (destination_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var destination: *align(1) state.Account = @ptrCast(destination_account.data()); + if (destination.state == state.Account.State.uninitialized) { + return TokenError.UninitializedState; + } + if (destination.state == state.Account.State.frozen) { + return TokenError.AccountFrozen; + } + + if (destination.isNative()) { + return TokenError.NativeNotSupported; + } + if (!mint_account.id().equals(destination.mint)) { + return TokenError.MintMismatch; + } + + if (mint_account.dataLen() != state.Mint.len) { + return TokenError.InvalidAccountData; + } + var mint: *align(1) state.Mint = @ptrCast(mint_account.data()); + if (mint.is_initialized != 1) { + return TokenError.UninitializedState; + } + //if let Some(expected_decimals) = expected_decimals { + // if expected_decimals != mint.decimals { + // return Err(TokenError::MintDecimalsMismatch.into()); + // } + //} + + if (mint.mint_authority.is_some == 0) { + return TokenError.FixedSupply; + } + + try validateOwner( + program_id, + &mint.mint_authority.value, + authority_account, + accounts[3..], + ); + + if (ix_data.amount == 0) { + if (!mint_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + if (!destination_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + } + + const destination_amount = @addWithOverflow(destination.amount, ix_data.amount); + if (destination_amount[1] != 0) { + return TokenError.Overflow; + } + destination.amount = destination_amount[0]; + const supply = @addWithOverflow(mint.supply, ix_data.amount); + if (supply[1] != 0) { + return TokenError.Overflow; + } + mint.supply = supply[0]; + }, + ix.InstructionDiscriminant.burn => { + if (accounts.len < 3) { + return TokenError.NotEnoughAccountKeys; + } + const burn_data: *align(1) const ix.AmountData = @ptrCast(data[1..]); + const source_account = accounts[0]; + const mint_account = accounts[1]; + const authority_account = accounts[2]; + + if (source_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var source: *align(1) state.Account = @ptrCast(source_account.data()); + if (source.state == state.Account.State.uninitialized) { + return TokenError.UninitializedState; + } + if (source.state == state.Account.State.frozen) { + return TokenError.AccountFrozen; + } + + if (source.isNative()) { + return TokenError.NativeNotSupported; + } + if (!mint_account.id().equals(source.mint)) { + return TokenError.MintMismatch; + } + + if (mint_account.dataLen() != state.Mint.len) { + return TokenError.InvalidAccountData; + } + var mint: *align(1) state.Mint = @ptrCast(mint_account.data()); + if (mint.is_initialized != 1) { + return TokenError.UninitializedState; + } + + if (source.amount < burn_data.amount) { + return TokenError.InsufficientFunds; + } + + //if let Some(expected_decimals) = expected_decimals { + // if expected_decimals != mint.decimals { + // return Err(TokenError::MintDecimalsMismatch.into()); + // } + //} + + //if !source_account.is_owned_by_system_program_or_incinerator() { + //match source_account.delegate { + //COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { + //Self::validate_owner( + //program_id, + //delegate, + //authority_info, + //account_info_iter.as_slice(), + //)?; + + //if source_account.delegated_amount < amount { + //return Err(TokenError::InsufficientFunds.into()); + //} + //source_account.delegated_amount = source_account + //.delegated_amount + //.checked_sub(amount) + //.ok_or(TokenError::Overflow)?; + //if source_account.delegated_amount == 0 { + //source_account.delegate = COption::None; + //} + //} + //} + //} + try validateOwner( + program_id, + &source.owner, + authority_account, + accounts[3..], + ); + + if (burn_data.amount == 0) { + if (!mint_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + if (!source_account.ownerId().equals(program_id.*)) { + return TokenError.IllegalOwner; + } + } + + source.amount -= burn_data.amount; + mint.supply -= burn_data.amount; + }, + ix.InstructionDiscriminant.close_account => { + if (accounts.len < 3) { + return TokenError.NotEnoughAccountKeys; + } + const source_account = accounts[0]; + const destination_account = accounts[1]; + const authority_account = accounts[2]; + + if (source_account.dataLen() != state.Account.len) { + return TokenError.InvalidAccountData; + } + var source: *align(1) state.Account = @ptrCast(source_account.data()); + if (source.state == state.Account.State.uninitialized) { + return TokenError.UninitializedState; + } + if (source.state == state.Account.State.frozen) { + return TokenError.AccountFrozen; + } + if (source_account.id().equals(destination_account.id())) { + return TokenError.InvalidAccountData; + } + if (!source.isNative() and source.amount != 0) { + return TokenError.NonNativeHasBalance; + } + + const authority = if (source.close_authority.is_some != 0) + source.close_authority.value + else + source.owner; + + //if !source_account.is_owned_by_system_program_or_incinerator() { + //Self::validate_owner( + //program_id, + //&authority, + //authority_info, + //account_info_iter.as_slice(), + //)?; + //} else if !solana_program::incinerator::check_id(destination_account_info.key) { + //return Err(ProgramError::InvalidAccountData); + //} + + try validateOwner( + program_id, + &authority, + authority_account, + accounts[3..], + ); + + destination_account.lamports().* += source_account.lamports().*; + source_account.lamports().* = 0; + source_account.assign(system_program_id); + source_account.reallocUnchecked(0); + }, + ix.InstructionDiscriminant.freeze_account => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.thaw_account => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.transfer_checked => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.approve_checked => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.mint_to_checked => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.burn_checked => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.initialize_account_2 => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.sync_native => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.initialize_account_3 => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.initialize_multisig_2 => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.initialize_mint_2 => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.get_account_data_size => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.initialize_immutable_owner => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.amount_to_ui_amount => { + return TokenError.InvalidState; + }, + ix.InstructionDiscriminant.ui_amount_to_amount => { + return TokenError.InvalidState; + }, + } +} + +fn validateOwner( + program_id: *align(1) const PublicKey, + expected_owner: *align(1) const PublicKey, + owner_account: sol.Account, + _: []sol.Account, +) TokenError!void { + if (!expected_owner.equals(owner_account.id())) { + return TokenError.OwnerMismatch; + } + if (program_id.equals(owner_account.ownerId()) and owner_account.dataLen() == state.Multisig.len) { + //let multisig = Multisig::unpack(&owner_account_info.data.borrow())?; + //let mut num_signers = 0; + //let mut matched = [false; MAX_SIGNERS]; + //for signer in signers.iter() { + // for (position, key) in multisig.signers[0..multisig.n as usize].iter().enumerate() { + // if Self::cmp_pubkeys(key, signer.key) && !matched[position] { + // if !signer.is_signer { + // return Err(ProgramError::MissingRequiredSignature); + // } + // matched[position] = true; + // num_signers += 1; + // } + // } + //} + //if num_signers < multisig.m { + return TokenError.MissingRequiredSignature; + //} + } else if (!owner_account.isSigner()) { + return TokenError.MissingRequiredSignature; + } +} + +test { + const std = @import("std"); + std.testing.refAllDecls(@This()); +} + +// TODO make public key comparisons faster +comptime { + asm ( + \\.global my_func; + \\.type my_func, @function; + \\my_func: + \\ ldxdw r3, [r1 + 0] + \\ ldxdw r4, [r2 + 0] + \\ jne r3, r4, error + \\ ldxdw r3, [r1 + 8] + \\ ldxdw r4, [r2 + 8] + \\ jne r3, r4, error + \\ ldxdw r3, [r1 + 16] + \\ ldxdw r4, [r2 + 16] + \\ jne r3, r4, error + \\ ldxdw r3, [r1 + 24] + \\ ldxdw r4, [r2 + 24] + \\ jne r3, r4, error + \\ mov64 r0, 1 + \\ exit + \\error: + \\ exit + ); +} +extern fn my_func(a: *align(1) const PublicKey, b: *align(1) const PublicKey) bool; diff --git a/token/zig/src/state.zig b/token/zig/src/state.zig new file mode 100644 index 0000000..5ce26b7 --- /dev/null +++ b/token/zig/src/state.zig @@ -0,0 +1,118 @@ +const std = @import("std"); +const PublicKey = @import("solana-program-sdk").PublicKey; + +pub const Mint = packed struct { + pub const len = 82; + + mint_authority: COption(PublicKey), + supply: u64, + decimals: u8, + is_initialized: u8, + freeze_authority: COption(PublicKey), +}; + +pub const Account = packed struct { + pub const len = 165; + + pub const State = enum(u8) { + uninitialized, + initialized, + frozen, + }; + + mint: PublicKey, + owner: PublicKey, + amount: u64, + delegate: COption(PublicKey), + state: Account.State, + is_native: COption(u64), + delegated_amount: u64, + close_authority: COption(PublicKey), + + pub fn isNative(self: Account) bool { + return self.is_native.is_some != 0; + } +}; + +pub fn COption(T: type) type { + return packed struct { + is_some: u32, + value: T, + const Self = @This(); + pub fn fromOptional(v: ?T) Self { + if (v) |value| { + return Self.fromValue(value); + } else { + return Self.asNull(); + } + } + pub fn fromValue(value: T) Self { + return Self{ + .is_some = 1, + .value = value, + }; + } + pub fn asNull() Self { + return Self{ + .is_some = 0, + .value = std.mem.zeroes(T), + }; + } + pub fn asOptional(self: *const Self) ?T { + if (self.is_some == 0) { + return null; + } else { + return self.value; + } + } + }; +} + +pub const Multisig = packed struct { + pub const len = 355; +}; + +test "Mint: bitCast" { + const mint = Mint{ + .authority = COption(PublicKey).fromOptional(std.mem.bytesToValue(PublicKey, &[_]u8{1} ** 32)), + .supply = 42, + .decimals = 7, + .is_initialized = 1, + .freeze_authority = COption(PublicKey).fromOptional(std.mem.bytesToValue(PublicKey, &[_]u8{2} ** 32)), + }; + const mint_buffer = [_]u8{ + 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 42, 0, 0, 0, 0, 0, 0, 0, 7, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + }; + try std.testing.expectEqualSlices(u8, &mint_buffer, std.mem.asBytes(&mint)[0..82]); + const cast_mint: Mint = @bitCast(mint_buffer); + try std.testing.expectEqual(cast_mint, mint); +} + +test "Mint: cast with padding" { + const mint = Mint{ + .authority = COption(PublicKey).new(std.mem.bytesToValue(PublicKey, &[_]u8{1} ** 32)), + .supply = 42, + .decimals = 7, + .is_initialized = 1, + .freeze_authority = COption(PublicKey).new(std.mem.bytesToValue(PublicKey, &[_]u8{2} ** 32)), + }; + const mint_buffer = [_]u8{ + 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 42, 0, 0, 0, 0, 0, 0, 0, 7, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + }; + try std.testing.expectEqualSlices(u8, mint_buffer[1..], std.mem.asBytes(&mint)[0..82]); + const cast_mint: *const Mint = @alignCast(@ptrCast(mint_buffer[1..])); + // can't test the whole thing because of post padding in struct + try std.testing.expectEqual(cast_mint.authority, mint.authority); +} + +test "COption: pubkey" { + const COptionKey = COption(PublicKey); + const none_key = COptionKey.new(null); + try std.testing.expectEqual(none_key.asOptional(), null); + const some_key = COptionKey.new(1); + try std.testing.expectEqual(some_key.asOptional(), 1); +}