Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/common/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestCompileWorkflowToWasm_TS_Success(t *testing.T) {
mainPath := filepath.Join(dir, "main.ts")
require.NoError(t, os.WriteFile(mainPath, []byte(`export async function main() { return "ok"; }
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"^1.16.0"}}
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"1.17.0-alpha.solana-log-trigger.1"}}
`), 0600))
install := exec.Command("bun", "install")
install.Dir = dir
Expand Down
16 changes: 12 additions & 4 deletions cmd/generate-bindings/solana/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,28 @@ generating both languages at once — use `--language` to disambiguate).

## What gets generated (CRE-reachable surface)

The Solana CRE capability is **write-only** through the keystone-forwarder: the
on-chain entrypoint is always `on_report`, and the payload is a bare
Borsh-encoded struct. Accordingly, both generators emit:
Writes go through the keystone-forwarder: the on-chain entrypoint is always
`on_report`, and the payload is a bare Borsh-encoded struct. Accordingly, both
generators emit:

- per-struct write methods: `writeReportFrom<Struct>` (single) and
`writeReportFrom<Struct>s` (Borsh `Vec`, u32-LE count + concatenated elements),
- a generic `writeReport(payload)` and `writeReportFromBorshEncodedVec(payloads)`,
- pure account/event **decoders** (discriminator-checked) — there is no
read/simulate capability, so these only decode bytes obtained elsewhere,
- per-event **log-trigger bindings**: an `<Event>Filters` type,
`encode<Event>Subkeys` (EQ comparers, OR across filter rows), and a typed
`logTrigger<Event>Log(filterName, filters, opts)` method whose output adapts
the raw log into decoded event data (Go: `bindings.DecodedLog[T]`, TS:
`SolanaDecodedLog<T>`). `opts.cpi` targets Anchor `emit_cpi!` events. Only
top-level scalar fields with supported subkey encodings are auto-filterable;
nested structs, vecs, arrays, bool, u128, and i128 need a manual
`SubkeyConfig`.
- a program mock (`new<Program>Mock`) that intercepts `writeReport` in the
test framework.

Native Anchor instruction builders and account fetchers are **not** generated
for TypeScript: they are unreachable through the write-only capability.
for TypeScript: they are unreachable through the CRE capability.

The wire format mirrors the Go bindings (`cre-sdk-go` solana `bindings` package):

Expand Down
96 changes: 96 additions & 0 deletions cmd/generate-bindings/solana/sourcecre.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,47 @@ import {
import { getAddressCodec, type Address } from '@solana/addresses'
{{- end}}
import {
{{- if .Events}}
adaptTrigger,
anchorCPILogTriggerConfig,
bytesToBase64,
{{- end}}
bytesToHex,
calculateAccountsHash,
encodeBorshVecU32,
encodeForwarderReport,
prepareSolanaReportRequest,
{{- if .UsesFloatSubkey}}
prepareSubkeyFloatValue,
{{- end}}
{{- if .UsesSubkeyValue}}
prepareSubkeyValue,
{{- end}}
type Runtime,
type SolanaAccountMeta,
SolanaClient,
solanaAccountMetasToJson,
solanaAddressToBytes,
type SolanaComputeConfig,
{{- if .Events}}
type SolanaDecodedLog,
type SolanaFilterLogTriggerRequestJson,
type SolanaLog,
type SolanaLogTriggerOptions,
type SolanaSubkeyConfigJson,
type SolanaValueComparatorJson,
type Trigger,
{{- end}}
} from '@chainlink/cre-sdk'

export const {{.ProgramIDConst}} = '{{.ProgramID}}'

export const {{.IdlConst}} = {{.IdlJSON}} as const
{{- if .Events}}

// Base64 of the compact IDL JSON, passed to log triggers as contractIdlJson.
const {{.IdlB64Const}} = '{{.IdlB64}}'
{{- end}}
{{- if or .Accounts .Events}}

const DISCRIMINATOR_SIZE = 8
Expand Down Expand Up @@ -111,6 +136,49 @@ export const parseAnyEvent = (data: Uint8Array): {{range $i, $e := .Events}}{{if
throw new Error(`unknown event discriminator: [${Array.from(disc).join(', ')}]`)
}
{{- end}}
{{- range .Triggers}}

/**
* Optional filter values for {{.Name}} log triggers. Set a field to filter on
* that value (OR across filter rows). Leave unset for wildcard. Only top-level
* scalar fields with supported subkey encodings are auto-filterable — nested
* structs, vecs, arrays, bool, u128, and i128 need a manual SubkeyConfig.
*/
{{- if .FilterFields}}
export type {{.Name}}Filters = {
{{- range .FilterFields}}
{{.Name}}?: {{.TSType}} | null
{{- end}}
}

export const encode{{.Name}}Subkeys = (filters: {{.Name}}Filters[]): SolanaSubkeyConfigJson[] => {
{{- range .FilterFields}}
const {{.Name}}Comparers: SolanaValueComparatorJson[] = []
{{- end}}
for (const f of filters) {
{{- range .FilterFields}}
if (f.{{.Name}} != null) {
{{.Name}}Comparers.push({
operator: 'COMPARISON_OPERATOR_EQ',
value: bytesToBase64({{.EncodeExpr}}),
})
}
{{- end}}
}
const subkeys: SolanaSubkeyConfigJson[] = []
{{- range .FilterFields}}
if ({{.Name}}Comparers.length > 0) {
subkeys.push({ path: ['{{.PathName}}'], comparers: {{.Name}}Comparers })
}
{{- end}}
return subkeys
}
{{- else}}
export type {{.Name}}Filters = Record<string, never>

export const encode{{.Name}}Subkeys = (_filters: {{.Name}}Filters[]): SolanaSubkeyConfigJson[] => []
{{- end}}
{{- end}}
{{- if or .Accounts .Events}}
{{end}}
export class {{.ClassName}} {
Expand Down Expand Up @@ -204,4 +272,32 @@ export class {{.ClassName}} {
)
}
{{- end}}
{{- range .Triggers}}

/**
* Registers a typed log trigger for {{.Name}} events. The trigger
* output is adapted to the decoded {{.Name}} data alongside the raw log.
* Pass opts.cpi for events emitted via Anchor's emit_cpi!.
*/
logTrigger{{.Name}}Log(
filterName: string,
filters: {{.Name}}Filters[] = [],
opts?: SolanaLogTriggerOptions,
): Trigger<SolanaLog, SolanaDecodedLog<{{.TypeName}}>> {
const config: SolanaFilterLogTriggerRequestJson = {
name: filterName,
address: bytesToBase64(this.programId),
eventName: '{{.EventName}}',
contractIdlJson: {{$.IdlB64Const}},
subkeys: encode{{.Name}}Subkeys(filters),
}
if (opts?.cpi) {
config.cpiFilterConfig = anchorCPILogTriggerConfig(this.programId)
}
return adaptTrigger(this.client.logTrigger(config), (log) => ({
log,
data: decode{{.Name}}Event(log.data),
}))
}
{{- end}}
}
Loading
Loading