// SPDX-License-Identifier: Apache-2.0

// Disclaimer: This library provides a way for Solidity to interact with Solana's SPL-Token. Although it is production ready,
// it has not been audited for security, so use it at your own risk.

import 'solana';

library SplToken {
	address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
	enum TokenInstruction {
		InitializeMint, // 0
		InitializeAccount, // 1
		InitializeMultisig, // 2
		Transfer, // 3
		Approve, // 4
		Revoke, // 5
		SetAuthority, // 6
		MintTo, // 7
		Burn, // 8
		CloseAccount, // 9
		FreezeAccount, // 10
		ThawAccount, // 11
		TransferChecked, // 12
		ApproveChecked, // 13
		MintToChecked, // 14
		BurnChecked, // 15
		InitializeAccount2, // 16
		SyncNative, // 17
		InitializeAccount3, // 18
		InitializeMultisig2, // 19
		InitializeMint2, // 20
		GetAccountDataSize, // 21
		InitializeImmutableOwner, // 22
		AmountToUiAmount, // 23
		UiAmountToAmount, // 24
		InitializeMintCloseAuthority, // 25
		TransferFeeExtension, // 26
		ConfidentialTransferExtension, // 27
		DefaultAccountStateExtension, // 28
		Reallocate, // 29
		MemoTransferExtension, // 30
		CreateNativeMint // 31
	}

	/// Mint new tokens. The transaction should be signed by the mint authority keypair
	///
	/// @param mint the account of the mint
	/// @param account the token account where the minted tokens should go
	/// @param authority the public key of the mint authority
	/// @param amount the amount of tokens to mint
	function mint_to(address mint, address account, address authority, uint64 amount) internal {
		bytes instr = new bytes(9);

		instr[0] = uint8(TokenInstruction.MintTo);
		instr.writeUint64LE(amount, 1);

		AccountMeta[3] metas = [
			AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: authority, is_writable: false, is_signer: true})
		];

		tokenProgramId.call{accounts: metas}(instr);
	}

	/// Transfer @amount token from @from to @to. The transaction should be signed by the owner
	/// keypair of the from account.
	///
	/// @param from the account to transfer tokens from
	/// @param to the account to transfer tokens to
	/// @param owner the publickey of the from account owner keypair
	/// @param amount the amount to transfer
	function transfer(address from, address to, address owner, uint64 amount) internal {
		bytes instr = new bytes(9);

		instr[0] = uint8(TokenInstruction.Transfer);
		instr.writeUint64LE(amount, 1);

		AccountMeta[3] metas = [
			AccountMeta({pubkey: from, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: to, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
		];

		tokenProgramId.call{accounts: metas}(instr);
	}

	/// Burn @amount tokens in account. This transaction should be signed by the owner.
	///
	/// @param account the acount for which tokens should be burned
	/// @param mint the mint for this token
	/// @param owner the publickey of the account owner keypair
	/// @param amount the amount to burn
	function burn(address account, address mint, address owner, uint64 amount) internal {
		bytes instr = new bytes(9);

		instr[0] = uint8(TokenInstruction.Burn);
		instr.writeUint64LE(amount, 1);

		AccountMeta[3] metas = [
			AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
		];

		tokenProgramId.call{accounts: metas}(instr);
	}

	/// Approve an amount to a delegate. This transaction should be signed by the owner
	///
	/// @param account the account for which a delegate should be approved
	/// @param delegate the delegate publickey
	/// @param owner the publickey of the account owner keypair
	/// @param amount the amount to approve
	function approve(address account, address delegate, address owner, uint64 amount) internal {
		bytes instr = new bytes(9);

		instr[0] = uint8(TokenInstruction.Approve);
		instr.writeUint64LE(amount, 1);

		AccountMeta[3] metas = [
			AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: delegate, is_writable: false, is_signer: false}),
			AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
		];

		tokenProgramId.call{accounts: metas}(instr);
	}

	/// Revoke a previously approved delegate. This transaction should be signed by the owner. After
	/// this transaction, no delegate is approved for any amount.
	///
	/// @param account the account for which a delegate should be approved
	/// @param owner the publickey of the account owner keypair
	function revoke(address account, address owner) internal {
		bytes instr = new bytes(1);

		instr[0] = uint8(TokenInstruction.Revoke);

		AccountMeta[2] metas = [
			AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
			AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
		];

		tokenProgramId.call{accounts: metas}(instr);
	}

	/// Get the total supply for the mint, i.e. the total amount in circulation
	/// @param account The AccountInfo struct for the mint account
	function total_supply(AccountInfo account) internal view returns (uint64) {
	
		return account.data.readUint64LE(36);
	}

	/// Get the balance for an account.
	///
	/// @param account the struct AccountInfo whose account balance we want to retrive
	function get_balance(AccountInfo account) internal view returns (uint64) {

		return account.data.readUint64LE(64);
	}

	/// Get the account info for an account. This walks the transaction account infos
	/// and find the account info, or the transaction fails.
	///
	/// @param account the account for which we want to have the acount info.
	function get_account_info(address account) internal view returns (AccountInfo) {
		for (uint64 i = 0; i < tx.accounts.length; i++) {
			AccountInfo ai = tx.accounts[i];
			if (ai.key == account) {
				return ai;
			}
		}

		revert("account missing");
	}

	/// This enum represents the state of a token account
	enum AccountState {
		Uninitialized,
		Initialized,
		Frozen
	}

	/// This struct is the return of 'get_token_account_data'
	struct TokenAccountData {
		address mintAccount;
		address owner;
		uint64 balance;
		bool delegate_present;
		address delegate;
		AccountState state;
		bool is_native_present;
		uint64 is_native;
		uint64 delegated_amount;
		bool close_authority_present;
		address close_authority;
	}

	/// Fetch the owner, mint account and balance for an associated token account.
	///
	/// @param ai the AccountInfo struct for the token account
	/// @return struct TokenAccountData
	function get_token_account_data(AccountInfo ai) public pure returns (TokenAccountData) {
		
		TokenAccountData data = TokenAccountData(
			{
				mintAccount: ai.data.readAddress(0), 
				owner: ai.data.readAddress(32),
			 	balance: ai.data.readUint64LE(64),
				delegate_present: ai.data.readUint32LE(72) > 0,
				delegate: ai.data.readAddress(76),
				state: AccountState(ai.data[108]),
				is_native_present: ai.data.readUint32LE(109) > 0,
				is_native: ai.data.readUint64LE(113),
				delegated_amount: ai.data.readUint64LE(121),
				close_authority_present: ai.data.readUint32LE(129) > 0,
				close_authority: ai.data.readAddress(133)
			}
		);

		return data;
	}

	// This struct is the return of 'get_mint_account_data'
	struct MintAccountData {
		bool authority_present;
		address mint_authority;
		uint64 supply;
		uint8 decimals;
		bool is_initialized;
		bool freeze_authority_present;
		address freeze_authority;
	}

	/// Retrieve the information saved in a mint account
	///
	/// @param ai the AccountInfo struct for the mint accounts
	/// @return the MintAccountData struct
	function get_mint_account_data(AccountInfo ai) public pure returns (MintAccountData) {

		uint32 authority_present = ai.data.readUint32LE(0);
		uint32 freeze_authority_present = ai.data.readUint32LE(46);
		MintAccountData data = MintAccountData( {
			authority_present: authority_present > 0,
			mint_authority: ai.data.readAddress(4),
			supply: ai.data.readUint64LE(36),
			decimals: uint8(ai.data[44]),
			is_initialized: ai.data[45] > 0,
			freeze_authority_present: freeze_authority_present > 0,
			freeze_authority: ai.data.readAddress(50)
		});

		return data;
	}

	// A mint account has an authority, whose type is one of the members of this struct.
	enum AuthorityType {
		MintTokens,
		FreezeAccount,
		AccountOwner,
		CloseAccount
	}

	/// Remove the mint authority from a mint account
	///
	/// @param mintAccount the public key for the mint account
	/// @param mintAuthority the public for the mint authority
	function remove_mint_authority(address mintAccount, address mintAuthority) public {
		AccountMeta[2] metas = [
			AccountMeta({pubkey: mintAccount, is_signer: false, is_writable: true}),
			AccountMeta({pubkey: mintAuthority, is_signer: true, is_writable: false})
		];

		bytes data = new bytes(3);
		data[0] = uint8(TokenInstruction.SetAuthority);
		data[1] = uint8(AuthorityType.MintTokens);
		data[2] = 0;
		
		tokenProgramId.call{accounts: metas}(data);
	}
}
