Skip to content

Commit 61cf8f0

Browse files
authored
feat: implement missing Token2022Program extensions (#3)
* feat: implement missing Token2022Program extensions - Add support for MetadataPointerExtension - Add support for TransferHookExtension - Implement initializeTokenMetadataInstruction per spl-token-metadata-interface * refactor: replace ByteBuffer with ByteUtil
1 parent 627b759 commit 61cf8f0

2 files changed

Lines changed: 334 additions & 0 deletions

File tree

solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import software.sava.core.programs.Discriminator;
99
import software.sava.core.tx.Instruction;
1010

11+
import java.nio.ByteBuffer;
12+
import java.nio.ByteOrder;
13+
import java.nio.charset.StandardCharsets;
1114
import java.util.Arrays;
1215
import java.util.Collection;
1316
import java.util.List;
@@ -1377,7 +1380,175 @@ public static Instruction withdrawExcessLamports(final SolanaAccounts solanaAcco
13771380
signerAccounts
13781381
);
13791382
}
1383+
public static Instruction initializeMetadataPointer(final SolanaAccounts solanaAccounts,
1384+
final PublicKey mintAccount,
1385+
final PublicKey authority,
1386+
final PublicKey metadataAccount) {
1387+
return initializeMetadataPointer(solanaAccounts.invokedToken2022Program(), mintAccount,authority, metadataAccount);
1388+
}
1389+
public static Instruction initializeMetadataPointer(final AccountMeta invokedTokenProgram,
1390+
final PublicKey mintAccount,
1391+
final PublicKey authority,
1392+
final PublicKey metadataAccount) {
1393+
final var keys = List.of(createWrite(mintAccount));
1394+
byte[] data = new byte[1+1+32+32];
1395+
data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal();
1396+
data[1] = (byte)0;
1397+
1398+
authority.write(data, 2);
1399+
1400+
metadataAccount.write(data, 34);
1401+
1402+
return createInstruction(invokedTokenProgram, keys, data);
1403+
}
1404+
1405+
public static Instruction updateMetadataPointer(final SolanaAccounts solanaAccounts,
1406+
final PublicKey mintAccount,
1407+
final PublicKey authority,
1408+
final PublicKey metadataAccount){
1409+
return updateMetadataPointer(solanaAccounts.invokedToken2022Program(),mintAccount,authority,metadataAccount);
1410+
}
1411+
public static Instruction updateMetadataPointer(final AccountMeta invokedTokenProgram,
1412+
final PublicKey mintAccount,
1413+
final PublicKey authority,
1414+
final PublicKey metadataAccount) {
1415+
1416+
final var keys = List.of(
1417+
AccountMeta.createWrite(mintAccount),
1418+
AccountMeta.createReadOnlySigner(authority)
1419+
);
1420+
1421+
byte[] data = new byte[1+1+32];
1422+
data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal();
1423+
data[1] = (byte)1;
1424+
1425+
metadataAccount.write(data, 2);
1426+
1427+
return createInstruction(
1428+
invokedTokenProgram,
1429+
keys,
1430+
data
1431+
);
1432+
}
1433+
1434+
public static Instruction initializeTokenMetadataInstruction(
1435+
final SolanaAccounts solanaAccounts,
1436+
final PublicKey metadataAccount,
1437+
final PublicKey updateAuthority,
1438+
final PublicKey mintAuthority,
1439+
final PublicKey mintAccount,
1440+
final String name,
1441+
final String symbol,
1442+
final String uri
1443+
) {
1444+
final var keys = List.of(
1445+
AccountMeta.createWrite(metadataAccount),
1446+
AccountMeta.createMeta(updateAuthority, false, false),
1447+
AccountMeta.createMeta(mintAccount, false, false),
1448+
AccountMeta.createMeta(mintAuthority, false, true)
1449+
);
1450+
1451+
byte[] data = buildInitializeTokenMetadataData(name, symbol, uri);
1452+
1453+
return createInstruction(
1454+
solanaAccounts.invokedToken2022Program(),
1455+
keys,
1456+
data
1457+
);
1458+
}
1459+
1460+
private static byte[] buildInitializeTokenMetadataData(
1461+
String name,
1462+
String symbol,
1463+
String uri) {
13801464

1465+
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
1466+
byte[] symbolBytes = symbol.getBytes(StandardCharsets.UTF_8);
1467+
byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8);
1468+
1469+
byte[] discriminator = new byte[]{
1470+
(byte) 0xD2, (byte) 0xE1, (byte) 0x1E, (byte) 0xA2,
1471+
(byte) 0x58, (byte) 0xB8, (byte) 0x4D, (byte) 0x8D
1472+
};
1473+
1474+
int dataSize = discriminator.length
1475+
+ Integer.BYTES + nameBytes.length
1476+
+ Integer.BYTES + symbolBytes.length
1477+
+ Integer.BYTES + uriBytes.length;
1478+
1479+
byte[] data = new byte[dataSize];
1480+
int offset = 0;
1481+
1482+
System.arraycopy(discriminator, 0, data, offset, discriminator.length);
1483+
offset += discriminator.length;
1484+
1485+
ByteUtil.putInt32LE(data, offset, nameBytes.length);
1486+
offset += Integer.BYTES;
1487+
System.arraycopy(nameBytes, 0, data, offset, nameBytes.length);
1488+
offset += nameBytes.length;
1489+
1490+
ByteUtil.putInt32LE(data, offset, symbolBytes.length);
1491+
offset += Integer.BYTES;
1492+
System.arraycopy(symbolBytes, 0, data, offset, symbolBytes.length);
1493+
offset += symbolBytes.length;
1494+
1495+
ByteUtil.putInt32LE(data, offset, uriBytes.length);
1496+
offset += Integer.BYTES;
1497+
System.arraycopy(uriBytes, 0, data, offset, uriBytes.length);
1498+
1499+
return data;
1500+
}
1501+
1502+
public static Instruction initializeTransferHook(final SolanaAccounts solanaAccounts,
1503+
final PublicKey mintAccount,
1504+
final PublicKey authority,
1505+
final PublicKey programAccount) {
1506+
return initializeTransferHook(solanaAccounts.invokedToken2022Program(), mintAccount,authority, programAccount);
1507+
}
1508+
public static Instruction initializeTransferHook(final AccountMeta invokedTokenProgram,
1509+
final PublicKey mintAccount,
1510+
final PublicKey authority,
1511+
final PublicKey programAccount) {
1512+
final var keys = List.of(AccountMeta.createWrite(mintAccount));
1513+
byte[] data = new byte[1+1+32+32];
1514+
data[0] = (byte)TokenInstruction.TransferHookExtension.ordinal();
1515+
data[1] = (byte)0;
1516+
1517+
authority.write(data, 2);
1518+
programAccount.write(data,34);
1519+
return createInstruction(invokedTokenProgram, keys, data);
1520+
}
1521+
1522+
public static Instruction updateTransferHook(final SolanaAccounts solanaAccounts,
1523+
final PublicKey mintAccount,
1524+
final PublicKey authority,
1525+
final PublicKey programAccount){
1526+
return updateTransferHook(solanaAccounts.invokedToken2022Program(),mintAccount,authority,programAccount);
1527+
}
1528+
public static Instruction updateTransferHook(
1529+
final AccountMeta invokedTokenProgram,
1530+
final PublicKey mintAccount,
1531+
final PublicKey authority,
1532+
final PublicKey programAccount) {
1533+
1534+
1535+
final var keys = List.of(
1536+
AccountMeta.createWrite(mintAccount),
1537+
AccountMeta.createReadOnlySigner(authority)
1538+
);
1539+
1540+
byte[] data = new byte[1+1+32];
1541+
data[0] = (byte)TokenInstruction.TransferHookExtension.ordinal();
1542+
data[1] = (byte)1;
1543+
1544+
programAccount.write(data, 2);
1545+
1546+
return createInstruction(
1547+
invokedTokenProgram,
1548+
keys,
1549+
data
1550+
);
1551+
}
13811552
private Token2022Program() {
13821553
}
13831554
}

solana-programs/src/test/java/software/sava/solana/programs/system/TokenProgramTests.java

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import software.sava.core.accounts.PublicKey;
55
import software.sava.core.accounts.SolanaAccounts;
66
import software.sava.core.accounts.meta.AccountMeta;
7+
import software.sava.core.encoding.Base58;
78
import software.sava.solana.programs.token.Token2022Program;
89
import software.sava.solana.programs.token.TokenProgram;
910

@@ -94,4 +95,166 @@ void initializeMint() {
9495

9596
assertArrayEquals(expectedData, initMintIx.data());
9697
}
98+
99+
@Test
100+
void createMintWithTransferHook() {
101+
// devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi
102+
103+
final var mintAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f");
104+
final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
105+
final var programAccount = PublicKey.fromBase58Encoded("2o6gvxp17hkML8Rz3cvqzbSTFStES287fYeDPeHhF7Vj");
106+
107+
final byte[] expectedData = Base58.decode("""
108+
F2LRfuZ8F9SkUvoY2DcGDFXNksBcb4d4UbpQyYcyRZjde31xioLJauJKYwRxjEjAuzMNPepJSHH3njMhSzFgvdM4Gy""".stripTrailing());
109+
110+
111+
final var solAccounts = SolanaAccounts.MAIN_NET;
112+
var initializeTransferHookIx = Token2022Program.initializeTransferHook(
113+
solAccounts,
114+
mintAccount,
115+
mintAuthority,
116+
programAccount
117+
);
118+
119+
assertEquals(solAccounts.invokedToken2022Program(), initializeTransferHookIx.programId());
120+
121+
var accounts = initializeTransferHookIx.accounts();
122+
assertEquals(1, accounts.size());
123+
assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst());
124+
125+
126+
assertArrayEquals(expectedData, initializeTransferHookIx.data());
127+
128+
}
129+
130+
@Test
131+
void createMintWithMetadataPointer() {
132+
// devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi
133+
134+
final var mint = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f");
135+
final var authority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
136+
137+
final byte[] expectedData = Base58.decode("""
138+
GC7FSeyRsRSWqdePvGFp5oZSbvCin5dinmBb7X5fn9DzNcfCdmyXiTV9iEzEZRrkmv3ixyvggyPXnUNyTekHbNx3Ph""".stripTrailing());
139+
140+
141+
final var solAccounts = SolanaAccounts.MAIN_NET;
142+
var initMetadataPointerIx = Token2022Program.initializeMetadataPointer(
143+
solAccounts,
144+
mint,
145+
authority,
146+
mint
147+
);
148+
149+
assertEquals(solAccounts.invokedToken2022Program(), initMetadataPointerIx.programId());
150+
151+
var accounts = initMetadataPointerIx.accounts();
152+
assertEquals(1, accounts.size());
153+
assertEquals(AccountMeta.createWrite(mint), accounts.getFirst());
154+
155+
assertArrayEquals(expectedData, initMetadataPointerIx.data());
156+
157+
}
158+
@Test
159+
void createMintWithInitializingMetadata() {
160+
// devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi
161+
162+
final var name = "SimpleTestCoin";
163+
final var symbol = "STC";
164+
final var uri = "https://example.com/metadata.json";
165+
final var mintAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f");
166+
final var metadataAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f");
167+
final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
168+
final var updateAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
169+
170+
final byte[] expectedData = Base58.decode("""
171+
AGUhRKBLRk1Ueut5CpnmUkTSsn2Fpg3v3un8sZ52wUqQh5ZvW8ots8FCc3MtpVSzANADodfMKGeGjhkTv59ziTJPF3XZ9LnH""".stripTrailing());
172+
173+
174+
final var solAccounts = SolanaAccounts.MAIN_NET;
175+
var initializeTokenMetadataIx = Token2022Program.initializeTokenMetadataInstruction(
176+
solAccounts,
177+
metadataAccount,
178+
mintAuthority,
179+
updateAuthority,
180+
mintAccount,
181+
name,
182+
symbol,
183+
uri
184+
);
185+
186+
assertEquals(solAccounts.invokedToken2022Program(), initializeTokenMetadataIx.programId());
187+
188+
var accounts = initializeTokenMetadataIx.accounts();
189+
assertEquals(4, accounts.size());
190+
assertEquals(AccountMeta.createWrite(metadataAccount), accounts.getFirst());
191+
assertEquals(AccountMeta.createRead(updateAuthority), accounts.get(1));
192+
assertEquals(AccountMeta.createRead(mintAccount), accounts.get(2));
193+
assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.get(3));
194+
195+
assertArrayEquals(expectedData, initializeTokenMetadataIx.data());
196+
197+
}
198+
199+
@Test
200+
void updateTransferHookAccount() {
201+
// devnet THZ3HTPAQZaEj6ggHSaLSxSS5CeGYp88VDa6NyXxY7pV9khHk1xJk1yHqP4jWByHjBUz34UuWLPffWQfeCzjNyi
202+
203+
final var mintAccount = PublicKey.fromBase58Encoded("HCRDkSQ6vM9QxDkJMGNUmVKjWqPYudkEsZRDwoJvyzQE");
204+
final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
205+
final var newTransferHookProgramId = PublicKey.fromBase58Encoded("7cjXTZvHYGuFarmmsYqjXsyYZY5TMyeNmvidxPJfvQ1Q");
206+
207+
final byte[] expectedData = Base58.decode("""
208+
pD8q5bQ9YX5HQ6qxGodu61fJtfqPFHjAKoTLes7gCwnme2""".stripTrailing());
209+
210+
211+
final var solAccounts = SolanaAccounts.MAIN_NET;
212+
var updateTransferHookIx = Token2022Program.updateTransferHook(
213+
solAccounts,
214+
mintAccount,
215+
mintAuthority,
216+
newTransferHookProgramId
217+
);
218+
219+
assertEquals(solAccounts.invokedToken2022Program(), updateTransferHookIx.programId());
220+
221+
var accounts = updateTransferHookIx.accounts();
222+
assertEquals(2, accounts.size());
223+
assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst());
224+
assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.getLast());
225+
226+
assertArrayEquals(expectedData, updateTransferHookIx.data());
227+
228+
}
229+
@Test
230+
void updateMintMetadataAccount() {
231+
// devnet 3iKA2XCusAq2uCxuGyWhw8oBkdYQMQMq87t5sJTXJpCcD169NDzDjBE3fcuTv6Dg8QpjC4QNmwxZXFhSB8DLZkj2
232+
233+
final var mintAccount = PublicKey.fromBase58Encoded("HCRDkSQ6vM9QxDkJMGNUmVKjWqPYudkEsZRDwoJvyzQE");
234+
final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV");
235+
final var newMetadataAddress = PublicKey.fromBase58Encoded("AsFagyk29GvS8dtibZ6vjtbfwjnMzn9xHcEzoAnRusCB");
236+
237+
final byte[] expectedData = Base58.decode("""
238+
t9LQrHqNgyQXjTT4nYpfgsSqXM8sAQVo3zQ8kxyueAPmrf""".stripTrailing());
239+
240+
241+
final var solAccounts = SolanaAccounts.MAIN_NET;
242+
var updateMetadataPointerIx = Token2022Program.updateMetadataPointer(
243+
solAccounts,
244+
mintAccount,
245+
mintAuthority,
246+
newMetadataAddress
247+
);
248+
249+
assertEquals(solAccounts.invokedToken2022Program(), updateMetadataPointerIx.programId());
250+
251+
var accounts = updateMetadataPointerIx.accounts();
252+
assertEquals(2, accounts.size());
253+
assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst());
254+
assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.getLast());
255+
256+
assertArrayEquals(expectedData, updateMetadataPointerIx.data());
257+
258+
}
259+
97260
}

0 commit comments

Comments
 (0)