Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
msrv = "1.82"
too-large-for-stack = 128
too-large-for-stack = 128
enum-variant-size-threshold = 4096
7 changes: 4 additions & 3 deletions crates/core/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ pub trait Filter: Send + Sync {

const DEDUP_CLEANUP_INTERVAL_SECS: u64 = 60;

type SeenInstructions = HashMap<(Signature, Vec<u8>), Instant>;
type SeenInstructionsMap = HashMap<(Signature, Vec<u8>), Instant>;
type SeenAccountsMap = HashMap<(Signature, Pubkey), Instant>;

pub struct DeduplicationFilter {
seen_instructions: Arc<RwLock<SeenInstructions>>,
seen_accounts: Arc<RwLock<HashMap<(Signature, Pubkey), Instant>>>,
seen_instructions: Arc<RwLock<SeenInstructionsMap>>,
seen_accounts: Arc<RwLock<SeenAccountsMap>>,
ttl: Duration,
creation: Instant,
last_cleanup_secs: AtomicU64,
Expand Down
4 changes: 2 additions & 2 deletions packages/renderer/src/cargoTomlGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function generateDecoderCargoToml(options: DecoderCargoTomlOptions): stri
features.push(serdeDeps);
}

if (withPostgres) {
if (withPostgres || withGraphQL) {
features.push('');
features.push('postgres = [');
features.push(' "carbon-core/postgres",');
Expand Down Expand Up @@ -104,7 +104,7 @@ export function generateDecoderCargoToml(options: DecoderCargoTomlOptions): stri
}
}

if (withPostgres) {
if (withPostgres || withGraphQL) {
dependencies.push('');
dependencies.push(sqlxDep);
dependencies.push(asyncTraitDep);
Expand Down
61 changes: 61 additions & 0 deletions packages/renderer/src/constants/rustKeywords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export const RUST_KEYWORDS = [
'as',
'break',
'const',
'continue',
'crate',
'else',
'enum',
'extern',
'false',
'fn',
'for',
'if',
'impl',
'in',
'let',
'loop',
'match',
'mod',
'move',
'mut',
'pub',
'ref',
'return',
'self',
'Self',
'static',
'struct',
'super',
'trait',
'true',
'type',
'unsafe',
'use',
'where',
'while',
'abstract',
'become',
'box',
'do',
'final',
'macro',
'override',
'priv',
'typeof',
'unsized',
'virtual',
'yield',
'async',
'await',
'dyn',
'try',
'union',
'macro_rules',
] as const;

export const RUST_KEYWORDS_SET = new Set<string>(RUST_KEYWORDS);

export function escapeRustKeyword(name: string): string {
return RUST_KEYWORDS_SET.has(name) ? `r#${name}` : name;
}
6 changes: 1 addition & 5 deletions packages/renderer/src/getPostgresTypeManifestVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,7 @@ export function getPostgresTypeManifestVisitor() {
if (inners.length === 1) {
return inners[0];
}
return {
imports: new ImportMap().mergeWith(...inners.map(i => i.imports)),
sqlxType: `(${inners.map(i => i.sqlxType).join(', ')})`,
postgresColumnType: `(${inners.map(i => i.postgresColumnType).join(', ')})`,
};
return jsonb();
},
visitFixedSizeType(node, { self }) {
// Fixed-size bytes stored as Vec<u8> (BYTEA) in Postgres
Expand Down
68 changes: 45 additions & 23 deletions packages/renderer/src/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
// GraphQLObject doesn't support empty structs
if (graphqlFields.length > 0) {
const schemaTemplate =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? 'graphqlSchemaPageGeneric.njk'
: 'graphqlSchemaPage.njk';
renderMap.add(
Expand Down Expand Up @@ -617,7 +617,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
// Instructions without arguments will only have instruction_metadata field
instructionsWithGraphQLSchemas.add(node.name);
const schemaTemplate =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? 'graphqlSchemaPageGeneric.njk'
: 'graphqlSchemaPage.njk';
renderMap.add(
Expand Down Expand Up @@ -745,7 +745,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
}
if (options.withGraphql !== false) {
const accountsGraphqlTemplate =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? 'accountsGraphqlModGeneric.njk'
: 'accountsGraphqlMod.njk';
const accountsGraphqlImports = new ImportMap().add('juniper::GraphQLObject');
Expand All @@ -767,7 +767,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
}
if (options.withGraphql !== false) {
const instructionsGraphqlTemplate =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? 'instructionsGraphqlModGeneric.njk'
: 'instructionsGraphqlMod.njk';
const instructionsGraphqlImports = new ImportMap().add('juniper::GraphQLObject');
Expand Down Expand Up @@ -809,7 +809,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
}
if (options.withGraphql !== false) {
const cpiEventSchemaTemplate =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? 'eventInstructionGraphqlSchemaPageGeneric.njk'
: 'eventInstructionGraphqlSchemaPage.njk';
const cpiEventSchemaImports = new ImportMap()
Expand Down Expand Up @@ -839,7 +839,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {

// Check if there are actually any GraphQL fields to generate
const hasAccountsWithFields =
options.postgresMode === 'generic'
options.postgresMode === 'generic' || options.withPostgres === false
? accountsToExport.length > 0
: accountsToExport.some(
acc => acc.data.kind === 'structTypeNode' && acc.data.fields.length > 0,
Expand All @@ -852,8 +852,9 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {

// Only generate query.rs if there are GraphQL fields to expose
if (hasActualGraphQLFields) {
// Use different query template based on postgres mode
if (options.postgresMode === 'generic') {
// Use generic query template when postgres mode is generic or when postgres is disabled
// (typed template needs per-instruction/account postgres rows which are only generated with postgres)
if (options.postgresMode === 'generic' || options.withPostgres === false) {
const graphqlQueryGenericImports = new ImportMap().add(
'juniper::{graphql_object, FieldResult}',
);
Expand Down Expand Up @@ -1015,21 +1016,42 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
const sourceField = `source.${prefix.join('.')}`;
const sourceColumn = `source.${column}`;

const expr = isJson
? manifest.sqlxType.includes('Json<')
? needsConversion
? `${sourceField}.map(|value| value.into())`
: sourceField
const needsSpecialHandling = isNode(itemType, 'arrayTypeNode') || isNode(itemType, 'tupleTypeNode');

let expr: string;
if (needsSpecialHandling) {
const innerExpr = buildExpression(itemType, 'value');
expr = innerExpr === 'value' ? sourceField : `${sourceField}.map(|value| ${innerExpr})`;
} else {
expr = isJson
? manifest.sqlxType.includes('Json<')
? needsConversion
? `${sourceField}.map(|value| value.into())`
: sourceField
: needsConversion
? `${sourceField}.map(|value| sqlx::types::Json(value.into()))`
: `${sourceField}.map(sqlx::types::Json)`
: needsConversion
? `${sourceField}.map(|value| sqlx::types::Json(value.into()))`
: `${sourceField}.map(sqlx::types::Json)`
: needsConversion
? `${sourceField}.map(|value| value.into())`
: sourceField;
? `${sourceField}.map(|value| value.into())`
: sourceField;
}

const reverseExpr = isJson
? `${sourceColumn}.map(|value| value.0)`
: buildReverseOptionType(typeNode, sourceColumn, manifest);
let reverseExpr: string;
if (needsSpecialHandling) {
const innerReverseExpr = buildReverse(itemType, 'value');
if (innerReverseExpr.includes('?')) {
const exprWithoutQuestion = innerReverseExpr.replace(/\?$/, '');
reverseExpr = `${sourceColumn}.map(|value| ${exprWithoutQuestion}).transpose()?`;
} else if (innerReverseExpr === 'value' || innerReverseExpr === 'value.0') {
reverseExpr = isJson ? `${sourceColumn}.map(|value| value.0)` : sourceColumn;
} else {
reverseExpr = `${sourceColumn}.map(|value| ${innerReverseExpr})`;
}
} else {
reverseExpr = isJson
? `${sourceColumn}.map(|value| value.0)`
: buildReverseOptionType(typeNode, sourceColumn, manifest);
}

return [
{
Expand Down Expand Up @@ -1210,7 +1232,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
if (typeNode.items.length === 1) {
return `${buildExpression(typeNode.items[0], `${prefix}`)}`;
}
return `(${typeNode.items.map((item, i) => buildExpression(item, `${prefix}.${i}`)).join(', ')})`;
return `sqlx::types::Json(serde_json::to_value(${prefix}).unwrap())`;
} else if (isNode(typeNode, 'publicKeyTypeNode')) {
// Pubkey from Rust needs conversion to Postgres Pubkey (solana_pubkey::Pubkey)
return `${prefix}.into()`;
Expand Down Expand Up @@ -1472,7 +1494,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
if (typeNode.items.length === 1) {
return `${buildReverse(typeNode.items[0], `${prefix}`)}`;
}
return `(${typeNode.items.map((it, i) => buildReverse(it, `${prefix}.${i}`)).join(', ')})`;
return `serde_json::from_value(${prefix}.0).map_err(|_| carbon_core::error::Error::Custom("Failed to deserialize tuple from JSON".to_string()))?`;
}
if (
isNode(typeNode, 'definedTypeLinkNode') ||
Expand Down
31 changes: 20 additions & 11 deletions packages/renderer/src/getTypeManifestVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { extendVisitor, mergeVisitor, pipe, visit } from '@codama/visitors-core'
import { ImportMap } from './ImportMap';
import { getDiscriminatorBytes } from './utils';
import { formatDocComments } from './utils/render';
import { escapeRustKeyword } from './constants/rustKeywords';

export type TypeManifest = {
imports: ImportMap;
Expand Down Expand Up @@ -143,25 +144,18 @@ export function getTypeManifestVisitor(

visitEnumStructVariantType(node, { self }) {
const name = pascalCase(node.name);
// Get the definedTypesMap and newtypeWrapperTypes from the original visitor (self)
// This is a workaround since we can't pass it through visitor context
const getDefinedTypesMap = (self as any).__definedTypesMap;
const enumDefinedTypesMap =
typeof getDefinedTypesMap === 'function' ? getDefinedTypesMap() : undefined;
const getNewtypeWrapperTypes = (self as any).__newtypeWrapperTypes;
const enumNewtypeWrapperTypes =
typeof getNewtypeWrapperTypes === 'function' ? getNewtypeWrapperTypes() : undefined;
// Store reference to original self for accessing definedTypesMap
const originalSelf = self;

// Create a custom visitor for enum struct variant fields that doesn't add 'pub'
// but includes BigArray detection logic
// Use originalSelf for visiting field types to ensure access to __definedTypesMap
const enumStructVisitor = extendVisitor(originalSelf, {
visitStructFieldType(node, { self: fieldSelf }) {
visitStructFieldType(node) {
// Visit the field type with originalSelf to get proper type resolution
const fieldManifest = visit(node.type, originalSelf);
const fieldName = snakeCase(node.name);
const fieldName = escapeRustKeyword(snakeCase(node.name));

// visitDefinedTypeLink already sets requiredBigArray appropriately (including undefined for newtype wrappers)
// Only need to override if it's a newtype wrapper to ensure it's undefined
Expand Down Expand Up @@ -338,8 +332,23 @@ export function getTypeManifestVisitor(
},

visitStructFieldType(node, { self }) {
// Check if this is a nested inline struct - flatten it
if (isNode(node.type, 'structTypeNode')) {
// Flatten: visit all nested fields directly
const nestedFields = node.type.fields.map(field => visit(field, self));
const mergedImports = new ImportMap().mergeWith(...nestedFields.map(f => f.imports));
const fieldTypes = nestedFields.map(f => f.type).join('\n');
const fieldBorshTypes = nestedFields.map(f => f.borshType).join('\n');

return {
imports: mergedImports,
type: fieldTypes, // No wrapper, just the fields
borshType: fieldBorshTypes,
};
}

const fieldManifest = visit(node.type, self);
const fieldName = snakeCase(node.name);
const fieldName = escapeRustKeyword(snakeCase(node.name));

// visitDefinedTypeLink already sets requiredBigArray appropriately (including undefined for newtype wrappers)
// Only check direct array types and use manifest's requiredBigArray
Expand Down Expand Up @@ -432,7 +441,7 @@ export function getTypeManifestVisitor(

export function getDiscriminatorManifest(
discriminators: DiscriminatorNode[] | undefined,
programName?: string | null,
_programName?: string | null,
discriminatorNames?: Map<number, string>,
): DiscriminatorManifest | null {
if (!discriminators || discriminators.length === 0) return null;
Expand Down
Loading
Loading