Skip to content

Commit 20aadf1

Browse files
authored
Merge pull request #5 from gitteri/arcade-token-template
Add Arcade Token Template Support
2 parents 79d9fe6 + c1393da commit 20aadf1

File tree

4 files changed

+338
-270
lines changed

4 files changed

+338
-270
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import type {
2+
Address,
3+
Instruction,
4+
Rpc,
5+
SolanaRpcApiMainnet,
6+
TransactionMessageWithFeePayer,
7+
TransactionSigner,
8+
TransactionVersion,
9+
FullTransaction,
10+
} from 'gill';
11+
import { createTransaction, some } from 'gill';
12+
import { getCreateAccountInstruction } from 'gill/programs';
13+
import {
14+
AccountState,
15+
getMintSize,
16+
type Extension,
17+
type ExtensionArgs,
18+
extension,
19+
getInitializeMintInstruction,
20+
getPreInitializeInstructionsForMintExtensions,
21+
TOKEN_2022_PROGRAM_ADDRESS,
22+
getInitializeTokenMetadataInstruction,
23+
} from 'gill/programs/token';
24+
25+
export class Token {
26+
private extensions: Extension[] = [];
27+
28+
getExtensions(): Extension[] {
29+
return this.extensions;
30+
}
31+
32+
withMetadata({
33+
mintAddress,
34+
authority,
35+
metadata,
36+
additionalMetadata,
37+
}: {
38+
mintAddress: Address;
39+
metadata: {
40+
name: string;
41+
symbol: string;
42+
uri: string;
43+
};
44+
authority: Address;
45+
additionalMetadata: Map<string, string>;
46+
}): Token {
47+
const metadataExtensions = createMetadataExtensions({
48+
mintAddress,
49+
authority,
50+
metadata,
51+
additionalMetadata,
52+
});
53+
this.extensions.push(...metadataExtensions);
54+
return this;
55+
}
56+
57+
withPermanentDelegate(authority: Address): Token {
58+
const permanentDelegateExtension = extension('PermanentDelegate', {
59+
delegate: authority,
60+
});
61+
this.extensions.push(permanentDelegateExtension);
62+
return this;
63+
}
64+
65+
withPausable(authority: Address): Token {
66+
const pausableConfigExtension = extension('PausableConfig', {
67+
authority: some(authority),
68+
paused: false,
69+
});
70+
this.extensions.push(pausableConfigExtension as Extension);
71+
return this;
72+
}
73+
74+
withDefaultAccountState(initialState: boolean): Token {
75+
const defaultAccountStateExtension = extension('DefaultAccountState', {
76+
state: initialState
77+
? AccountState.Initialized
78+
: AccountState.Uninitialized,
79+
});
80+
this.extensions.push(defaultAccountStateExtension);
81+
return this;
82+
}
83+
84+
withConfidentialBalances(authority: Address): Token {
85+
const confidentialBalancesExtension = extension(
86+
'ConfidentialTransferMint',
87+
{
88+
authority: some(authority),
89+
autoApproveNewAccounts: false,
90+
auditorElgamalPubkey: null,
91+
}
92+
);
93+
this.extensions.push(confidentialBalancesExtension as Extension);
94+
return this;
95+
}
96+
97+
async buildInstructions({
98+
rpc,
99+
decimals,
100+
authority,
101+
mint,
102+
feePayer,
103+
}: {
104+
rpc: Rpc<SolanaRpcApiMainnet>;
105+
decimals: number;
106+
authority: Address;
107+
mint: TransactionSigner<string>;
108+
feePayer: TransactionSigner<string>;
109+
}): Promise<Instruction[]> {
110+
// Get instructions for creating and initializing the mint account
111+
const [createMintAccountInstruction, initMintInstruction] =
112+
await getCreateMintInstructions({
113+
rpc: rpc,
114+
decimals,
115+
extensions: this.extensions,
116+
freezeAuthority: authority,
117+
mint: mint,
118+
payer: feePayer,
119+
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
120+
});
121+
const preInitializeInstructions = this.extensions.flatMap(ext =>
122+
getPreInitializeInstructionsForMintExtensions(mint.address, [ext])
123+
);
124+
125+
// TODO: Add other post-initialize instructions as needed like for transfer hooks
126+
const postInitializeInstructions = this.extensions.flatMap(ext =>
127+
ext.__kind === 'TokenMetadata'
128+
? [
129+
getInitializeTokenMetadataInstruction({
130+
metadata: mint.address,
131+
mint: mint.address,
132+
mintAuthority: feePayer,
133+
name: ext.name,
134+
symbol: ext.symbol,
135+
uri: ext.uri,
136+
updateAuthority: authority,
137+
}),
138+
]
139+
: []
140+
);
141+
142+
return [
143+
createMintAccountInstruction,
144+
...preInitializeInstructions,
145+
initMintInstruction,
146+
...postInitializeInstructions,
147+
];
148+
}
149+
150+
async buildTransaction({
151+
rpc,
152+
decimals,
153+
authority,
154+
mint,
155+
feePayer,
156+
}: {
157+
rpc: Rpc<SolanaRpcApiMainnet>;
158+
decimals: number;
159+
authority: Address;
160+
mint: TransactionSigner<string>;
161+
feePayer: TransactionSigner<string>;
162+
}): Promise<
163+
FullTransaction<TransactionVersion, TransactionMessageWithFeePayer>
164+
> {
165+
const instructions = await this.buildInstructions({
166+
rpc,
167+
decimals,
168+
authority,
169+
mint,
170+
feePayer,
171+
});
172+
173+
// Get latest blockhash for transaction
174+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
175+
176+
return createTransaction({
177+
feePayer,
178+
version: 'legacy',
179+
latestBlockhash,
180+
instructions,
181+
});
182+
}
183+
}
184+
185+
/**
186+
* Generates instructions for creating and initializing a new token mint
187+
* @param input Configuration parameters for mint creation
188+
* @returns Array of instructions for creating and initializing the mint
189+
*/
190+
export const getCreateMintInstructions = async (input: {
191+
rpc: Rpc<SolanaRpcApiMainnet>;
192+
decimals?: number;
193+
extensions?: ExtensionArgs[];
194+
freezeAuthority?: Address;
195+
mint: TransactionSigner<string>;
196+
payer: TransactionSigner<string>;
197+
programAddress?: Address;
198+
}): Promise<Instruction<string>[]> => {
199+
// Calculate required space for mint account including extensions
200+
const space = getMintSize(input.extensions);
201+
const postInitializeExtensions: Extension['__kind'][] = ['TokenMetadata'];
202+
203+
// Calculate space excluding post-initialization extensions
204+
const spaceWithoutPostInitializeExtensions = input.extensions
205+
? getMintSize(
206+
input.extensions.filter(
207+
e => !postInitializeExtensions.includes(e.__kind)
208+
)
209+
)
210+
: space;
211+
212+
// Get minimum rent-exempt balance
213+
const rent = await input.rpc
214+
.getMinimumBalanceForRentExemption(BigInt(space))
215+
.send();
216+
217+
// Return create account and initialize mint instructions
218+
return [
219+
getCreateAccountInstruction({
220+
payer: input.payer,
221+
newAccount: input.mint,
222+
lamports: rent,
223+
space: spaceWithoutPostInitializeExtensions,
224+
programAddress: input.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS,
225+
}),
226+
getInitializeMintInstruction(
227+
{
228+
mint: input.mint.address,
229+
decimals: input.decimals ?? 0,
230+
freezeAuthority: input.freezeAuthority,
231+
mintAuthority: input.payer.address,
232+
},
233+
{
234+
programAddress: input.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS,
235+
}
236+
),
237+
];
238+
};
239+
240+
const createMetadataExtensions = ({
241+
mintAddress,
242+
authority,
243+
metadata,
244+
additionalMetadata,
245+
}: {
246+
mintAddress: Address;
247+
metadata: {
248+
name: string;
249+
symbol: string;
250+
uri: string;
251+
};
252+
authority: Address;
253+
additionalMetadata: Map<string, string>;
254+
}): Extension[] => {
255+
const metadataPointer = extension('MetadataPointer', {
256+
metadataAddress: some(mintAddress),
257+
authority: some(authority),
258+
});
259+
260+
const metadataExtensionData = extension('TokenMetadata', {
261+
updateAuthority: some(authority),
262+
mint: mintAddress,
263+
name: metadata.name,
264+
symbol: metadata.symbol,
265+
uri: metadata.uri,
266+
additionalMetadata: additionalMetadata,
267+
});
268+
269+
return [metadataPointer, metadataExtensionData] as [Extension, Extension];
270+
};

0 commit comments

Comments
 (0)