Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions token/js/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority';
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner';
import { TRANSFER_FEE_CONFIG_SIZE, TRANSFER_FEE_AMOUNT_SIZE } from './transferFee';
import { NON_TRANSFERABLE_SIZE } from './nonTransferable';
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state';

export enum ExtensionType {
Uninitialized,
Expand All @@ -20,6 +21,7 @@ export enum ExtensionType {
ImmutableOwner,
MemoTransfer,
NonTransferable,
InterestBearingMint,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -49,6 +51,8 @@ export function getTypeLen(e: ExtensionType): number {
return MEMO_TRANSFER_SIZE;
case ExtensionType.NonTransferable:
return NON_TRANSFERABLE_SIZE;
case ExtensionType.InterestBearingMint:
return INTEREST_BEARING_MINT_CONFIG_STATE_SIZE;
default:
throw Error(`Unknown extension type: ${e}`);
}
Expand All @@ -68,6 +72,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.MintCloseAuthority:
case ExtensionType.NonTransferable:
case ExtensionType.Uninitialized:
case ExtensionType.InterestBearingMint:
return ExtensionType.Uninitialized;
}
}
Expand Down
1 change: 1 addition & 0 deletions token/js/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from './extensionType';
export * from './memoTransfer/index';
export * from './mintCloseAuthority';
export * from './immutableOwner';
export * from './interestBearingMint';
export * from './nonTransferable';
export * from './transferFee/index';
95 changes: 95 additions & 0 deletions token/js/src/extensions/interestBearingMint/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
ConfirmOptions,
Connection,
Keypair,
PublicKey,
sendAndConfirmTransaction,
Signer,
SystemProgram,
Transaction,
} from '@solana/web3.js';
import { getSigners } from '../../actions/internal';
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
import { createInitializeMintInstruction } from '../../instructions';
import { ExtensionType, getMintLen } from '../extensionType';
import {
createInitializeInterestBearingMintInstruction,
createUpdateRateInterestBearingMintInstruction,
} from './instructions';

/**
* Initialize an interest bearing account on a mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mintAuthority Account or multisig that will control minting
* @param freezeAuthority Optional account or multisig that can freeze token accounts
* @param rateAuthority The public key for the account that can update the rate
* @param rate The initial interest rate
* @param decimals Location of the decimal place
* @param keypair Optional keypair, defaulting to a new random one
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Public key of the mint
*/
export async function createInterestBearingMint(
connection: Connection,
payer: Signer,
mintAuthority: PublicKey,
freezeAuthority: PublicKey,
rateAuthority: PublicKey,
rate: number,
decimals: number,
keypair = Keypair.generate(),
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<PublicKey> {
const mintLen = getMintLen([ExtensionType.InterestBearingMint]);
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: keypair.publicKey,
space: mintLen,
lamports,
programId,
}),
createInitializeInterestBearingMintInstruction(keypair.publicKey, rateAuthority, rate, programId),
createInitializeMintInstruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId)
);
await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions);
return keypair.publicKey;
}

/**
* Update the interest rate of an interest bearing account
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Public key of the mint
* @param rateAuthority The public key for the account that can update the rate
* @param rate The initial interest rate
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function updateRateInterestBearingMint(
connection: Connection,
payer: Signer,
mint: PublicKey,
rateAuthority: Signer,
rate: number,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID
): Promise<string> {
const [rateAuthorityPublicKey, signers] = getSigners(rateAuthority, multiSigners);
const transaction = new Transaction().add(
createUpdateRateInterestBearingMintInstruction(mint, rateAuthorityPublicKey, rate, signers, programId)
);

return await sendAndConfirmTransaction(connection, transaction, [payer, rateAuthority, ...signers], confirmOptions);
}
3 changes: 3 additions & 0 deletions token/js/src/extensions/interestBearingMint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './actions';
export * from './instructions';
export * from './state';
106 changes: 106 additions & 0 deletions token/js/src/extensions/interestBearingMint/instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { struct, s16, u8 } from '@solana/buffer-layout';
import { publicKey } from '@solana/buffer-layout-utils';
import { PublicKey, Signer, TransactionInstruction } from '@solana/web3.js';
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
import { TokenInstruction } from '../../instructions';
import { addSigners } from '../../instructions/internal';

export enum InterestBearingMintInstruction {
Initialize = 0,
UpdateRate = 1,
}

export interface InterestBearingMintInitializeInstructionData {
instruction: TokenInstruction.InterestBearingMintExtension;
interestBearingMintInstruction: InterestBearingMintInstruction.Initialize;
rateAuthority: PublicKey;
rate: number;
}

export interface InterestBearingMintUpdateRateInstructionData {
instruction: TokenInstruction.InterestBearingMintExtension;
interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate;
rate: number;
}

export const interestBearingMintInitializeInstructionData = struct<InterestBearingMintInitializeInstructionData>([
u8('instruction'),
u8('interestBearingMintInstruction'),
// TODO: Make this an optional public key
publicKey('rateAuthority'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just realizing that this isn't done correctly with the transfer fee extension, since it's not actually a publicKey, but rather an OptionalNonZeroPublicKey, meaning that it's really publicKey?. We can keep this for now, but eventually we need to properly handle the optionality

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha. is that type typicall added to @solana/buffer-layout-utils or are they usually defined in the token js library?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we can probably add it to the token js library. If it eventually gets ported to the solana sdk, then we can also add it to buffer-layout-utils

s16('rate'),
]);

export const interestBearingMintUpdateRateInstructionData = struct<InterestBearingMintUpdateRateInstructionData>([
u8('instruction'),
u8('interestBearingMintInstruction'),
s16('rate'),
]);

/**
* Construct an InitializeInterestBearingMint instruction
*
* @param mint Mint to initialize
* @param rateAuthority The public key for the account that can update the rate
* @param rate The initial interest rate
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createInitializeInterestBearingMintInstruction(
mint: PublicKey,
rateAuthority: PublicKey,
rate: number,
programId = TOKEN_2022_PROGRAM_ID
) {
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
const data = Buffer.alloc(interestBearingMintInitializeInstructionData.span);
interestBearingMintInitializeInstructionData.encode(
{
instruction: TokenInstruction.InterestBearingMintExtension,
interestBearingMintInstruction: InterestBearingMintInstruction.Initialize,
rateAuthority,
rate,
},
data
);
return new TransactionInstruction({ keys, programId, data });
}

/**
* Construct an UpdateRateInterestBearingMint instruction
*
* @param mint Mint to initialize
* @param rateAuthority The public key for the account that can update the rate
* @param rate The updated interest rate
* @param multiSigners Signing accounts if `rateAuthority` is a multisig
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createUpdateRateInterestBearingMintInstruction(
mint: PublicKey,
rateAuthority: PublicKey,
rate: number,
multiSigners: Signer[] = [],
programId = TOKEN_2022_PROGRAM_ID
) {
const keys = addSigners(
[
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: rateAuthority, isSigner: !multiSigners.length, isWritable: false },
],
rateAuthority,
multiSigners
);
const data = Buffer.alloc(interestBearingMintUpdateRateInstructionData.span);
interestBearingMintUpdateRateInstructionData.encode(
{
instruction: TokenInstruction.InterestBearingMintExtension,
interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate,
rate,
},
data
);
return new TransactionInstruction({ keys, programId, data });
}
31 changes: 31 additions & 0 deletions token/js/src/extensions/interestBearingMint/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { s16, ns64, struct } from '@solana/buffer-layout';
import { publicKey } from '@solana/buffer-layout-utils';
import { PublicKey } from '@solana/web3.js';
import { Mint } from '../../state';
import { ExtensionType, getExtensionData } from '../extensionType';

export interface InterestBearingMintConfigState {
rateAuthority: PublicKey;
initializationTimestamp: BigInt;
preUpdateAverageRate: number;
lastUpdateTimestamp: BigInt;
currentRate: number;
}

export const InterestBearingMintConfigStateLayout = struct<InterestBearingMintConfigState>([
publicKey('rateAuthority'),
ns64('initializationTimestamp'),
s16('preUpdateAverageRate'),
ns64('lastUpdateTimestamp'),
s16('currentRate'),
]);

export const INTEREST_BEARING_MINT_CONFIG_STATE_SIZE = InterestBearingMintConfigStateLayout.span;

export function getInterestBearingMintConfigState(mint: Mint): InterestBearingMintConfigState | null {
const extensionData = getExtensionData(ExtensionType.InterestBearingMint, mint.tlvData);
if (extensionData !== null) {
return InterestBearingMintConfigStateLayout.decode(extensionData);
}
return null;
}
1 change: 1 addition & 0 deletions token/js/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export enum TokenInstruction {
MemoTransferExtension = 30,
CreateNativeMint = 31,
InitializeNonTransferableMint = 32,
InterestBearingMintExtension = 33,
}
83 changes: 83 additions & 0 deletions token/js/test/e2e-2022/interestBearingMint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);

import { Connection, Keypair, PublicKey, Signer } from '@solana/web3.js';
import {
createInterestBearingMint,
getInterestBearingMintConfigState,
getMint,
updateRateInterestBearingMint,
} from '../../src';
import { getConnection, newAccountWithLamports, TEST_PROGRAM_ID } from '../common';

const TEST_TOKEN_DECIMALS = 2;
const TEST_RATE = 10;
const TEST_UPDATE_RATE = 50;

describe('interestBearingMint', () => {
let connection: Connection;
let payer: Signer;
let mint: PublicKey;
let rateAuthority: Keypair;
let mintAuthority: Keypair;
let freezeAuthority: Keypair;
let mintKeypair: Keypair;

before(async () => {
connection = await getConnection();
payer = await newAccountWithLamports(connection, 1000000000);
rateAuthority = Keypair.generate();
mintAuthority = Keypair.generate();
freezeAuthority = Keypair.generate();
});

it('initialize and update rate', async () => {
mintKeypair = Keypair.generate();
mint = mintKeypair.publicKey;
await createInterestBearingMint(
connection,
payer,
mintAuthority.publicKey,
freezeAuthority.publicKey,
rateAuthority.publicKey,
TEST_RATE,
TEST_TOKEN_DECIMALS,
mintKeypair,
undefined,
TEST_PROGRAM_ID
);
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
const interestBearingMintConfigState = getInterestBearingMintConfigState(mintInfo);
expect(interestBearingMintConfigState).to.not.be.null;
if (interestBearingMintConfigState !== null) {
expect(interestBearingMintConfigState.rateAuthority).to.eql(rateAuthority.publicKey);
expect(interestBearingMintConfigState.preUpdateAverageRate).to.eql(TEST_RATE);
expect(interestBearingMintConfigState.currentRate).to.eql(TEST_RATE);
expect(interestBearingMintConfigState.lastUpdateTimestamp).to.be.greaterThan(0);
expect(interestBearingMintConfigState.initializationTimestamp).to.be.greaterThan(0);
}

await updateRateInterestBearingMint(
connection,
payer,
mint,
rateAuthority,
TEST_UPDATE_RATE,
[],
undefined,
TEST_PROGRAM_ID
);
const mintInfoUpdatedRate = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
const updatedRateConfigState = getInterestBearingMintConfigState(mintInfoUpdatedRate);

expect(updatedRateConfigState).to.not.be.null;
if (updatedRateConfigState !== null) {
expect(updatedRateConfigState.rateAuthority).to.eql(rateAuthority.publicKey);
expect(updatedRateConfigState.currentRate).to.eql(TEST_UPDATE_RATE);
expect(updatedRateConfigState.preUpdateAverageRate).to.eql(TEST_RATE);
expect(updatedRateConfigState.lastUpdateTimestamp).to.be.greaterThan(0);
expect(updatedRateConfigState.initializationTimestamp).to.be.greaterThan(0);
}
});
});