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

Commit 3dd744d

Browse files
js support for permanent delegate authority (#3833)
1 parent 0de5f73 commit 3dd744d

File tree

10 files changed

+298
-1
lines changed

10 files changed

+298
-1
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/s
99
import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js';
1010
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
1111
import { NON_TRANSFERABLE_SIZE } from './nonTransferable.js';
12+
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
1213
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
1314

1415
export enum ExtensionType {
@@ -23,6 +24,7 @@ export enum ExtensionType {
2324
MemoTransfer,
2425
NonTransferable,
2526
InterestBearingMint,
27+
PermanentDelegate,
2628
}
2729

2830
export const TYPE_SIZE = 2;
@@ -54,6 +56,8 @@ export function getTypeLen(e: ExtensionType): number {
5456
return NON_TRANSFERABLE_SIZE;
5557
case ExtensionType.InterestBearingMint:
5658
return INTEREST_BEARING_MINT_CONFIG_STATE_SIZE;
59+
case ExtensionType.PermanentDelegate:
60+
return PERMANENT_DELEGATE_SIZE;
5761
default:
5862
throw Error(`Unknown extension type: ${e}`);
5963
}
@@ -74,6 +78,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
7478
case ExtensionType.NonTransferable:
7579
case ExtensionType.Uninitialized:
7680
case ExtensionType.InterestBearingMint:
81+
case ExtensionType.PermanentDelegate:
7782
return ExtensionType.Uninitialized;
7883
}
7984
}

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './memoTransfer/index.js';
77
export * from './mintCloseAuthority.js';
88
export * from './nonTransferable.js';
99
export * from './transferFee/index.js';
10+
export * from './permanentDelegate.js';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { struct } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import type { PublicKey } from '@solana/web3.js';
4+
import type { Mint } from '../state/mint.js';
5+
import { ExtensionType, getExtensionData } from './extensionType.js';
6+
7+
/** PermanentDelegate as stored by the program */
8+
export interface PermanentDelegate {
9+
delegate: PublicKey;
10+
}
11+
12+
/** Buffer layout for de/serializing a mint */
13+
export const PermanentDelegateLayout = struct<PermanentDelegate>([publicKey('delegate')]);
14+
15+
export const PERMANENT_DELEGATE_SIZE = PermanentDelegateLayout.span;
16+
17+
export function getPermanentDelegate(mint: Mint): PermanentDelegate | null {
18+
const extensionData = getExtensionData(ExtensionType.PermanentDelegate, mint.tlvData);
19+
if (extensionData !== null) {
20+
return PermanentDelegateLayout.decode(extensionData);
21+
} else {
22+
return null;
23+
}
24+
}

token/js/src/instructions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ export * from './initializeMintCloseAuthority.js'; // 25
3030
export * from './reallocate.js'; // 29
3131
export * from './createNativeMint.js'; // 31
3232
export * from './initializeNonTransferableMint.js'; // 32
33+
export * from './initializePermanentDelegate.js'; // 35
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { struct, u8 } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import type { AccountMeta } from '@solana/web3.js';
4+
import { PublicKey } from '@solana/web3.js';
5+
import { TransactionInstruction } from '@solana/web3.js';
6+
import { programSupportsExtensions } from '../constants.js';
7+
import {
8+
TokenInvalidInstructionDataError,
9+
TokenInvalidInstructionKeysError,
10+
TokenInvalidInstructionProgramError,
11+
TokenInvalidInstructionTypeError,
12+
TokenUnsupportedInstructionError,
13+
} from '../errors.js';
14+
import { TokenInstruction } from './types.js';
15+
16+
/** TODO: docs */
17+
export interface InitializePermanentDelegateInstructionData {
18+
instruction: TokenInstruction.InitializePermanentDelegate;
19+
delegate: PublicKey;
20+
}
21+
22+
/** TODO: docs */
23+
export const initializePermanentDelegateInstructionData = struct<InitializePermanentDelegateInstructionData>([
24+
u8('instruction'),
25+
publicKey('delegate'),
26+
]);
27+
28+
/**
29+
* Construct an InitializePermanentDelegate instruction
30+
*
31+
* @param mint Token mint account
32+
* @param permanentDelegate Authority that may sign for `Transfer`s and `Burn`s on any account
33+
* @param programId SPL Token program account
34+
*
35+
* @return Instruction to add to a transaction
36+
*/
37+
export function createInitializePermanentDelegateInstruction(
38+
mint: PublicKey,
39+
permanentDelegate: PublicKey | null,
40+
programId: PublicKey
41+
): TransactionInstruction {
42+
if (!programSupportsExtensions(programId)) {
43+
throw new TokenUnsupportedInstructionError();
44+
}
45+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
46+
47+
const data = Buffer.alloc(initializePermanentDelegateInstructionData.span);
48+
initializePermanentDelegateInstructionData.encode(
49+
{
50+
instruction: TokenInstruction.InitializePermanentDelegate,
51+
delegate: permanentDelegate || new PublicKey(0),
52+
},
53+
data
54+
);
55+
56+
return new TransactionInstruction({ keys, programId, data });
57+
}
58+
59+
/** A decoded, valid InitializePermanentDelegate instruction */
60+
export interface DecodedInitializePermanentDelegateInstruction {
61+
programId: PublicKey;
62+
keys: {
63+
mint: AccountMeta;
64+
};
65+
data: {
66+
instruction: TokenInstruction.InitializePermanentDelegate;
67+
delegate: PublicKey | null;
68+
};
69+
}
70+
71+
/**
72+
* Decode an InitializePermanentDelegate instruction and validate it
73+
*
74+
* @param instruction Transaction instruction to decode
75+
* @param programId SPL Token program account
76+
*
77+
* @return Decoded, valid instruction
78+
*/
79+
export function decodeInitializePermanentDelegateInstruction(
80+
instruction: TransactionInstruction,
81+
programId: PublicKey
82+
): DecodedInitializePermanentDelegateInstruction {
83+
if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError();
84+
if (instruction.data.length !== initializePermanentDelegateInstructionData.span)
85+
throw new TokenInvalidInstructionDataError();
86+
87+
const {
88+
keys: { mint },
89+
data,
90+
} = decodeInitializePermanentDelegateInstructionUnchecked(instruction);
91+
if (data.instruction !== TokenInstruction.InitializePermanentDelegate) throw new TokenInvalidInstructionTypeError();
92+
if (!mint) throw new TokenInvalidInstructionKeysError();
93+
94+
return {
95+
programId,
96+
keys: {
97+
mint,
98+
},
99+
data,
100+
};
101+
}
102+
103+
/** A decoded, non-validated InitializePermanentDelegate instruction */
104+
export interface DecodedInitializePermanentDelegateInstructionUnchecked {
105+
programId: PublicKey;
106+
keys: {
107+
mint: AccountMeta | undefined;
108+
};
109+
data: {
110+
instruction: number;
111+
delegate: PublicKey | null;
112+
};
113+
}
114+
115+
/**
116+
* Decode an InitializePermanentDelegate instruction without validating it
117+
*
118+
* @param instruction Transaction instruction to decode
119+
*
120+
* @return Decoded, non-validated instruction
121+
*/
122+
export function decodeInitializePermanentDelegateInstructionUnchecked({
123+
programId,
124+
keys: [mint],
125+
data,
126+
}: TransactionInstruction): DecodedInitializePermanentDelegateInstructionUnchecked {
127+
const { instruction, delegate } = initializePermanentDelegateInstructionData.decode(data);
128+
129+
return {
130+
programId,
131+
keys: {
132+
mint,
133+
},
134+
data: {
135+
instruction,
136+
delegate,
137+
},
138+
};
139+
}

token/js/src/instructions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export enum TokenInstruction {
3434
CreateNativeMint = 31,
3535
InitializeNonTransferableMint = 32,
3636
InterestBearingMintExtension = 33,
37+
InitializePermanentDelegate = 35,
3738
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import chai, { expect } from 'chai';
2+
import chaiAsPromised from 'chai-as-promised';
3+
chai.use(chaiAsPromised);
4+
5+
import type { Connection, PublicKey, Signer } from '@solana/web3.js';
6+
import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js';
7+
import {
8+
createAccount,
9+
createInitializeMintInstruction,
10+
mintTo,
11+
getMintLen,
12+
ExtensionType,
13+
createInitializePermanentDelegateInstruction,
14+
burn,
15+
transferChecked,
16+
} from '../../src';
17+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
18+
19+
const TEST_TOKEN_DECIMALS = 0;
20+
const EXTENSIONS = [ExtensionType.PermanentDelegate];
21+
describe('permanentDelegate', () => {
22+
let connection: Connection;
23+
let payer: Signer;
24+
let mint: PublicKey;
25+
let mintAuthority: Keypair;
26+
let permanentDelegate: Keypair;
27+
let account: PublicKey;
28+
let destination: PublicKey;
29+
before(async () => {
30+
connection = await getConnection();
31+
payer = await newAccountWithLamports(connection, 1000000000);
32+
mintAuthority = Keypair.generate();
33+
permanentDelegate = Keypair.generate();
34+
});
35+
beforeEach(async () => {
36+
const mintKeypair = Keypair.generate();
37+
mint = mintKeypair.publicKey;
38+
const mintLen = getMintLen(EXTENSIONS);
39+
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
40+
const transaction = new Transaction().add(
41+
SystemProgram.createAccount({
42+
fromPubkey: payer.publicKey,
43+
newAccountPubkey: mint,
44+
space: mintLen,
45+
lamports,
46+
programId: TEST_PROGRAM_ID,
47+
}),
48+
createInitializePermanentDelegateInstruction(mint, permanentDelegate.publicKey, TEST_PROGRAM_ID),
49+
createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID)
50+
);
51+
52+
await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined);
53+
});
54+
it('burn tokens ', async () => {
55+
const owner = Keypair.generate();
56+
account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID);
57+
await mintTo(connection, payer, mint, account, mintAuthority, 5, [], undefined, TEST_PROGRAM_ID);
58+
await burn(connection, payer, account, mint, permanentDelegate, 2, undefined, undefined, TEST_PROGRAM_ID);
59+
const info = await connection.getTokenAccountBalance(account);
60+
expect(info).to.not.be.null;
61+
if (info !== null) {
62+
expect(info.value.uiAmount).to.eql(3);
63+
}
64+
});
65+
it('transfer tokens', async () => {
66+
const owner1 = Keypair.generate();
67+
const owner2 = Keypair.generate();
68+
destination = await createAccount(
69+
connection,
70+
payer,
71+
mint,
72+
owner2.publicKey,
73+
undefined,
74+
undefined,
75+
TEST_PROGRAM_ID
76+
);
77+
account = await createAccount(connection, payer, mint, owner1.publicKey, undefined, undefined, TEST_PROGRAM_ID);
78+
await mintTo(connection, payer, mint, account, mintAuthority, 5, [], undefined, TEST_PROGRAM_ID);
79+
await transferChecked(
80+
connection,
81+
payer,
82+
account,
83+
mint,
84+
destination,
85+
permanentDelegate,
86+
2,
87+
0,
88+
undefined,
89+
undefined,
90+
TEST_PROGRAM_ID
91+
);
92+
const source_info = await connection.getTokenAccountBalance(account);
93+
const destination_info = await connection.getTokenAccountBalance(destination);
94+
expect(source_info).to.not.be.null;
95+
expect(destination_info).to.not.be.null;
96+
if (source_info !== null) {
97+
expect(source_info.value.uiAmount).to.eql(3);
98+
}
99+
if (destination_info !== null) {
100+
expect(destination_info.value.uiAmount).to.eql(2);
101+
}
102+
});
103+
});

token/js/test/unit/decode.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Keypair } from '@solana/web3.js';
22
import chai, { expect } from 'chai';
33
import chaiAsPromised from 'chai-as-promised';
4-
import { createInitializeMintCloseAuthorityInstruction, TOKEN_2022_PROGRAM_ID } from '../../src';
4+
import {
5+
createInitializeMintCloseAuthorityInstruction,
6+
createInitializePermanentDelegateInstruction,
7+
TOKEN_2022_PROGRAM_ID,
8+
} from '../../src';
59

610
chai.use(chaiAsPromised);
711

@@ -15,4 +19,13 @@ describe('spl-token-2022 instructions', () => {
1519
expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID);
1620
expect(ix.keys).to.have.length(1);
1721
});
22+
it('InitializePermanentDelegate', () => {
23+
const ix = createInitializePermanentDelegateInstruction(
24+
Keypair.generate().publicKey,
25+
Keypair.generate().publicKey,
26+
TOKEN_2022_PROGRAM_ID
27+
);
28+
expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID);
29+
expect(ix.keys).to.have.length(1);
30+
});
1831
});

token/js/test/unit/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,6 @@ describe('extensionType', () => {
236236
expect(getAccountLen([ExtensionType.MintCloseAuthority, ExtensionType.TransferFeeConfig])).to.eql(314);
237237
expect(getAccountLen([])).to.eql(165);
238238
expect(getAccountLen([ExtensionType.ImmutableOwner])).to.eql(170);
239+
expect(getAccountLen([ExtensionType.PermanentDelegate])).to.eql(202);
239240
});
240241
});

token/js/test/unit/programId.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TOKEN_PROGRAM_ID,
1515
TOKEN_2022_PROGRAM_ID,
1616
TokenUnsupportedInstructionError,
17+
createInitializePermanentDelegateInstruction,
1718
} from '../../src';
1819
chai.use(chaiAsPromised);
1920

@@ -70,4 +71,12 @@ describe('unsupported extensions in spl-token', () => {
7071
createInitializeNonTransferableMintInstruction(mint, TOKEN_2022_PROGRAM_ID);
7172
}).to.not.throw(TokenUnsupportedInstructionError);
7273
});
74+
it('initializePermanentDelegate', () => {
75+
expect(function () {
76+
createInitializePermanentDelegateInstruction(mint, null, TOKEN_PROGRAM_ID);
77+
}).to.throw(TokenUnsupportedInstructionError);
78+
expect(function () {
79+
createInitializePermanentDelegateInstruction(mint, null, TOKEN_2022_PROGRAM_ID);
80+
}).to.not.throw(TokenUnsupportedInstructionError);
81+
});
7382
});

0 commit comments

Comments
 (0)