Skip to content

Commit

Permalink
minting tokens and various renames (#33)
Browse files Browse the repository at this point in the history
* refactor: renamed

* feat: added mint tokens instructions

* refactor: changed input param name

* docs: example text

* feat: create transaction

* refactor: rename functions

* refactor: checked function

* fix: checked addresses

* chore: changeset

* feat: added tokens example

* fix: ata bug and cu

* feat: improved example

* docs: typos
  • Loading branch information
nickfrosty authored Feb 17, 2025
1 parent 60d00a9 commit be3110d
Show file tree
Hide file tree
Showing 12 changed files with 746 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-impalas-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gill": minor
---

added mint token functions
145 changes: 145 additions & 0 deletions examples/esrun/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
getExplorerLink,
createSolanaClient,
SolanaClusterMoniker,
getSignatureFromTransaction,
signTransactionMessageWithSigners,
generateKeyPairSigner,
address,
} from "gill";
import { loadKeypairSignerFromFile } from "gill/node";
import { buildCreateTokenTransaction, buildMintTokensTransaction } from "gill/programs";
import { TOKEN_2022_PROGRAM_ADDRESS } from "gill/programs/token22";

/** Turn on debug mode */
global.__GILL_DEBUG_LEVEL__ = "debug";

/**
* Load a keypair signer from the local filesystem
*
* This defaults to the file path used by the Solana CLI: `~/.config/solana/id.json`
*/
const signer = await loadKeypairSignerFromFile();
console.log("address:", signer.address);

/**
* Declare what Solana network cluster we want our code to interact with
*/
const cluster: SolanaClusterMoniker = "devnet";

/**
* Create a client connection to the Solana blockchain
*
* Note: `urlOrMoniker` can be either a Solana network moniker or a full URL of your RPC provider
*/
const { rpc, sendAndConfirmTransaction } = createSolanaClient({
urlOrMoniker: cluster,
});

/**
* Declare our token mint and desired token program
*/
const tokenProgram = TOKEN_2022_PROGRAM_ADDRESS;
const mint = await generateKeyPairSigner();

/**
* Get the latest blockhash (aka transaction lifetime). This acts as a recent timestamp
* for the blockchain to key on when processing your transaction
*
* Pro tip: only request this value just before you are going to use it your code
*/
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
console.log("latestBlockhash:", latestBlockhash);

/**
* Create a transaction that will create a new token (with metadata)
*
* - this will use the original SPL token by default (`TOKEN_PROGRAM_ADDRESS`)
*/
const createTokenTx = await buildCreateTokenTransaction({
mint,
latestBlockhash,
payer: signer,
// mintAuthority, // default=same as the `payer`
metadata: {
isMutable: true, // if the `updateAuthority` can change this metadata in the future
name: "Only Possible On Solana",
symbol: "OPOS",
uri: "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/Climate/metadata.json",
},
decimals: 2, // default=9,
tokenProgram, //default=TOKEN_PROGRAM_ADDRESS
});

/**
* Sign the transaction with the provided `signer` from when it was created
*/
let signedTransaction = await signTransactionMessageWithSigners(createTokenTx);
console.log("signedTransaction:");
console.log(signedTransaction);

/**
* Get the transaction signature after it has been signed by at least one signer
*/
let signature = getSignatureFromTransaction(signedTransaction);

/**
* Log the Solana Explorer link for the transaction we are about to send
*/
console.log("\nExplorer Link (for creating the mint):");
console.log(
getExplorerLink({
cluster,
transaction: signature,
}),
);

/**
* Actually send the transaction to the blockchain and confirm it
*/
await sendAndConfirmTransaction(signedTransaction);

/**
* Declare the wallet address that we want to mint the tokens to
*/
const destination = address("nicktrLHhYzLmoVbuZQzHUTicd2sfP571orwo9jfc8c");

/**
* Create a transaction that mints new tokens to the `destination` wallet address
* (raising the token's overall supply)
*
* - be sure to use the correct token program that the `mint` was created with
* - ensure the `mintAuthority` is the correct signer in order to actually mint new tokens
*/
const mintTokensTx = await buildMintTokensTransaction({
payer: signer,
latestBlockhash,
mint,
mintAuthority: signer,
amount: 1000, // note: be sure to consider the mint's `decimals` value
// if decimals=2 => this will mint 10.00 tokens
// if decimals=4 => this will mint 0.100 tokens
destination,
tokenProgram, // default=TOKEN_PROGRAM_ADDRESS
});

console.log("Transaction to mint tokens:");
console.log(mintTokensTx);

/**
* Sign the transaction with the provided `signer` from when it was created
*/
signedTransaction = await signTransactionMessageWithSigners(mintTokensTx);
signature = getSignatureFromTransaction(signedTransaction);

console.log("\nExplorer Link (for minting the tokens to the destination wallet):");
console.log(
getExplorerLink({
cluster,
transaction: signature,
}),
);

await sendAndConfirmTransaction(signedTransaction);

console.log("Complete.");
177 changes: 177 additions & 0 deletions packages/gill/src/__tests__/mint-tokens-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
getCreateAssociatedTokenIdempotentInstruction,
getMintToInstruction,
TOKEN_2022_PROGRAM_ADDRESS,
} from "@solana-program/token-2022";
import { getMintTokensInstructions, GetMintTokensInstructionsArgs } from "../programs";
import type { KeyPairSigner } from "@solana/signers";
import type { Address } from "@solana/addresses";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";

// Mock the imported functions
jest.mock("@solana-program/token-2022", () => ({
// preserve all real implementations to only change the desired ones
...jest.requireActual("@solana-program/token-2022"),

getCreateAssociatedTokenIdempotentInstruction: jest.fn(),
getMintToInstruction: jest.fn(),
}));

describe("getMintTokensInstructions", () => {
const mockPayer = { address: "payer" } as KeyPairSigner;
const mockMint = { address: "mint" } as KeyPairSigner;
const mockMintAuthority = { address: "mintAuthority" } as KeyPairSigner;
const mockDestination = { address: "destination" } as KeyPairSigner;

const mockAta = "mockAtaAddress" as Address;
const mockAmount = BigInt(1000);

beforeEach(() => {
(getCreateAssociatedTokenIdempotentInstruction as jest.Mock).mockReturnValue({
instruction: "mockCreateAtaInstruction",
});

(getMintToInstruction as jest.Mock).mockReturnValue({
instruction: "mockMintToInstruction",
});
});

afterEach(() => {
jest.clearAllMocks();
});

it("should create instructions with default token program", () => {
const args: GetMintTokensInstructionsArgs = {
payer: mockPayer,
mint: mockMint.address,
mintAuthority: mockMintAuthority,
destination: mockDestination.address,
ata: mockAta,
amount: mockAmount,
};

const instructions = getMintTokensInstructions(args);

expect(instructions).toHaveLength(2);

expect(getCreateAssociatedTokenIdempotentInstruction).toHaveBeenCalledWith({
owner: mockDestination.address,
mint: mockMint.address,
ata: mockAta,
payer: mockPayer,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

expect(getMintToInstruction).toHaveBeenCalledWith(
{
mint: mockMint.address,
mintAuthority: mockMintAuthority,
token: mockAta,
amount: mockAmount,
},
{
programAddress: TOKEN_PROGRAM_ADDRESS,
},
);
});

it("should create instructions with Token-2022 program", () => {
const args: GetMintTokensInstructionsArgs = {
payer: mockPayer,
mint: mockMint.address,
mintAuthority: mockMintAuthority,
destination: mockDestination.address,
ata: mockAta,
amount: mockAmount,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
};

const instructions = getMintTokensInstructions(args);

expect(instructions).toHaveLength(2);
expect(getCreateAssociatedTokenIdempotentInstruction).toHaveBeenCalledWith({
owner: mockDestination.address,
mint: mockMint.address,
ata: mockAta,
payer: mockPayer,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
});
});

it("should accept Address type for mint, mintAuthority, and destination", () => {
const args: GetMintTokensInstructionsArgs = {
payer: mockPayer,
mint: "mintAddress" as Address,
mintAuthority: "mintAuthorityAddress" as Address,
destination: "ownerAddress" as Address,
ata: mockAta,
amount: mockAmount,
};

const instructions = getMintTokensInstructions(args);

expect(instructions).toHaveLength(2);

expect(getCreateAssociatedTokenIdempotentInstruction).toHaveBeenCalledWith({
owner: args.destination,
mint: args.mint,
ata: mockAta,
payer: mockPayer,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

expect(getMintToInstruction).toHaveBeenCalledWith(
{
mint: "mintAddress",
mintAuthority: "mintAuthorityAddress",
token: mockAta,
amount: mockAmount,
},
{
programAddress: TOKEN_PROGRAM_ADDRESS,
},
);
});

it("should accept number type for amount", () => {
const args: GetMintTokensInstructionsArgs = {
payer: mockPayer,
mint: mockMint.address,
mintAuthority: mockMintAuthority,
destination: mockDestination.address,
ata: mockAta,
amount: 1000,
};

const instructions = getMintTokensInstructions(args);

expect(instructions).toHaveLength(2);
expect(getMintToInstruction).toHaveBeenCalledWith(
{
mint: mockMint.address,
mintAuthority: mockMintAuthority,
token: mockAta,
amount: 1000,
},
{
programAddress: TOKEN_PROGRAM_ADDRESS,
},
);
});

it("should throw error for unsupported token program", () => {
const args: GetMintTokensInstructionsArgs = {
payer: mockPayer,
mint: mockMint.address,
mintAuthority: mockMintAuthority,
destination: mockDestination.address,
ata: mockAta,
amount: mockAmount,
tokenProgram: "UnsupportedProgramId" as Address,
};

expect(() => getMintTokensInstructions(args)).toThrow(
"Unsupported token program. Try 'TOKEN_PROGRAM_ADDRESS' or 'TOKEN_2022_PROGRAM_ADDRESS'",
);
});
});
Loading

0 comments on commit be3110d

Please sign in to comment.