Skip to content

Commit 62545be

Browse files
authored
Add isProgramError helper in JS renderer (#127)
1 parent affff89 commit 62545be

File tree

10 files changed

+190
-1
lines changed

10 files changed

+190
-1
lines changed

.changeset/tasty-lamps-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@kinobi-so/renderers-js": patch
3+
---
4+
5+
Add isProgramError helper

packages/renderers-js/e2e/anchor/src/generated/errors/wenTransferGuard.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
* @see https://github.com/kinobi-so/kinobi
77
*/
88

9+
import {
10+
isProgramError,
11+
type Address,
12+
type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM,
13+
type SolanaError,
14+
} from '@solana/web3.js';
15+
import { WEN_TRANSFER_GUARD_PROGRAM_ADDRESS } from '../programs';
16+
917
/** CpiRuleEnforcementFailed: Cpi Rule Enforcement Failed */
1018
export const WEN_TRANSFER_GUARD_ERROR__CPI_RULE_ENFORCEMENT_FAILED = 0x1770; // 6000
1119
/** TransferAmountRuleEnforceFailed: Transfer Amount Rule Enforce Failed */
@@ -60,3 +68,21 @@ export function getWenTransferGuardErrorMessage(
6068

6169
return 'Error message not available in production bundles.';
6270
}
71+
72+
export function isWenTransferGuardError<
73+
TProgramErrorCode extends WenTransferGuardError,
74+
>(
75+
error: unknown,
76+
transactionMessage: {
77+
instructions: Record<number, { programAddress: Address }>;
78+
},
79+
code?: TProgramErrorCode
80+
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> &
81+
Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
82+
return isProgramError<TProgramErrorCode>(
83+
error,
84+
transactionMessage,
85+
WEN_TRANSFER_GUARD_PROGRAM_ADDRESS,
86+
code
87+
);
88+
}

packages/renderers-js/e2e/system/src/generated/errors/system.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
* @see https://github.com/kinobi-so/kinobi
77
*/
88

9+
import {
10+
isProgramError,
11+
type Address,
12+
type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM,
13+
type SolanaError,
14+
} from '@solana/web3.js';
15+
import { SYSTEM_PROGRAM_ADDRESS } from '../programs';
16+
917
/** AccountAlreadyInUse: an account with the same address already exists */
1018
export const SYSTEM_ERROR__ACCOUNT_ALREADY_IN_USE = 0x0; // 0
1119
/** ResultWithNegativeLamports: account does not have enough SOL to perform the operation */
@@ -58,3 +66,19 @@ export function getSystemErrorMessage(code: SystemError): string {
5866

5967
return 'Error message not available in production bundles.';
6068
}
69+
70+
export function isSystemError<TProgramErrorCode extends SystemError>(
71+
error: unknown,
72+
transactionMessage: {
73+
instructions: Record<number, { programAddress: Address }>;
74+
},
75+
code?: TProgramErrorCode
76+
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> &
77+
Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
78+
return isProgramError<TProgramErrorCode>(
79+
error,
80+
transactionMessage,
81+
SYSTEM_PROGRAM_ADDRESS,
82+
code
83+
);
84+
}

packages/renderers-js/e2e/system/test/transferSol.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ import {
33
type Address,
44
appendTransactionMessageInstruction,
55
generateKeyPairSigner,
6+
isSolanaError,
67
lamports,
78
pipe,
9+
SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM,
10+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
11+
SolanaError,
812
} from '@solana/web3.js';
913
import test from 'ava';
1014
import {
1115
getTransferSolInstruction,
16+
isSystemError,
1217
parseTransferSolInstruction,
18+
SYSTEM_ERROR__RESULT_WITH_NEGATIVE_LAMPORTS,
19+
type SystemError,
1320
} from '../src/index.js';
1421
import {
1522
createDefaultSolanaClient,
@@ -46,6 +53,62 @@ test('it transfers SOL from one account to another', async (t) => {
4653
t.is(await getBalance(client, destination), lamports(1_000_000_000n));
4754
});
4855

56+
test('it fails if the source account does not have enough SOLs', async (t) => {
57+
// Given a source account with 1 SOL and a destination account with no SOL.
58+
const client = createDefaultSolanaClient();
59+
const source = await generateKeyPairSignerWithSol(client, 1_000_000_000n);
60+
const destination = (await generateKeyPairSigner()).address;
61+
62+
// When the source account tries to transfer 2 SOLs to the destination account.
63+
const transferSol = getTransferSolInstruction({
64+
source,
65+
destination,
66+
amount: 2_000_000_000,
67+
});
68+
const txMessage = pipe(await createDefaultTransaction(client, source), (tx) =>
69+
appendTransactionMessageInstruction(transferSol, tx)
70+
);
71+
const promise = signAndSendTransaction(client, txMessage);
72+
73+
// Then we expect the transaction to fail and to have the correct error code.
74+
const error = await t.throwsAsync(promise);
75+
if (
76+
isSolanaError(
77+
error,
78+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE
79+
)
80+
) {
81+
error satisfies SolanaError<
82+
typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE
83+
>;
84+
if (isSystemError(error.cause, txMessage)) {
85+
error.cause satisfies SolanaError<
86+
typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM
87+
> & { readonly context: { readonly code: SystemError } };
88+
if (
89+
isSystemError(
90+
error.cause,
91+
txMessage,
92+
SYSTEM_ERROR__RESULT_WITH_NEGATIVE_LAMPORTS
93+
)
94+
) {
95+
error.cause.context
96+
.code satisfies typeof SYSTEM_ERROR__RESULT_WITH_NEGATIVE_LAMPORTS;
97+
t.is(
98+
error.cause.context.code,
99+
SYSTEM_ERROR__RESULT_WITH_NEGATIVE_LAMPORTS
100+
);
101+
} else {
102+
t.fail('Expected a negative lamports system program error');
103+
}
104+
} else {
105+
t.fail('Expected a system program error');
106+
}
107+
} else {
108+
t.fail('Expected a preflight failure');
109+
}
110+
});
111+
49112
test('it parses the accounts and the data of an existing transfer SOL instruction', async (t) => {
50113
// Given a transfer SOL instruction with the following accounts and data.
51114
const source = await generateKeyPairSigner();

packages/renderers-js/e2e/token/src/generated/errors/associatedToken.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
* @see https://github.com/kinobi-so/kinobi
77
*/
88

9+
import {
10+
isProgramError,
11+
type Address,
12+
type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM,
13+
type SolanaError,
14+
} from '@solana/web3.js';
15+
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS } from '../programs';
16+
917
/** InvalidOwner: Associated token account owner does not match address derivation */
1018
export const ASSOCIATED_TOKEN_ERROR__INVALID_OWNER = 0x0; // 0
1119

@@ -31,3 +39,21 @@ export function getAssociatedTokenErrorMessage(
3139

3240
return 'Error message not available in production bundles.';
3341
}
42+
43+
export function isAssociatedTokenError<
44+
TProgramErrorCode extends AssociatedTokenError,
45+
>(
46+
error: unknown,
47+
transactionMessage: {
48+
instructions: Record<number, { programAddress: Address }>;
49+
},
50+
code?: TProgramErrorCode
51+
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> &
52+
Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
53+
return isProgramError<TProgramErrorCode>(
54+
error,
55+
transactionMessage,
56+
ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
57+
code
58+
);
59+
}

packages/renderers-js/e2e/token/src/generated/errors/token.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
* @see https://github.com/kinobi-so/kinobi
77
*/
88

9+
import {
10+
isProgramError,
11+
type Address,
12+
type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM,
13+
type SolanaError,
14+
} from '@solana/web3.js';
15+
import { TOKEN_PROGRAM_ADDRESS } from '../programs';
16+
917
/** NotRentExempt: Lamport balance below rent-exempt threshold */
1018
export const TOKEN_ERROR__NOT_RENT_EXEMPT = 0x0; // 0
1119
/** InsufficientFunds: Insufficient funds */
@@ -102,3 +110,19 @@ export function getTokenErrorMessage(code: TokenError): string {
102110

103111
return 'Error message not available in production bundles.';
104112
}
113+
114+
export function isTokenError<TProgramErrorCode extends TokenError>(
115+
error: unknown,
116+
transactionMessage: {
117+
instructions: Record<number, { programAddress: Address }>;
118+
},
119+
code?: TProgramErrorCode
120+
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> &
121+
Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
122+
return isProgramError<TProgramErrorCode>(
123+
error,
124+
transactionMessage,
125+
TOKEN_PROGRAM_ADDRESS,
126+
code
127+
);
128+
}

packages/renderers-js/public/templates/fragments/programErrors.njk

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ export function {{ programGetErrorMessageFunction }}(code: {{ programErrorUnion
2626

2727
return 'Error message not available in production bundles.';
2828
}
29+
30+
export function {{ programIsErrorFunction }}<TProgramErrorCode extends {{ programErrorUnion }}>(
31+
error: unknown,
32+
transactionMessage: { instructions: Record<number, { programAddress: Address }> },
33+
code?: TProgramErrorCode,
34+
): error is SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> & Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> {
35+
return isProgramError<TProgramErrorCode>(error, transactionMessage, {{ programAddressConstant }}, code);
36+
}

packages/renderers-js/src/ImportMap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ const DEFAULT_EXTERNAL_MODULE_MAP: Record<string, string> = {
1010
solanaCodecsDataStructures: '@solana/web3.js',
1111
solanaCodecsNumbers: '@solana/web3.js',
1212
solanaCodecsStrings: '@solana/web3.js',
13+
solanaErrors: '@solana/web3.js',
1314
solanaInstructions: '@solana/web3.js',
1415
solanaOptions: '@solana/web3.js',
16+
solanaPrograms: '@solana/web3.js',
1517
solanaSigners: '@solana/web3.js',
1618
};
1719

@@ -22,8 +24,10 @@ const DEFAULT_GRANULAR_EXTERNAL_MODULE_MAP: Record<string, string> = {
2224
solanaCodecsDataStructures: '@solana/codecs',
2325
solanaCodecsNumbers: '@solana/codecs',
2426
solanaCodecsStrings: '@solana/codecs',
27+
solanaErrors: '@solana/errors',
2528
solanaInstructions: '@solana/instructions',
2629
solanaOptions: '@solana/codecs',
30+
solanaPrograms: '@solana/programs',
2731
solanaSigners: '@solana/signers',
2832
};
2933

packages/renderers-js/src/fragments/programErrors.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ export function getProgramErrorsFragment(
99
},
1010
): Fragment {
1111
const { programNode, nameApi } = scope;
12+
const programAddressConstant = nameApi.programAddressConstant(programNode.name);
1213
return fragmentFromTemplate('programErrors.njk', {
1314
errors: programNode.errors,
1415
getProgramErrorConstant: (name: string) =>
1516
nameApi.programErrorConstantPrefix(programNode.name) + nameApi.programErrorConstant(name),
17+
programAddressConstant,
1618
programErrorMessagesMap: nameApi.programErrorMessagesMap(programNode.name),
1719
programErrorUnion: nameApi.programErrorUnion(programNode.name),
1820
programGetErrorMessageFunction: nameApi.programGetErrorMessageFunction(programNode.name),
19-
});
21+
programIsErrorFunction: nameApi.programIsErrorFunction(programNode.name),
22+
})
23+
.addImports('generatedPrograms', [programAddressConstant])
24+
.addImports('solanaPrograms', ['isProgramError'])
25+
.addImports('solanaErrors', ['type SolanaError', 'type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM'])
26+
.addImports('solanaAddresses', ['type Address']);
2027
}

packages/renderers-js/src/nameTransformers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type NameTransformerKey =
5454
| 'programInstructionsEnumVariant'
5555
| 'programInstructionsIdentifierFunction'
5656
| 'programInstructionsParsedUnionType'
57+
| 'programIsErrorFunction'
5758
| 'resolverFunction';
5859

5960
export type NameTransformers = Record<NameTransformerKey, NameTransformer>;
@@ -117,5 +118,6 @@ export const DEFAULT_NAME_TRANSFORMERS: NameTransformers = {
117118
programInstructionsEnumVariant: name => `${pascalCase(name)}`,
118119
programInstructionsIdentifierFunction: name => `identify${pascalCase(name)}Instruction`,
119120
programInstructionsParsedUnionType: name => `Parsed${pascalCase(name)}Instruction`,
121+
programIsErrorFunction: name => `is${pascalCase(name)}Error`,
120122
resolverFunction: name => `${camelCase(name)}`,
121123
};

0 commit comments

Comments
 (0)