From 9668b14f19266de25584282eea331c88df0bc833 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 15 Nov 2024 14:23:41 +0100 Subject: [PATCH 1/2] 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. --- .github/workflows/main.yml | 2 +- README.md | 6 + test-zig.sh | 5 +- token/tests/assert_instruction_count.rs | 2 - token/zig/build.zig | 24 ++ token/zig/build.zig.zon | 38 ++ token/zig/src/error.zig | 140 +++++++ token/zig/src/id.zig | 4 + token/zig/src/instruction.zig | 521 ++++++++++++++++++++++++ token/zig/src/main.zig | 521 ++++++++++++++++++++++++ token/zig/src/state.zig | 119 ++++++ 11 files changed, 1377 insertions(+), 5 deletions(-) create mode 100644 token/zig/build.zig create mode 100644 token/zig/build.zig.zon create mode 100644 token/zig/src/error.zig create mode 100644 token/zig/src/id.zig create mode 100644 token/zig/src/instruction.zig create mode 100644 token/zig/src/main.zig create mode 100644 token/zig/src/state.zig 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..5b17339 --- /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..e1e1148 --- /dev/null +++ b/token/zig/src/main.zig @@ -0,0 +1,521 @@ +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..03ad4d9 --- /dev/null +++ b/token/zig/src/state.zig @@ -0,0 +1,119 @@ +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); +} From fd9bd9fb9e2e256a5b4771a504663c8a247aed70 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 15 Nov 2024 14:28:46 +0100 Subject: [PATCH 2/2] Run zig fmt --- token/zig/src/instruction.zig | 8 ++--- token/zig/src/main.zig | 65 ++++++++++++++++++----------------- token/zig/src/state.zig | 19 +++++----- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/token/zig/src/instruction.zig b/token/zig/src/instruction.zig index 5b17339..430720d 100644 --- a/token/zig/src/instruction.zig +++ b/token/zig/src/instruction.zig @@ -54,13 +54,13 @@ pub fn IxOption(T: type) type { } } pub fn fromValue(value: T) Self { - return Self { + return Self{ .is_some = 1, .value = value, }; } pub fn fromNull() Self { - return Self { + return Self{ .is_some = 0, .value = std.mem.zeroes(T), }; @@ -73,7 +73,7 @@ pub fn IxOption(T: type) type { } } pub fn toCOption(self: Self) COption(T) { - return COption(T) { + return COption(T){ .is_some = self.is_some, .value = self.value, }; @@ -92,7 +92,7 @@ pub const InitializeMintData = packed struct { pub const AmountData = packed struct { /// The amount of tokens to process. - amount: u64 + amount: u64, }; pub const Instruction = union(InstructionDiscriminant) { diff --git a/token/zig/src/main.zig b/token/zig/src/main.zig index e1e1148..16b3d34 100644 --- a/token/zig/src/main.zig +++ b/token/zig/src/main.zig @@ -316,27 +316,27 @@ fn processInstruction(program_id: *align(1) PublicKey, accounts: []sol.Account, //} //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; - //} - //} - //} + //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, @@ -383,17 +383,19 @@ fn processInstruction(program_id: *align(1) PublicKey, accounts: []sol.Account, } const authority = if (source.close_authority.is_some != 0) - source.close_authority.value else source.owner; + 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(), - //)?; + //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); + //return Err(ProgramError::InvalidAccountData); //} try validateOwner( @@ -465,8 +467,7 @@ fn validateOwner( if (!expected_owner.equals(owner_account.id())) { return TokenError.OwnerMismatch; } - if (program_id.equals(owner_account.ownerId()) and owner_account.dataLen() == state.Multisig.len) - { + 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]; @@ -482,7 +483,7 @@ fn validateOwner( // } //} //if num_signers < multisig.m { - return TokenError.MissingRequiredSignature; + return TokenError.MissingRequiredSignature; //} } else if (!owner_account.isSigner()) { return TokenError.MissingRequiredSignature; diff --git a/token/zig/src/state.zig b/token/zig/src/state.zig index 03ad4d9..5ce26b7 100644 --- a/token/zig/src/state.zig +++ b/token/zig/src/state.zig @@ -47,13 +47,13 @@ pub fn COption(T: type) type { } } pub fn fromValue(value: T) Self { - return Self { + return Self{ .is_some = 1, .value = value, }; } pub fn asNull() Self { - return Self { + return Self{ .is_some = 0, .value = std.mem.zeroes(T), }; @@ -73,7 +73,7 @@ pub const Multisig = packed struct { }; test "Mint: bitCast" { - const mint = Mint { + const mint = Mint{ .authority = COption(PublicKey).fromOptional(std.mem.bytesToValue(PublicKey, &[_]u8{1} ** 32)), .supply = 42, .decimals = 7, @@ -81,9 +81,9 @@ test "Mint: bitCast" { .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, 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, + 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); @@ -91,7 +91,7 @@ test "Mint: bitCast" { } test "Mint: cast with padding" { - const mint = Mint { + const mint = Mint{ .authority = COption(PublicKey).new(std.mem.bytesToValue(PublicKey, &[_]u8{1} ** 32)), .supply = 42, .decimals = 7, @@ -99,10 +99,9 @@ test "Mint: cast with padding" { .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, + 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..]));