@thru/token-program 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ import { encodeAddress } from '@thru/helpers';
2
+ import type { Account } from '@thru/thru-sdk';
3
+ import { Pubkey as AbiPubkey } from './abi/thru/common/primitives/types';
4
+ import {
5
+ TickerField,
6
+ TokenAccount,
7
+ TokenMintAccount,
8
+ } from './abi/thru/program/token/types';
9
+ import type { MintAccountInfo, TokenAccountInfo } from './types';
10
+
11
+ const TEXT_DECODER = new TextDecoder();
12
+
13
+ export function parseMintAccountData(account: Account): MintAccountInfo {
14
+ const data = account.data?.data;
15
+ if (!data) {
16
+ throw new Error('Mint account data is missing');
17
+ }
18
+
19
+ const parsed = TokenMintAccount.from_array(data);
20
+ if (!parsed) {
21
+ throw new Error('Mint account data is malformed');
22
+ }
23
+
24
+ const hasFreezeAuthority = parsed.get_has_freeze_authority() === 1;
25
+ const freezeAuthority = hasFreezeAuthority
26
+ ? encodeAddress(pubkeyToBytes(parsed.get_freeze_authority()))
27
+ : null;
28
+
29
+ return {
30
+ decimals: parsed.get_decimals(),
31
+ supply: parsed.get_supply(),
32
+ creator: encodeAddress(pubkeyToBytes(parsed.get_creator())),
33
+ mintAuthority: encodeAddress(pubkeyToBytes(parsed.get_mint_authority())),
34
+ freezeAuthority,
35
+ hasFreezeAuthority,
36
+ ticker: decodeTicker(parsed.get_ticker()),
37
+ };
38
+ }
39
+
40
+ export function parseTokenAccountData(account: Account): TokenAccountInfo {
41
+ const data = account.data?.data;
42
+ if (!data) {
43
+ throw new Error('Token account data is missing');
44
+ }
45
+
46
+ const parsed = TokenAccount.from_array(data);
47
+ if (!parsed) {
48
+ throw new Error('Token account data is malformed');
49
+ }
50
+
51
+ return {
52
+ mint: encodeAddress(pubkeyToBytes(parsed.get_mint())),
53
+ owner: encodeAddress(pubkeyToBytes(parsed.get_owner())),
54
+ amount: parsed.get_amount(),
55
+ isFrozen: parsed.get_is_frozen() === 1,
56
+ };
57
+ }
58
+
59
+ export function isAccountNotFoundError(err: unknown): boolean {
60
+ if (!err) return false;
61
+ return (err as { code?: number }).code === 5;
62
+ }
63
+
64
+ function pubkeyToBytes(pubkey: AbiPubkey): Uint8Array {
65
+ return Uint8Array.from(pubkey.bytes);
66
+ }
67
+
68
+ function decodeTicker(field: TickerField): string {
69
+ const length = field.get_length();
70
+ const bytes = Uint8Array.from(field.get_bytes()).slice(0, length);
71
+ return TEXT_DECODER.decode(bytes).replace(/\0+$/, '');
72
+ }
@@ -0,0 +1,3 @@
1
+ export const PUBKEY_LENGTH = 32;
2
+ export const TICKER_MAX_LENGTH = 8;
3
+ export const ZERO_PUBKEY = new Uint8Array(PUBKEY_LENGTH);
@@ -0,0 +1,74 @@
1
+ import { deriveAddress, deriveProgramAddress, Pubkey } from '@thru/thru-sdk';
2
+ import { PUBKEY_LENGTH } from './constants';
3
+
4
+ const TOKEN_ACCOUNT_DEFAULT_SEED = new Uint8Array(PUBKEY_LENGTH);
5
+
6
+ export function deriveMintAddress(
7
+ mintAuthorityAddress: string,
8
+ seed: string,
9
+ tokenProgramAddress: string
10
+ ): { address: string; bytes: Uint8Array; derivedSeed: Uint8Array } {
11
+ const mintAuthorityBytes = Pubkey.from(mintAuthorityAddress).toBytes();
12
+ const seedBytes = hexToBytes(seed);
13
+ if (seedBytes.length !== 32) throw new Error('Seed must be 32 bytes (64 hex characters)');
14
+
15
+ const { bytes: derivedSeed } = deriveAddress([mintAuthorityBytes, seedBytes]);
16
+
17
+ const result = deriveProgramAddress({
18
+ programAddress: tokenProgramAddress,
19
+ seed: derivedSeed,
20
+ ephemeral: false,
21
+ });
22
+
23
+ return {
24
+ address: result.address,
25
+ bytes: result.bytes,
26
+ derivedSeed,
27
+ };
28
+ }
29
+
30
+ export function deriveTokenAccountAddress(
31
+ ownerAddress: string,
32
+ mintAddress: string,
33
+ tokenProgramAddress: string,
34
+ seed: Uint8Array = TOKEN_ACCOUNT_DEFAULT_SEED
35
+ ): { address: string; bytes: Uint8Array; derivedSeed: Uint8Array } {
36
+ if (seed.length !== PUBKEY_LENGTH) throw new Error('Token account seed must be 32 bytes');
37
+
38
+ const ownerBytes = Pubkey.from(ownerAddress).toBytes();
39
+ const mintBytes = Pubkey.from(mintAddress).toBytes();
40
+
41
+ const { bytes: derivedSeed } = deriveAddress([ownerBytes, mintBytes, seed]);
42
+
43
+ const result = deriveProgramAddress({
44
+ programAddress: tokenProgramAddress,
45
+ seed: derivedSeed,
46
+ ephemeral: false,
47
+ });
48
+
49
+ return {
50
+ address: result.address,
51
+ bytes: result.bytes,
52
+ derivedSeed,
53
+ };
54
+ }
55
+
56
+ export function deriveWalletSeed(
57
+ walletAddress: string,
58
+ extraSeeds: Uint8Array[] = []
59
+ ): Uint8Array {
60
+ const walletBytes = Pubkey.from(walletAddress).toBytes();
61
+ return deriveAddress([walletBytes, ...extraSeeds]).bytes;
62
+ }
63
+
64
+ function hexToBytes(hex: string): Uint8Array {
65
+ const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
66
+ if (normalized.length % 2 !== 0) {
67
+ throw new Error('Hex string must have even number of characters');
68
+ }
69
+ const bytes = new Uint8Array(normalized.length / 2);
70
+ for (let i = 0; i < normalized.length; i += 2) {
71
+ bytes[i / 2] = parseInt(normalized.substring(i, i + 2), 16);
72
+ }
73
+ return bytes;
74
+ }
package/src/format.ts ADDED
@@ -0,0 +1,24 @@
1
+ export function formatRawAmount(amount: bigint, decimals: number): string {
2
+ if (decimals === 0) return amount.toString();
3
+ const scale = BigInt(10) ** BigInt(decimals);
4
+ const whole = amount / scale;
5
+ const fraction = amount % scale;
6
+ const fractionStr = fraction.toString().padStart(decimals, '0').replace(/0+$/, '');
7
+ return fractionStr ? `${whole.toString()}.${fractionStr}` : whole.toString();
8
+ }
9
+
10
+ export function bytesToHex(bytes: Uint8Array): string {
11
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
12
+ }
13
+
14
+ export function hexToBytes(hex: string): Uint8Array {
15
+ const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
16
+ if (normalized.length % 2 !== 0) {
17
+ throw new Error('Hex string must have even number of characters');
18
+ }
19
+ const bytes = new Uint8Array(normalized.length / 2);
20
+ for (let i = 0; i < normalized.length; i += 2) {
21
+ bytes[i / 2] = parseInt(normalized.substring(i, i + 2), 16);
22
+ }
23
+ return bytes;
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Types
2
+ export type {
3
+ AccountLookupContext,
4
+ InstructionData,
5
+ MintAccountInfo,
6
+ TokenAccountInfo,
7
+ InitializeMintArgs,
8
+ InitializeAccountArgs,
9
+ MintToArgs,
10
+ TransferArgs,
11
+ } from './types';
12
+
13
+ // Constants
14
+ export { PUBKEY_LENGTH, TICKER_MAX_LENGTH, ZERO_PUBKEY } from './constants';
15
+
16
+ // Instructions
17
+ export {
18
+ createInitializeMintInstruction,
19
+ createInitializeAccountInstruction,
20
+ createMintToInstruction,
21
+ createTransferInstruction,
22
+ buildTokenInstructionBytes,
23
+ } from './instructions/index';
24
+
25
+ // Derivation
26
+ export { deriveMintAddress, deriveTokenAccountAddress, deriveWalletSeed } from './derivation';
27
+
28
+ // Account parsing
29
+ export { parseMintAccountData, parseTokenAccountData, isAccountNotFoundError } from './accounts';
30
+
31
+ // Formatting
32
+ export { formatRawAmount, bytesToHex, hexToBytes } from './format';
@@ -0,0 +1,5 @@
1
+ export { createInitializeMintInstruction } from './initialize-mint';
2
+ export { createInitializeAccountInstruction } from './initialize-account';
3
+ export { createMintToInstruction } from './mint-to';
4
+ export { createTransferInstruction } from './transfer';
5
+ export { buildTokenInstructionBytes } from './shared';
@@ -0,0 +1,27 @@
1
+ import { InitializeAccountInstructionBuilder } from '../abi/thru/program/token/types';
2
+ import type { AccountLookupContext, InitializeAccountArgs, InstructionData } from '../types';
3
+ import { buildTokenInstructionBytes } from './shared';
4
+
5
+ export function createInitializeAccountInstruction(
6
+ args: InitializeAccountArgs
7
+ ): InstructionData {
8
+ if (args.seedBytes.length !== 32) {
9
+ throw new Error('Token account seed must be 32 bytes');
10
+ }
11
+
12
+ return async (context: AccountLookupContext): Promise<Uint8Array> => {
13
+ const tokenAccountIndex = context.getAccountIndex(args.tokenAccountBytes);
14
+ const mintAccountIndex = context.getAccountIndex(args.mintAccountBytes);
15
+ const ownerAccountIndex = context.getAccountIndex(args.ownerAccountBytes);
16
+
17
+ const payload = new InitializeAccountInstructionBuilder()
18
+ .set_token_account_index(tokenAccountIndex)
19
+ .set_mint_account_index(mintAccountIndex)
20
+ .set_owner_account_index(ownerAccountIndex)
21
+ .set_new_account_seed(args.seedBytes)
22
+ .set_state_proof(args.stateProof)
23
+ .build();
24
+
25
+ return buildTokenInstructionBytes('initialize_account', payload);
26
+ };
27
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ InitializeMintInstructionBuilder,
3
+ TickerFieldBuilder,
4
+ } from '../abi/thru/program/token/types';
5
+ import { TICKER_MAX_LENGTH, ZERO_PUBKEY } from '../constants';
6
+ import type { AccountLookupContext, InitializeMintArgs, InstructionData } from '../types';
7
+ import { buildTokenInstructionBytes } from './shared';
8
+
9
+ const TEXT_ENCODER = new TextEncoder();
10
+
11
+ export function hexToBytes(hex: string): Uint8Array {
12
+ const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
13
+ if (normalized.length % 2 !== 0) {
14
+ throw new Error('Hex string must have even number of characters');
15
+ }
16
+ const bytes = new Uint8Array(normalized.length / 2);
17
+ for (let i = 0; i < normalized.length; i += 2) {
18
+ bytes[i / 2] = parseInt(normalized.substring(i, i + 2), 16);
19
+ }
20
+ return bytes;
21
+ }
22
+
23
+ export function createInitializeMintInstruction({
24
+ mintAccountBytes,
25
+ decimals,
26
+ mintAuthorityBytes,
27
+ freezeAuthorityBytes,
28
+ ticker,
29
+ seedHex,
30
+ stateProof,
31
+ creatorBytes,
32
+ }: InitializeMintArgs): InstructionData {
33
+ const hasFreezeAuthority = freezeAuthorityBytes ? 1 : 0;
34
+ const seedBytes = hexToBytes(seedHex);
35
+ if (seedBytes.length !== 32) {
36
+ throw new Error('Seed must be 32 bytes (64 hex characters)');
37
+ }
38
+ const tickerFieldBytes = buildTickerFieldBytes(ticker);
39
+ const creator = creatorBytes ?? mintAuthorityBytes;
40
+
41
+ return async (context: AccountLookupContext): Promise<Uint8Array> => {
42
+ const mintAccountIndex = context.getAccountIndex(mintAccountBytes);
43
+ const mintAuthority = mintAuthorityBytes;
44
+ const freezeAuthority = freezeAuthorityBytes ?? ZERO_PUBKEY;
45
+
46
+ const payload = new InitializeMintInstructionBuilder()
47
+ .set_mint_account_index(mintAccountIndex)
48
+ .set_decimals(decimals)
49
+ .set_creator(creator)
50
+ .set_mint_authority(mintAuthority)
51
+ .set_freeze_authority(freezeAuthority)
52
+ .set_has_freeze_authority(hasFreezeAuthority)
53
+ .set_ticker(tickerFieldBytes)
54
+ .set_seed(seedBytes)
55
+ .set_state_proof(stateProof)
56
+ .build();
57
+
58
+ return buildTokenInstructionBytes('initialize_mint', payload);
59
+ };
60
+ }
61
+
62
+ function buildTickerFieldBytes(ticker: string): Uint8Array {
63
+ const normalized = ticker.trim().toUpperCase();
64
+ const tickerBytes = TEXT_ENCODER.encode(normalized);
65
+ if (tickerBytes.length > TICKER_MAX_LENGTH) {
66
+ throw new Error('Ticker must be 8 characters or less');
67
+ }
68
+
69
+ const padded = new Uint8Array(TICKER_MAX_LENGTH);
70
+ padded.set(tickerBytes);
71
+
72
+ return new TickerFieldBuilder()
73
+ .set_length(tickerBytes.length)
74
+ .set_bytes(Array.from(padded))
75
+ .build();
76
+ }
@@ -0,0 +1,26 @@
1
+ import { MintToInstructionBuilder } from '../abi/thru/program/token/types';
2
+ import type { AccountLookupContext, MintToArgs, InstructionData } from '../types';
3
+ import { buildTokenInstructionBytes } from './shared';
4
+
5
+ type MintToInstructionBuilderWithBigInt = MintToInstructionBuilder & {
6
+ set_amount(value: number | bigint): MintToInstructionBuilder;
7
+ };
8
+
9
+ export function createMintToInstruction(args: MintToArgs): InstructionData {
10
+ return async (context: AccountLookupContext): Promise<Uint8Array> => {
11
+ const mintAccountIndex = context.getAccountIndex(args.mintAccountBytes);
12
+ const destinationIndex = context.getAccountIndex(args.destinationAccountBytes);
13
+ const authorityIndex = context.getAccountIndex(args.authorityAccountBytes);
14
+
15
+ const payloadBuilder = new MintToInstructionBuilder()
16
+ .set_mint_account_index(mintAccountIndex)
17
+ .set_dest_account_index(destinationIndex)
18
+ .set_authority_account_index(authorityIndex);
19
+
20
+ (payloadBuilder as MintToInstructionBuilderWithBigInt).set_amount(args.amount);
21
+
22
+ const payload = payloadBuilder.build();
23
+
24
+ return buildTokenInstructionBytes('mint_to', payload);
25
+ };
26
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ TokenInstruction,
3
+ TokenInstructionBuilder,
4
+ } from '../abi/thru/program/token/types';
5
+
6
+ import type { AccountLookupContext } from '../types';
7
+
8
+ export type { AccountLookupContext };
9
+
10
+ type TokenInstructionVariantName =
11
+ (typeof TokenInstruction.payloadVariantDescriptors)[number]['name'];
12
+
13
+ export function buildTokenInstructionBytes(
14
+ variant: TokenInstructionVariantName,
15
+ payload: Uint8Array
16
+ ): Uint8Array {
17
+ const builder = new TokenInstructionBuilder();
18
+ builder.payload().select(variant).writePayload(payload).finish();
19
+ return builder.build();
20
+ }
@@ -0,0 +1,24 @@
1
+ import { TransferInstruction, TransferInstructionBuilder } from '../abi/thru/program/token/types';
2
+ import type { AccountLookupContext, TransferArgs, InstructionData } from '../types';
3
+ import { buildTokenInstructionBytes } from './shared';
4
+
5
+ type TransferInstructionBuilderWithBigInt = TransferInstructionBuilder & {
6
+ set_amount(value: number | bigint): TransferInstructionBuilder;
7
+ };
8
+
9
+ export function createTransferInstruction(args: TransferArgs): InstructionData {
10
+ return async (context: AccountLookupContext): Promise<Uint8Array> => {
11
+ const sourceIndex = context.getAccountIndex(args.sourceAccountBytes);
12
+ const destinationIndex = context.getAccountIndex(args.destinationAccountBytes);
13
+
14
+ const payloadBuilder = TransferInstruction.builder()
15
+ .set_source_account_index(sourceIndex)
16
+ .set_dest_account_index(destinationIndex);
17
+
18
+ (payloadBuilder as TransferInstructionBuilderWithBigInt).set_amount(args.amount);
19
+
20
+ const payload = payloadBuilder.build();
21
+
22
+ return buildTokenInstructionBytes('transfer', payload);
23
+ };
24
+ }
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type AccountLookupContext = {
2
+ getAccountIndex: (pubkey: Uint8Array) => number;
3
+ };
4
+
5
+ export type InstructionData = (context: AccountLookupContext) => Promise<Uint8Array>;
6
+
7
+ export interface MintAccountInfo {
8
+ decimals: number;
9
+ supply: bigint;
10
+ creator: string;
11
+ mintAuthority: string;
12
+ freezeAuthority: string | null;
13
+ hasFreezeAuthority: boolean;
14
+ ticker: string;
15
+ }
16
+
17
+ export interface TokenAccountInfo {
18
+ mint: string;
19
+ owner: string;
20
+ amount: bigint;
21
+ isFrozen: boolean;
22
+ }
23
+
24
+ export interface InitializeMintArgs {
25
+ mintAccountBytes: Uint8Array;
26
+ decimals: number;
27
+ creatorBytes?: Uint8Array;
28
+ mintAuthorityBytes: Uint8Array;
29
+ freezeAuthorityBytes?: Uint8Array;
30
+ ticker: string;
31
+ seedHex: string;
32
+ stateProof: Uint8Array;
33
+ }
34
+
35
+ export interface InitializeAccountArgs {
36
+ tokenAccountBytes: Uint8Array;
37
+ mintAccountBytes: Uint8Array;
38
+ ownerAccountBytes: Uint8Array;
39
+ seedBytes: Uint8Array;
40
+ stateProof: Uint8Array;
41
+ }
42
+
43
+ export interface MintToArgs {
44
+ mintAccountBytes: Uint8Array;
45
+ destinationAccountBytes: Uint8Array;
46
+ authorityAccountBytes: Uint8Array;
47
+ amount: bigint;
48
+ }
49
+
50
+ export interface TransferArgs {
51
+ sourceAccountBytes: Uint8Array;
52
+ destinationAccountBytes: Uint8Array;
53
+ amount: bigint;
54
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ treeshake: true,
10
+ });