Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 7caf27c

Browse files
Support interest bearing mint in token-js (#3266)
* Support interest bearing mint in token-js * pr feedback * Add tests + actions.ts * Update docs * pr feedback
1 parent 57c5fd8 commit 7caf27c

File tree

8 files changed

+325
-0
lines changed

8 files changed

+325
-0
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority';
88
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner';
99
import { TRANSFER_FEE_CONFIG_SIZE, TRANSFER_FEE_AMOUNT_SIZE } from './transferFee';
1010
import { NON_TRANSFERABLE_SIZE } from './nonTransferable';
11+
import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state';
1112

1213
export enum ExtensionType {
1314
Uninitialized,
@@ -20,6 +21,7 @@ export enum ExtensionType {
2021
ImmutableOwner,
2122
MemoTransfer,
2223
NonTransferable,
24+
InterestBearingMint,
2325
}
2426

2527
export const TYPE_SIZE = 2;
@@ -49,6 +51,8 @@ export function getTypeLen(e: ExtensionType): number {
4951
return MEMO_TRANSFER_SIZE;
5052
case ExtensionType.NonTransferable:
5153
return NON_TRANSFERABLE_SIZE;
54+
case ExtensionType.InterestBearingMint:
55+
return INTEREST_BEARING_MINT_CONFIG_STATE_SIZE;
5256
default:
5357
throw Error(`Unknown extension type: ${e}`);
5458
}
@@ -68,6 +72,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
6872
case ExtensionType.MintCloseAuthority:
6973
case ExtensionType.NonTransferable:
7074
case ExtensionType.Uninitialized:
75+
case ExtensionType.InterestBearingMint:
7176
return ExtensionType.Uninitialized;
7277
}
7378
}

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from './extensionType';
44
export * from './memoTransfer/index';
55
export * from './mintCloseAuthority';
66
export * from './immutableOwner';
7+
export * from './interestBearingMint';
78
export * from './nonTransferable';
89
export * from './transferFee/index';
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
ConfirmOptions,
3+
Connection,
4+
Keypair,
5+
PublicKey,
6+
sendAndConfirmTransaction,
7+
Signer,
8+
SystemProgram,
9+
Transaction,
10+
} from '@solana/web3.js';
11+
import { getSigners } from '../../actions/internal';
12+
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
13+
import { createInitializeMintInstruction } from '../../instructions';
14+
import { ExtensionType, getMintLen } from '../extensionType';
15+
import {
16+
createInitializeInterestBearingMintInstruction,
17+
createUpdateRateInterestBearingMintInstruction,
18+
} from './instructions';
19+
20+
/**
21+
* Initialize an interest bearing account on a mint
22+
*
23+
* @param connection Connection to use
24+
* @param payer Payer of the transaction fees
25+
* @param mintAuthority Account or multisig that will control minting
26+
* @param freezeAuthority Optional account or multisig that can freeze token accounts
27+
* @param rateAuthority The public key for the account that can update the rate
28+
* @param rate The initial interest rate
29+
* @param decimals Location of the decimal place
30+
* @param keypair Optional keypair, defaulting to a new random one
31+
* @param confirmOptions Options for confirming the transaction
32+
* @param programId SPL Token program account
33+
*
34+
* @return Public key of the mint
35+
*/
36+
export async function createInterestBearingMint(
37+
connection: Connection,
38+
payer: Signer,
39+
mintAuthority: PublicKey,
40+
freezeAuthority: PublicKey,
41+
rateAuthority: PublicKey,
42+
rate: number,
43+
decimals: number,
44+
keypair = Keypair.generate(),
45+
confirmOptions?: ConfirmOptions,
46+
programId = TOKEN_2022_PROGRAM_ID
47+
): Promise<PublicKey> {
48+
const mintLen = getMintLen([ExtensionType.InterestBearingMint]);
49+
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
50+
const transaction = new Transaction().add(
51+
SystemProgram.createAccount({
52+
fromPubkey: payer.publicKey,
53+
newAccountPubkey: keypair.publicKey,
54+
space: mintLen,
55+
lamports,
56+
programId,
57+
}),
58+
createInitializeInterestBearingMintInstruction(keypair.publicKey, rateAuthority, rate, programId),
59+
createInitializeMintInstruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId)
60+
);
61+
await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions);
62+
return keypair.publicKey;
63+
}
64+
65+
/**
66+
* Update the interest rate of an interest bearing account
67+
*
68+
* @param connection Connection to use
69+
* @param payer Payer of the transaction fees
70+
* @param mint Public key of the mint
71+
* @param rateAuthority The public key for the account that can update the rate
72+
* @param rate The initial interest rate
73+
* @param multiSigners Signing accounts if `owner` is a multisig
74+
* @param confirmOptions Options for confirming the transaction
75+
* @param programId SPL Token program account
76+
*
77+
* @return Signature of the confirmed transaction
78+
*/
79+
export async function updateRateInterestBearingMint(
80+
connection: Connection,
81+
payer: Signer,
82+
mint: PublicKey,
83+
rateAuthority: Signer,
84+
rate: number,
85+
multiSigners: Signer[] = [],
86+
confirmOptions?: ConfirmOptions,
87+
programId = TOKEN_2022_PROGRAM_ID
88+
): Promise<string> {
89+
const [rateAuthorityPublicKey, signers] = getSigners(rateAuthority, multiSigners);
90+
const transaction = new Transaction().add(
91+
createUpdateRateInterestBearingMintInstruction(mint, rateAuthorityPublicKey, rate, signers, programId)
92+
);
93+
94+
return await sendAndConfirmTransaction(connection, transaction, [payer, rateAuthority, ...signers], confirmOptions);
95+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './actions';
2+
export * from './instructions';
3+
export * from './state';
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { struct, s16, u8 } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import { PublicKey, Signer, TransactionInstruction } from '@solana/web3.js';
4+
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
5+
import { TokenInstruction } from '../../instructions';
6+
import { addSigners } from '../../instructions/internal';
7+
8+
export enum InterestBearingMintInstruction {
9+
Initialize = 0,
10+
UpdateRate = 1,
11+
}
12+
13+
export interface InterestBearingMintInitializeInstructionData {
14+
instruction: TokenInstruction.InterestBearingMintExtension;
15+
interestBearingMintInstruction: InterestBearingMintInstruction.Initialize;
16+
rateAuthority: PublicKey;
17+
rate: number;
18+
}
19+
20+
export interface InterestBearingMintUpdateRateInstructionData {
21+
instruction: TokenInstruction.InterestBearingMintExtension;
22+
interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate;
23+
rate: number;
24+
}
25+
26+
export const interestBearingMintInitializeInstructionData = struct<InterestBearingMintInitializeInstructionData>([
27+
u8('instruction'),
28+
u8('interestBearingMintInstruction'),
29+
// TODO: Make this an optional public key
30+
publicKey('rateAuthority'),
31+
s16('rate'),
32+
]);
33+
34+
export const interestBearingMintUpdateRateInstructionData = struct<InterestBearingMintUpdateRateInstructionData>([
35+
u8('instruction'),
36+
u8('interestBearingMintInstruction'),
37+
s16('rate'),
38+
]);
39+
40+
/**
41+
* Construct an InitializeInterestBearingMint instruction
42+
*
43+
* @param mint Mint to initialize
44+
* @param rateAuthority The public key for the account that can update the rate
45+
* @param rate The initial interest rate
46+
* @param programId SPL Token program account
47+
*
48+
* @return Instruction to add to a transaction
49+
*/
50+
export function createInitializeInterestBearingMintInstruction(
51+
mint: PublicKey,
52+
rateAuthority: PublicKey,
53+
rate: number,
54+
programId = TOKEN_2022_PROGRAM_ID
55+
) {
56+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
57+
const data = Buffer.alloc(interestBearingMintInitializeInstructionData.span);
58+
interestBearingMintInitializeInstructionData.encode(
59+
{
60+
instruction: TokenInstruction.InterestBearingMintExtension,
61+
interestBearingMintInstruction: InterestBearingMintInstruction.Initialize,
62+
rateAuthority,
63+
rate,
64+
},
65+
data
66+
);
67+
return new TransactionInstruction({ keys, programId, data });
68+
}
69+
70+
/**
71+
* Construct an UpdateRateInterestBearingMint instruction
72+
*
73+
* @param mint Mint to initialize
74+
* @param rateAuthority The public key for the account that can update the rate
75+
* @param rate The updated interest rate
76+
* @param multiSigners Signing accounts if `rateAuthority` is a multisig
77+
* @param programId SPL Token program account
78+
*
79+
* @return Instruction to add to a transaction
80+
*/
81+
export function createUpdateRateInterestBearingMintInstruction(
82+
mint: PublicKey,
83+
rateAuthority: PublicKey,
84+
rate: number,
85+
multiSigners: Signer[] = [],
86+
programId = TOKEN_2022_PROGRAM_ID
87+
) {
88+
const keys = addSigners(
89+
[
90+
{ pubkey: mint, isSigner: false, isWritable: true },
91+
{ pubkey: rateAuthority, isSigner: !multiSigners.length, isWritable: false },
92+
],
93+
rateAuthority,
94+
multiSigners
95+
);
96+
const data = Buffer.alloc(interestBearingMintUpdateRateInstructionData.span);
97+
interestBearingMintUpdateRateInstructionData.encode(
98+
{
99+
instruction: TokenInstruction.InterestBearingMintExtension,
100+
interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate,
101+
rate,
102+
},
103+
data
104+
);
105+
return new TransactionInstruction({ keys, programId, data });
106+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { s16, ns64, struct } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import { PublicKey } from '@solana/web3.js';
4+
import { Mint } from '../../state';
5+
import { ExtensionType, getExtensionData } from '../extensionType';
6+
7+
export interface InterestBearingMintConfigState {
8+
rateAuthority: PublicKey;
9+
initializationTimestamp: BigInt;
10+
preUpdateAverageRate: number;
11+
lastUpdateTimestamp: BigInt;
12+
currentRate: number;
13+
}
14+
15+
export const InterestBearingMintConfigStateLayout = struct<InterestBearingMintConfigState>([
16+
publicKey('rateAuthority'),
17+
ns64('initializationTimestamp'),
18+
s16('preUpdateAverageRate'),
19+
ns64('lastUpdateTimestamp'),
20+
s16('currentRate'),
21+
]);
22+
23+
export const INTEREST_BEARING_MINT_CONFIG_STATE_SIZE = InterestBearingMintConfigStateLayout.span;
24+
25+
export function getInterestBearingMintConfigState(mint: Mint): InterestBearingMintConfigState | null {
26+
const extensionData = getExtensionData(ExtensionType.InterestBearingMint, mint.tlvData);
27+
if (extensionData !== null) {
28+
return InterestBearingMintConfigStateLayout.decode(extensionData);
29+
}
30+
return null;
31+
}

token/js/src/instructions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export enum TokenInstruction {
3333
MemoTransferExtension = 30,
3434
CreateNativeMint = 31,
3535
InitializeNonTransferableMint = 32,
36+
InterestBearingMintExtension = 33,
3637
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import chai, { expect } from 'chai';
2+
import chaiAsPromised from 'chai-as-promised';
3+
chai.use(chaiAsPromised);
4+
5+
import { Connection, Keypair, PublicKey, Signer } from '@solana/web3.js';
6+
import {
7+
createInterestBearingMint,
8+
getInterestBearingMintConfigState,
9+
getMint,
10+
updateRateInterestBearingMint,
11+
} from '../../src';
12+
import { getConnection, newAccountWithLamports, TEST_PROGRAM_ID } from '../common';
13+
14+
const TEST_TOKEN_DECIMALS = 2;
15+
const TEST_RATE = 10;
16+
const TEST_UPDATE_RATE = 50;
17+
18+
describe('interestBearingMint', () => {
19+
let connection: Connection;
20+
let payer: Signer;
21+
let mint: PublicKey;
22+
let rateAuthority: Keypair;
23+
let mintAuthority: Keypair;
24+
let freezeAuthority: Keypair;
25+
let mintKeypair: Keypair;
26+
27+
before(async () => {
28+
connection = await getConnection();
29+
payer = await newAccountWithLamports(connection, 1000000000);
30+
rateAuthority = Keypair.generate();
31+
mintAuthority = Keypair.generate();
32+
freezeAuthority = Keypair.generate();
33+
});
34+
35+
it('initialize and update rate', async () => {
36+
mintKeypair = Keypair.generate();
37+
mint = mintKeypair.publicKey;
38+
await createInterestBearingMint(
39+
connection,
40+
payer,
41+
mintAuthority.publicKey,
42+
freezeAuthority.publicKey,
43+
rateAuthority.publicKey,
44+
TEST_RATE,
45+
TEST_TOKEN_DECIMALS,
46+
mintKeypair,
47+
undefined,
48+
TEST_PROGRAM_ID
49+
);
50+
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
51+
const interestBearingMintConfigState = getInterestBearingMintConfigState(mintInfo);
52+
expect(interestBearingMintConfigState).to.not.be.null;
53+
if (interestBearingMintConfigState !== null) {
54+
expect(interestBearingMintConfigState.rateAuthority).to.eql(rateAuthority.publicKey);
55+
expect(interestBearingMintConfigState.preUpdateAverageRate).to.eql(TEST_RATE);
56+
expect(interestBearingMintConfigState.currentRate).to.eql(TEST_RATE);
57+
expect(interestBearingMintConfigState.lastUpdateTimestamp).to.be.greaterThan(0);
58+
expect(interestBearingMintConfigState.initializationTimestamp).to.be.greaterThan(0);
59+
}
60+
61+
await updateRateInterestBearingMint(
62+
connection,
63+
payer,
64+
mint,
65+
rateAuthority,
66+
TEST_UPDATE_RATE,
67+
[],
68+
undefined,
69+
TEST_PROGRAM_ID
70+
);
71+
const mintInfoUpdatedRate = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
72+
const updatedRateConfigState = getInterestBearingMintConfigState(mintInfoUpdatedRate);
73+
74+
expect(updatedRateConfigState).to.not.be.null;
75+
if (updatedRateConfigState !== null) {
76+
expect(updatedRateConfigState.rateAuthority).to.eql(rateAuthority.publicKey);
77+
expect(updatedRateConfigState.currentRate).to.eql(TEST_UPDATE_RATE);
78+
expect(updatedRateConfigState.preUpdateAverageRate).to.eql(TEST_RATE);
79+
expect(updatedRateConfigState.lastUpdateTimestamp).to.be.greaterThan(0);
80+
expect(updatedRateConfigState.initializationTimestamp).to.be.greaterThan(0);
81+
}
82+
});
83+
});

0 commit comments

Comments
 (0)