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
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.14.0"}}
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"^1.15.0"}}
`), 0600))
install := exec.Command("bun", "install")
install.Dir = dir
Expand Down
1 change: 0 additions & 1 deletion cmd/generate-bindings/evm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ func (h *handler) processAbiDirectory(inputs Inputs) error {
indexContent += "// Code generated — DO NOT EDIT.\n"
for _, name := range generatedContracts {
indexContent += fmt.Sprintf("export * from './%s'\n", name)
indexContent += fmt.Sprintf("export * from './%s_mock'\n", name)
}
if err := os.WriteFile(indexPath, []byte(indexContent), 0o600); err != nil {
return fmt.Errorf("failed to write index.ts: %w", err)
Expand Down
85 changes: 85 additions & 0 deletions cmd/generate-bindings/solana/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CRE Solana Generated Bindings

Generates CRE workflow bindings from Anchor IDL files, for **Go** and
**TypeScript**.

The Go generator wraps a forked [anchor-go](https://github.com/gagliardetto/anchor-go)
(vendored under `./anchor-go`) and adds CRE write-report extensions. The
TypeScript generator (`tsbindgen.go`) consumes the same IDLs and emits bindings
on top of `@chainlink/cre-sdk` + `@solana/codecs`.

## Usage

```bash
# auto-detects go/typescript from project files
cre generate-bindings solana

# explicit
cre generate-bindings solana --language typescript
cre generate-bindings solana --language go
```

Defaults:

| | IDL input | Output |
|---|---|---|
| Go | `contracts/solana/src/idl/*.json` | `contracts/solana/src/generated/<program>/` (one package per program) |
| TypeScript | `contracts/solana/src/idl/*.json` | `contracts/solana/ts/generated/<Program>.ts` + `<Program>_mock.ts` + `index.ts` barrel |

`--out` overrides the output directory for the selected language (rejected when
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:

- 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,
- 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.

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

```
ForwarderReport = [32-byte sha256(concat account pubkeys)][u32-LE payload len][payload]
report request: EncoderName="solana", SigningAlgo="ecdsa", HashingAlgo="keccak256"
```

## TypeScript type coverage (v1)

| Anchor type | TypeScript | Codec |
|---|---|---|
| bool | boolean | `getBooleanCodec()` |
| u8…u32 / i8…i32 | number | `getU8Codec()`… |
| u64/u128 / i64/i128 | bigint | `getU64Codec()`… |
| f32 / f64 | number | `getF32Codec()` / `getF64Codec()` |
| string | string | `addCodecSizePrefix(getUtf8Codec(), getU32Codec())` |
| bytes | Uint8Array | `addCodecSizePrefix(getBytesCodec(), getU32Codec())` |
| pubkey | Address | `getAddressCodec()` |
| vec\<T\> | T[] | `getArrayCodec(inner, { size: getU32Codec() })` |
| array\<T, N\> | T[] | `getArrayCodec(inner, { size: N })` |
| option\<T\> | T \| null | `getNullableCodec(inner)` |
| defined struct | type ref | generated codec const |
| enum (scalar) | TS enum | `getEnumCodec(Enum)` (u8 tag) |

Unsupported types **fail loudly** at generation time (never silently
mis-encode): `u256`/`i256`, `COption`, data-carrying enums, tuple structs,
generics, cyclic type references.

## Tests

Golden source-compare tests live in `tsbindgen_test.go` against
`testdata/data_storage_ts/` and `testdata/feature_matrix_ts/`. Regenerate
goldens (Go + TS) with:

```bash
go generate ./cmd/generate-bindings/solana
```
13 changes: 9 additions & 4 deletions cmd/generate-bindings/solana/bindgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ func GenerateBindings(
return fmt.Errorf("invalid IDL: %w", err)
}
if parsedIdl.Address == nil || parsedIdl.Address.IsZero() {
return fmt.Errorf("address is empty in idl file: %s", pathToIdl)
slog.Warn("IDL has no address field; program_id.go will not be generated — pass the program ID at runtime",
"path", pathToIdl)
} else {
slog.Info("Using IDL address as program ID", "address", parsedIdl.Address.String())
}
slog.Info("Using IDL address as program ID", "address", parsedIdl.Address.String())

parsedIdl.Metadata.Name = bin.ToSnakeForSighash(parsedIdl.Metadata.Name)
// check that the name is not a reserved keyword:
Expand Down Expand Up @@ -85,11 +87,14 @@ func GenerateBindings(
ProgramId: parsedIdl.Address,
}

programIDStr := ""
if parsedIdl.Address != nil && !parsedIdl.Address.IsZero() {
programIDStr = parsedIdl.Address.String()
}
slog.Info("Parsed IDL successfully",
"version", parsedIdl.Metadata.Version,
"name", parsedIdl.Metadata.Name,
"address", parsedIdl.Address,
"programId", parsedIdl.Address.String(),
"programId", programIDStr,
"instructionsCount", len(parsedIdl.Instructions),
"accountsCount", len(parsedIdl.Accounts),
"eventsCount", len(parsedIdl.Events),
Expand Down
31 changes: 31 additions & 0 deletions cmd/generate-bindings/solana/gen_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package solana_test

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/cmd/generate-bindings/solana"
)

Expand All @@ -15,3 +20,29 @@ func TestGenerateBindings(t *testing.T) {
t.Fatal(err)
}
}

// TestGenerateBindings_MissingAddress verifies that an IDL without an address
// field succeeds (previously it returned an error). program_id.go must not be
// generated since there is no address to embed.
func TestGenerateBindings_MissingAddress(t *testing.T) {
idl := `{
"metadata": {"name": "no_addr", "version": "0.1.0", "spec": "0.1.0"},
"instructions": [
{"name": "on_report", "discriminator": [214,173,18,221,173,148,151,208], "accounts": [], "args": []}
],
"accounts": [],
"events": [],
"errors": [],
"types": []
}`
idlPath := filepath.Join(t.TempDir(), "no_addr.json")
require.NoError(t, os.WriteFile(idlPath, []byte(idl), 0o600))

outDir := t.TempDir()
err := solana.GenerateBindings(idlPath, "no_addr", outDir)
require.NoError(t, err)

// program_id.go must not be generated when there is no address in the IDL.
_, statErr := os.Stat(filepath.Join(outDir, "program_id.go"))
assert.True(t, os.IsNotExist(statErr), "program_id.go should not be generated when IDL has no address")
}
19 changes: 19 additions & 0 deletions cmd/generate-bindings/solana/mockcontract.ts.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Code generated — DO NOT EDIT.
import { addSolanaContractMock, type SolanaContractMock, type SolanaMock } from '@chainlink/cre-sdk/test'

import { {{.ProgramIDConst}} } from './{{.ClassName}}'

export type {{.MockName}} = SolanaContractMock

/**
* Registers a {{.ClassName}} program mock on a SolanaMock instance.
* The Solana CRE capability is write-only, so the mock routes writeReport
* calls targeting this program's ID; set the returned mock's writeReport
* property to define the reply.
*/
export function new{{.MockName}}(
solanaMock: SolanaMock,
programId: string | Uint8Array = {{.ProgramIDConst}},
): {{.MockName}} {
return addSolanaContractMock(solanaMock, { programId })
}
Loading
Loading