solana-haskell-sdk-1.2.0.0: src/Network/Solana/SplPrograms/Token.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Client for the SPL Token program: <https://spl.solana.com/token>
-- Instruction data uses SPL Token's hand-rolled pack format (u8 discriminant,
-- little-endian integers, raw 32-byte pubkeys, and optional pubkeys encoded
-- as a 1-byte tag followed by the key when present) — this is NOT bincode.
module Network.Solana.SplPrograms.Token where
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import GHC.Generics (Generic)
import Network.Solana.Core.Crypto
import Network.Solana.Core.Instruction
import Network.Solana.Sysvar qualified as Sysvar
-- | Token account state: Uninitialized (0), Initialized (1), or Frozen (2).
data AccountState = TokenAccountUninitialized | TokenAccountInitialized | TokenAccountFrozen
deriving (Eq, Show, Enum)
-- | SPL Token account state (165 bytes fixed).
data TokenAccount = TokenAccount
{ taMint :: SolanaPublicKey,
taOwner :: SolanaPublicKey,
taAmount :: Word64,
taDelegate :: Maybe SolanaPublicKey,
taState :: AccountState,
taIsNative :: Maybe Word64,
taDelegatedAmount :: Word64,
taCloseAuthority :: Maybe SolanaPublicKey
}
deriving (Eq, Show)
-- | SPL Token mint state (82 bytes fixed).
data Mint = Mint
{ mMintAuthority :: Maybe SolanaPublicKey,
mSupply :: Word64,
mDecimals :: Word8,
mIsInitialized :: Bool,
mFreezeAuthority :: Maybe SolanaPublicKey
}
deriving (Eq, Show)
-- | Helper for parsing fixed-width state COption (4-byte LE tag + payload).
getStateCOption :: Int -> Get a -> Get (Maybe a)
getStateCOption payloadLen inner = do
tag <- getWord32le
case tag of
0 -> skip payloadLen >> pure Nothing
1 -> Just <$> inner
_ -> fail ("state COption: invalid tag " <> show tag)
-- | Decode a SPL Token account state from bytes (exactly 165 bytes).
decodeTokenAccount :: BS.ByteString -> Either String TokenAccount
decodeTokenAccount bs
| BS.length bs /= 165 = Left ("TokenAccount: expected 165 bytes, got " <> show (BS.length bs))
| otherwise = case runGetOrFail getTokenAccount (BL.fromStrict bs) of
Left (_, _, err) -> Left err
Right (_, _, ta) -> Right ta
where
getTokenAccount =
TokenAccount
<$> get
<*> get
<*> getWord64le
<*> getStateCOption 32 get
<*> getAccountState
<*> getStateCOption 8 getWord64le
<*> getWord64le
<*> getStateCOption 32 get
getAccountState = do
byte <- getWord8
if byte <= 2
then pure (toEnum (fromIntegral byte))
else fail ("AccountState: invalid value " <> show byte)
-- | Decode a SPL Token mint state from bytes (exactly 82 bytes).
decodeMint :: BS.ByteString -> Either String Mint
decodeMint bs
| BS.length bs /= 82 = Left ("Mint: expected 82 bytes, got " <> show (BS.length bs))
| otherwise = case runGetOrFail getMint (BL.fromStrict bs) of
Left (_, _, err) -> Left err
Right (_, _, m) -> Right m
where
getMint =
Mint
<$> getStateCOption 32 get
<*> getWord64le
<*> getWord8
<*> getIsInitialized
<*> getStateCOption 32 get
getIsInitialized = do
byte <- getWord8
case byte of
0 -> pure False
1 -> pure True
_ -> fail ("Mint: invalid is_initialized byte " <> show byte)
-- | SPL Token program address.
tokenProgramId :: SolanaPublicKey
tokenProgramId = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
-- | Authorities a token mint or account can carry (SPL @AuthorityType@, u8 0-3).
data AuthorityType
= MintTokens
| FreezeAuthority
| AccountOwner
| CloseAuthority
deriving (Eq, Show, Enum, Bounded, Generic)
-- | SPL Token instructions 0-20 (instructions 21-24 are deferred post-1.0).
data TokenInstruction
= InitializeMint {tiDecimals :: Word8, tiMintAuthority :: SolanaPublicKey, tiFreezeAuthority :: Maybe SolanaPublicKey}
| InitializeAccount
| InitializeMultisig {tiM :: Word8}
| Transfer {tiAmount :: Word64}
| Approve {tiAmount :: Word64}
| Revoke
| SetAuthority {tiAuthorityType :: AuthorityType, tiNewAuthority :: Maybe SolanaPublicKey}
| MintTo {tiAmount :: Word64}
| Burn {tiAmount :: Word64}
| CloseAccount
| FreezeAccount
| ThawAccount
| TransferChecked {tiAmount :: Word64, tiDecimals :: Word8}
| ApproveChecked {tiAmount :: Word64, tiDecimals :: Word8}
| MintToChecked {tiAmount :: Word64, tiDecimals :: Word8}
| BurnChecked {tiAmount :: Word64, tiDecimals :: Word8}
| InitializeAccount2 {tiOwner :: SolanaPublicKey}
| SyncNative
| InitializeAccount3 {tiOwner :: SolanaPublicKey}
| InitializeMultisig2 {tiM :: Word8}
| InitializeMint2 {tiDecimals :: Word8, tiMintAuthority :: SolanaPublicKey, tiFreezeAuthority :: Maybe SolanaPublicKey}
deriving (Eq, Show, Generic)
putCOptionPubkey :: Maybe SolanaPublicKey -> Put
putCOptionPubkey Nothing = putWord8 0
putCOptionPubkey (Just pk) = do
putWord8 1
putByteString (getSolanaPublicKeyRaw pk)
getCOptionPubkey :: Get (Maybe SolanaPublicKey)
getCOptionPubkey = do
tag <- getWord8
case tag of
0 -> pure Nothing
1 -> Just <$> get
_ -> fail ("COption<Pubkey>: invalid tag " <> show tag)
instance Binary TokenInstruction where
put :: TokenInstruction -> Put
put (InitializeMint decimals mintAuth freezeAuth) = do
putWord8 0
putWord8 decimals
putByteString (getSolanaPublicKeyRaw mintAuth)
putCOptionPubkey freezeAuth
put InitializeAccount = putWord8 1
put (InitializeMultisig m) = do
putWord8 2
putWord8 m
put (Transfer amount) = do
putWord8 3
putWord64le amount
put (Approve amount) = do
putWord8 4
putWord64le amount
put Revoke = putWord8 5
put (SetAuthority authType newAuth) = do
putWord8 6
putWord8 (fromIntegral (fromEnum authType))
putCOptionPubkey newAuth
put (MintTo amount) = do
putWord8 7
putWord64le amount
put (Burn amount) = do
putWord8 8
putWord64le amount
put CloseAccount = putWord8 9
put FreezeAccount = putWord8 10
put ThawAccount = putWord8 11
put (TransferChecked amount decimals) = do
putWord8 12
putWord64le amount
putWord8 decimals
put (ApproveChecked amount decimals) = do
putWord8 13
putWord64le amount
putWord8 decimals
put (MintToChecked amount decimals) = do
putWord8 14
putWord64le amount
putWord8 decimals
put (BurnChecked amount decimals) = do
putWord8 15
putWord64le amount
putWord8 decimals
put (InitializeAccount2 owner) = do
putWord8 16
putByteString (getSolanaPublicKeyRaw owner)
put SyncNative = putWord8 17
put (InitializeAccount3 owner) = do
putWord8 18
putByteString (getSolanaPublicKeyRaw owner)
put (InitializeMultisig2 m) = do
putWord8 19
putWord8 m
put (InitializeMint2 decimals mintAuth freezeAuth) = do
putWord8 20
putWord8 decimals
putByteString (getSolanaPublicKeyRaw mintAuth)
putCOptionPubkey freezeAuth
get :: Get TokenInstruction
get = do
disc <- getWord8
case disc of
0 -> InitializeMint <$> getWord8 <*> get <*> getCOptionPubkey
1 -> pure InitializeAccount
2 -> InitializeMultisig <$> getWord8
3 -> Transfer <$> getWord64le
4 -> Approve <$> getWord64le
5 -> pure Revoke
6 -> SetAuthority <$> getAuthorityType <*> getCOptionPubkey
7 -> MintTo <$> getWord64le
8 -> Burn <$> getWord64le
9 -> pure CloseAccount
10 -> pure FreezeAccount
11 -> pure ThawAccount
12 -> TransferChecked <$> getWord64le <*> getWord8
13 -> ApproveChecked <$> getWord64le <*> getWord8
14 -> MintToChecked <$> getWord64le <*> getWord8
15 -> BurnChecked <$> getWord64le <*> getWord8
16 -> InitializeAccount2 <$> get
17 -> pure SyncNative
18 -> InitializeAccount3 <$> get
19 -> InitializeMultisig2 <$> getWord8
20 -> InitializeMint2 <$> getWord8 <*> get <*> getCOptionPubkey
_ -> fail ("TokenInstruction: unknown discriminant " <> show disc)
where
getAuthorityType = do
b <- getWord8
if b <= 3
then pure (toEnum (fromIntegral b))
else fail ("AuthorityType: invalid value " <> show b)
-- | Rent sysvar (re-exported for account-meta lists).
rentSysvar :: SolanaPublicKey
rentSysvar = Sysvar.rent
-- Owner-or-multisig meta convention (mirrors the Rust @spl-token@ builders):
-- with an empty signer list the owner itself signs; with a non-empty list the
-- owner is a readonly non-signer (the multisig account) and each listed key
-- is a readonly signer.
ownerMetas :: SolanaPublicKey -> [SolanaPublicKey] -> [AccountMeta]
ownerMetas owner signers =
AccountMeta {accountPubKey = owner, isSigner = null signers, isWritable = False}
: [AccountMeta {accountPubKey = s, isSigner = True, isWritable = False} | s <- signers]
-- | Initialize a new mint. Accounts: 0. `[WRITE]` mint, 1. `[]` rent sysvar.
initializeMint :: SolanaPublicKey -> Word8 -> SolanaPublicKey -> Maybe SolanaPublicKey -> Instruction
initializeMint mint decimals mintAuthority freezeAuthority =
mkInstruction
tokenProgramId
[ AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = rentSysvar, isSigner = False, isWritable = False}
]
(InitializeMint decimals mintAuthority freezeAuthority)
-- | Initialize a token account. Accounts: 0. `[WRITE]` account, 1. `[]` mint, 2. `[]` owner, 3. `[]` rent sysvar.
initializeAccount :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> Instruction
initializeAccount account mint owner =
mkInstruction
tokenProgramId
[ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False},
AccountMeta {accountPubKey = owner, isSigner = False, isWritable = False},
AccountMeta {accountPubKey = rentSysvar, isSigner = False, isWritable = False}
]
InitializeAccount
-- | Transfer tokens. Accounts: 0. `[WRITE]` source, 1. `[WRITE]` destination,
-- then owner-or-multisig metas.
transfer :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Instruction
transfer source destination owner signers amount =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = source, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = destination, isSigner = False, isWritable = True}
]
<> ownerMetas owner signers
)
(Transfer amount)
-- | Approve a delegate. Accounts: 0. `[WRITE]` source, 1. `[]` delegate, then owner metas.
approve :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Instruction
approve source delegate owner signers amount =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = source, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = delegate, isSigner = False, isWritable = False}
]
<> ownerMetas owner signers
)
(Approve amount)
-- | Revoke a delegate. Accounts: 0. `[WRITE]` source, then owner metas.
revoke :: SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Instruction
revoke source owner signers =
mkInstruction
tokenProgramId
(AccountMeta {accountPubKey = source, isSigner = False, isWritable = True} : ownerMetas owner signers)
Revoke
-- | Change a mint or account authority. Accounts: 0. `[WRITE]` mint/account, then owner metas.
setAuthority :: SolanaPublicKey -> AuthorityType -> Maybe SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Instruction
setAuthority owned authorityType newAuthority currentAuthority signers =
mkInstruction
tokenProgramId
(AccountMeta {accountPubKey = owned, isSigner = False, isWritable = True} : ownerMetas currentAuthority signers)
(SetAuthority authorityType newAuthority)
-- | Mint new tokens. Accounts: 0. `[WRITE]` mint, 1. `[WRITE]` destination account, then owner metas.
mintTo :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Instruction
mintTo mint destination mintAuthority signers amount =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = destination, isSigner = False, isWritable = True}
]
<> ownerMetas mintAuthority signers
)
(MintTo amount)
-- | Burn tokens. Accounts: 0. `[WRITE]` account, 1. `[WRITE]` mint, then owner metas.
burn :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Instruction
burn account mint owner signers amount =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True}
]
<> ownerMetas owner signers
)
(Burn amount)
-- | Close a token account, reclaiming its lamports. Accounts: 0. `[WRITE]` account,
-- 1. `[WRITE]` lamport destination, then owner metas.
closeAccount :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Instruction
closeAccount account destination owner signers =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = destination, isSigner = False, isWritable = True}
]
<> ownerMetas owner signers
)
CloseAccount
-- | Freeze a token account. Accounts: 0. `[WRITE]` account, 1. `[]` mint, then freeze-authority metas.
freezeAccount :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Instruction
freezeAccount account mint freezeAuthority signers =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False}
]
<> ownerMetas freezeAuthority signers
)
FreezeAccount
-- | Thaw a frozen token account. Same accounts as 'freezeAccount'.
thawAccount :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Instruction
thawAccount account mint freezeAuthority signers =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False}
]
<> ownerMetas freezeAuthority signers
)
ThawAccount
-- | Transfer with decimals check. Accounts: 0. `[WRITE]` source, 1. `[]` mint,
-- 2. `[WRITE]` destination, then owner metas.
transferChecked :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Word8 -> Instruction
transferChecked source mint destination owner signers amount decimals =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = source, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False},
AccountMeta {accountPubKey = destination, isSigner = False, isWritable = True}
]
<> ownerMetas owner signers
)
(TransferChecked amount decimals)
-- | Sync a native (wrapped SOL) account's balance. Accounts: 0. `[WRITE]` account.
syncNative :: SolanaPublicKey -> Instruction
syncNative account =
mkInstruction
tokenProgramId
[AccountMeta {accountPubKey = account, isSigner = False, isWritable = True}]
SyncNative
-- | Initialize a multisig account. The signer pubkeys are the full member set;
-- m is the number of required signatures (the Rust SDK validates 1 <= m <= 11 <= length signers
-- client-side; this builder, like the others in this module, performs no client-side validation).
-- # Account references
-- 0. `[WRITE]` Multisig account
-- 1. `[]` Rent sysvar
-- 2..n. `[]` Signer member accounts
initializeMultisig :: SolanaPublicKey -> [SolanaPublicKey] -> Word8 -> Instruction
initializeMultisig multisig members m =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = multisig, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = rentSysvar, isSigner = False, isWritable = False}
]
<> [AccountMeta {accountPubKey = s, isSigner = False, isWritable = False} | s <- members]
)
(InitializeMultisig m)
-- | Like 'initializeMultisig' without the rent sysvar account.
initializeMultisig2 :: SolanaPublicKey -> [SolanaPublicKey] -> Word8 -> Instruction
initializeMultisig2 multisig members m =
mkInstruction
tokenProgramId
( AccountMeta {accountPubKey = multisig, isSigner = False, isWritable = True}
: [AccountMeta {accountPubKey = s, isSigner = False, isWritable = False} | s <- members]
)
(InitializeMultisig2 m)
-- | 'approve' with decimals check. Accounts: 0. `[WRITE]` source, 1. `[]` mint, 2. `[]` delegate, then owner metas.
approveChecked :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Word8 -> Instruction
approveChecked source mint delegate owner signers amount decimals =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = source, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False},
AccountMeta {accountPubKey = delegate, isSigner = False, isWritable = False}
]
<> ownerMetas owner signers
)
(ApproveChecked amount decimals)
-- | 'mintTo' with decimals check. Accounts: 0. `[WRITE]` mint, 1. `[WRITE]` destination, then owner metas.
mintToChecked :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Word8 -> Instruction
mintToChecked mint destination mintAuthority signers amount decimals =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = destination, isSigner = False, isWritable = True}
]
<> ownerMetas mintAuthority signers
)
(MintToChecked amount decimals)
-- | 'burn' with decimals check. Accounts: 0. `[WRITE]` account, 1. `[WRITE]` mint, then owner metas.
burnChecked :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> [SolanaPublicKey] -> Word64 -> Word8 -> Instruction
burnChecked account mint owner signers amount decimals =
mkInstruction
tokenProgramId
( [ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True}
]
<> ownerMetas owner signers
)
(BurnChecked amount decimals)
-- | 'initializeAccount' with the owner in instruction data. Accounts: 0. `[WRITE]` account, 1. `[]` mint, 2. `[]` rent sysvar.
initializeAccount2 :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> Instruction
initializeAccount2 account mint owner =
mkInstruction
tokenProgramId
[ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False},
AccountMeta {accountPubKey = rentSysvar, isSigner = False, isWritable = False}
]
(InitializeAccount2 owner)
-- | Like 'initializeAccount2' without the rent sysvar account. Accounts: 0. `[WRITE]` account, 1. `[]` mint.
initializeAccount3 :: SolanaPublicKey -> SolanaPublicKey -> SolanaPublicKey -> Instruction
initializeAccount3 account mint owner =
mkInstruction
tokenProgramId
[ AccountMeta {accountPubKey = account, isSigner = False, isWritable = True},
AccountMeta {accountPubKey = mint, isSigner = False, isWritable = False}
]
(InitializeAccount3 owner)
-- | Like 'initializeMint' without the rent sysvar account. Accounts: 0. `[WRITE]` mint.
initializeMint2 :: SolanaPublicKey -> Word8 -> SolanaPublicKey -> Maybe SolanaPublicKey -> Instruction
initializeMint2 mint decimals mintAuthority freezeAuthority =
mkInstruction
tokenProgramId
[AccountMeta {accountPubKey = mint, isSigner = False, isWritable = True}]
(InitializeMint2 decimals mintAuthority freezeAuthority)