Skip to content

Commit c42cf8d

Browse files
authored
improved error handling for tokens (#72)
* feat: pathc bigint stringify * feat: added error things * refactor: improved logger * feat: simulation errors * chore: changeset
1 parent 8d9c520 commit c42cf8d

File tree

10 files changed

+522
-508
lines changed

10 files changed

+522
-508
lines changed

.changeset/bright-dolls-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mucho": minor
3+
---
4+
5+
improved error handling for gill transactions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@commander-js/extra-typings": "^12.1.0",
3131
"@iarna/toml": "^2.2.5",
3232
"@inquirer/prompts": "^7.0.0",
33+
"@solana/errors": "^2.1.0",
3334
"cli-table3": "^0.6.5",
3435
"commander": "^12.1.0",
3536
"dotenv": "^16.4.5",

pnpm-lock.yaml

Lines changed: 8 additions & 203 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/token/create.ts

Lines changed: 166 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
createSolanaClient,
1111
generateKeyPairSigner,
1212
isStringifiedNumber,
13-
signTransactionMessageWithSigners,
1413
getExplorerLink,
1514
isAddress,
1615
address,
16+
isSolanaError,
1717
} from "gill";
1818
import {
1919
buildCreateTokenTransaction,
@@ -25,6 +25,7 @@ import {
2525
import { loadKeypairSignerFromFile } from "gill/node";
2626
import { parseOrLoadSignerAddress } from "@/lib/gill/keys";
2727
import { parseOptionsFlagForRpcUrl } from "@/lib/cli/parsers";
28+
import { simulateTransactionOnThrow } from "@/lib/gill/errors";
2829

2930
export function createTokenCommand() {
3031
return new Command("create")
@@ -110,6 +111,7 @@ export function createTokenCommand() {
110111
.addOption(COMMON_OPTIONS.url)
111112
.action(async (options) => {
112113
titleMessage("Create a new token");
114+
const spinner = ora();
113115

114116
if (!options.name) {
115117
return errorOutro(
@@ -174,142 +176,176 @@ export function createTokenCommand() {
174176
);
175177
}
176178

177-
const parsedRpcUrl = parseOptionsFlagForRpcUrl(
178-
options.url,
179-
/* use the Solana cli config's rpc url as the fallback */
180-
loadSolanaCliConfig().json_rpc_url,
181-
);
182-
183-
const tokenProgram = checkedTokenProgramAddress(
184-
address(options.tokenProgram),
185-
);
186-
187-
// payer will always be used to pay the fees
188-
const payer = await loadKeypairSignerFromFile(options.keypair);
189-
190-
// mint authority is required to sign in order to mint the initial tokens
191-
const mintAuthority = options.mintAuthority
192-
? await loadKeypairSignerFromFile(options.mintAuthority)
193-
: payer;
194-
195-
const freezeAuthority = await getAddressFromStringOrFilePath(
196-
options.freezeAuthority || payer.address,
197-
);
198-
199-
const mint = options.customMint
200-
? await loadKeypairSignerFromFile(options.customMint)
201-
: await generateKeyPairSigner();
202-
203-
console.log(); // line spacer after the common "ExperimentalWarning for Ed25519 Web Crypto API"
204-
const spinner = ora("Preparing to create token").start();
205-
206-
const { rpc, sendAndConfirmTransaction } = createSolanaClient({
207-
urlOrMoniker: parsedRpcUrl.url,
208-
});
209-
210-
spinner.text = "Fetching the latest blockhash";
211-
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
212-
213-
spinner.text = "Preparing to create token mint";
214-
215-
const createMintTx = await buildCreateTokenTransaction({
216-
feePayer: payer,
217-
latestBlockhash,
218-
mint,
219-
mintAuthority,
220-
tokenProgram,
221-
freezeAuthority,
222-
updateAuthority: mintAuthority,
223-
metadata: {
224-
isMutable: true,
225-
name: options.name,
226-
symbol: options.symbol,
227-
uri: options.metadata,
228-
},
229-
decimals: Number(options.decimals),
230-
});
231-
232-
spinner.text = "Creating token mint: " + mint.address;
233-
let signature = await sendAndConfirmTransaction(
234-
await signTransactionMessageWithSigners(createMintTx),
235-
);
236-
237-
spinner.succeed("Token mint created: " + mint.address);
238-
239-
console.log(
240-
" ",
241-
getExplorerLink({
242-
cluster: parsedRpcUrl.cluster,
243-
transaction: signature,
244-
}),
245-
"\n",
246-
);
247-
248-
if (!options.amount) {
249-
warnMessage(
250-
"No amount provided, skipping minting tokens to your account",
179+
spinner.start("Preparing to create token");
180+
181+
try {
182+
const parsedRpcUrl = parseOptionsFlagForRpcUrl(
183+
options.url,
184+
/* use the Solana cli config's rpc url as the fallback */
185+
loadSolanaCliConfig().json_rpc_url,
186+
);
187+
188+
const tokenProgram = checkedTokenProgramAddress(
189+
address(options.tokenProgram),
190+
);
191+
192+
// payer will always be used to pay the fees
193+
const payer = await loadKeypairSignerFromFile(options.keypair);
194+
195+
// mint authority is required to sign in order to mint the initial tokens
196+
const mintAuthority = options.mintAuthority
197+
? await loadKeypairSignerFromFile(options.mintAuthority)
198+
: payer;
199+
200+
const freezeAuthority = await getAddressFromStringOrFilePath(
201+
options.freezeAuthority || payer.address,
251202
);
252203

253-
const mintCommand = [
254-
"npx mucho token mint",
255-
`--url ${parsedRpcUrl.cluster}`,
256-
`--mint ${mint.address}`,
257-
`--destination <DESTINATION_ADDRESS>`,
258-
`--amount <AMOUNT>`,
259-
];
204+
const mint = options.customMint
205+
? await loadKeypairSignerFromFile(options.customMint)
206+
: await generateKeyPairSigner();
207+
208+
const { rpc, sendAndConfirmTransaction, simulateTransaction } =
209+
createSolanaClient({
210+
urlOrMoniker: parsedRpcUrl.url,
211+
});
212+
213+
spinner.text = "Fetching the latest blockhash";
214+
const { value: latestBlockhash } = await rpc
215+
.getLatestBlockhash()
216+
.send();
217+
218+
spinner.text = "Preparing to create token mint";
219+
220+
const createMintTx = await buildCreateTokenTransaction({
221+
feePayer: payer,
222+
latestBlockhash,
223+
mint,
224+
mintAuthority,
225+
tokenProgram,
226+
freezeAuthority,
227+
updateAuthority: mintAuthority,
228+
metadata: {
229+
isMutable: true,
230+
name: options.name,
231+
symbol: options.symbol,
232+
uri: options.metadata,
233+
},
234+
decimals: Number(options.decimals),
235+
});
236+
237+
spinner.text = "Creating token mint: " + mint.address;
238+
let signature = await sendAndConfirmTransaction(createMintTx, {
239+
commitment: "confirmed",
240+
// skipPreflight: true,
241+
}).catch(async (err) => {
242+
await simulateTransactionOnThrow(
243+
simulateTransaction,
244+
err,
245+
createMintTx,
246+
);
247+
throw err;
248+
});
249+
250+
spinner.succeed("Token mint created: " + mint.address);
260251

261252
console.log(
262-
"To mint new tokens to any destination wallet in the future, run the following command:",
253+
" ",
254+
getExplorerLink({
255+
cluster: parsedRpcUrl.cluster,
256+
transaction: signature,
257+
}),
258+
"\n",
263259
);
264-
console.log(mintCommand.join(" "));
265-
spinner.stop();
266-
return;
267-
}
268260

269-
if (!isStringifiedNumber(options.amount)) {
270-
return errorOutro(
271-
"Please provide a valid amount with -a <AMOUNT>",
272-
"Invalid value",
261+
if (!options.amount) {
262+
warnMessage(
263+
"No amount provided, skipping minting tokens to your account",
264+
);
265+
266+
const mintCommand = [
267+
"npx mucho token mint",
268+
`--url ${parsedRpcUrl.cluster}`,
269+
`--mint ${mint.address}`,
270+
`--destination <DESTINATION_ADDRESS>`,
271+
`--amount <AMOUNT>`,
272+
];
273+
274+
console.log(
275+
"To mint new tokens to any destination wallet in the future, run the following command:",
276+
);
277+
console.log(mintCommand.join(" "));
278+
spinner.stop();
279+
return;
280+
}
281+
282+
if (!isStringifiedNumber(options.amount)) {
283+
return errorOutro(
284+
"Please provide a valid amount with -a <AMOUNT>",
285+
"Invalid value",
286+
);
287+
}
288+
289+
const destination = options.destination
290+
? await parseOrLoadSignerAddress(options.destination)
291+
: payer.address;
292+
293+
spinner.start(
294+
`Preparing to mint '${options.amount}' tokens to ${destination}`,
273295
);
274-
}
275296

276-
const destination = options.destination
277-
? await parseOrLoadSignerAddress(options.destination)
278-
: payer.address;
279-
280-
spinner.start(
281-
`Preparing to mint '${options.amount}' tokens to ${destination}`,
282-
);
283-
284-
const mintTokensTx = await buildMintTokensTransaction({
285-
feePayer: payer,
286-
latestBlockhash,
287-
mint,
288-
mintAuthority,
289-
tokenProgram,
290-
amount: Number(options.amount) * 10 ** Number(options.decimals),
291-
destination,
292-
});
293-
294-
const tokenPlurality = wordWithPlurality(
295-
options.amount,
296-
"token",
297-
"tokens",
298-
);
299-
300-
spinner.text = `Minting '${options.amount}' ${tokenPlurality} to ${destination}`;
301-
signature = await sendAndConfirmTransaction(
302-
await signTransactionMessageWithSigners(mintTokensTx),
303-
);
304-
spinner.succeed(
305-
`Minted '${options.amount}' ${tokenPlurality} to ${destination}`,
306-
);
307-
console.log(
308-
" ",
309-
getExplorerLink({
310-
cluster: parsedRpcUrl.cluster,
311-
transaction: signature,
312-
}),
313-
);
297+
const mintTokensTx = await buildMintTokensTransaction({
298+
feePayer: payer,
299+
latestBlockhash,
300+
mint,
301+
mintAuthority,
302+
tokenProgram,
303+
amount: Number(options.amount) * 10 ** Number(options.decimals),
304+
destination,
305+
});
306+
307+
const tokenPlurality = wordWithPlurality(
308+
options.amount,
309+
"token",
310+
"tokens",
311+
);
312+
313+
spinner.text = `Minting '${options.amount}' ${tokenPlurality} to ${destination}`;
314+
signature = await sendAndConfirmTransaction(mintTokensTx, {
315+
commitment: "confirmed",
316+
skipPreflight: true,
317+
}).catch(async (err) => {
318+
await simulateTransactionOnThrow(
319+
simulateTransaction,
320+
err,
321+
createMintTx,
322+
);
323+
throw err;
324+
});
325+
326+
spinner.succeed(
327+
`Minted '${options.amount}' ${tokenPlurality} to ${destination}`,
328+
);
329+
console.log(
330+
" ",
331+
getExplorerLink({
332+
cluster: parsedRpcUrl.cluster,
333+
transaction: signature,
334+
}),
335+
);
336+
} catch (err) {
337+
spinner.stop();
338+
let title = "Failed to complete operation";
339+
let message = err;
340+
let extraLog = null;
341+
342+
if (isSolanaError(err)) {
343+
title = "SolanaError";
344+
message = err.message;
345+
extraLog = err.context;
346+
}
347+
348+
errorOutro(message, title, extraLog);
349+
}
314350
});
315351
}

0 commit comments

Comments
 (0)