bridgesolana

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2026 License: MIT Imports: 16 Imported by: 0

README

bridgesolana

CI Go Reference Go Report Card License

Detect cross-chain bridge events from Solana program logs.

Status: pre-1.0. The public API may change in any 0.x.0 release; patch releases (0.x.y) will not break callers. See CHANGELOG.md.

Build a BridgeDetector once at process start, then hand it the raw log strings from a Solana transaction's metadata. Detect returns one Detection per matched bridge event.

A Detection carries the bridge identity (name, description, leg type) plus one of:

  • A populated CorrelationID — extracted in-band from an Anchor Program data: event. The detection is fully resolved; no RPC needed.
  • A non-nil Resolution — the bridge fires through a CPI without emitting an event. The caller fetches the relevant on-chain bytes via RPC and passes them to Resolution.Resolve, which parses and extracts the correlation ID in one step.

Usage

import "github.com/miradorlabs/bridgesolana"

d, err := bridgesolana.NewBridgeDetector()
if err != nil {
    log.Fatal(err)
}

// `logs` is the slice of "Program ..." strings from a transaction's
// metadata (e.g. `meta.logMessages` from a getTransaction RPC response).
for _, det := range d.Detect(logs) {
    // Log-mode: the correlation ID is already on the detection.
    if det.Resolution == nil {
        fmt.Printf("%s %s leg, correlation %s\n",
            det.BridgeName, det.BridgeLegType, det.CorrelationID)
        continue
    }

    // Instruction-mode: resolve via RPC. What you fetch depends on the leg.
    var data []byte
    switch det.BridgeLegType {
    case bridgesolana.LegTypeSource:
        // Find the writable account in this transaction owned by
        // det.Resolution.MessageProgramID whose first 8 bytes match
        // det.Resolution.AccountDiscriminator. Resolve will re-check
        // the discriminator and return matched=false if you guessed
        // wrong, so it's also fine to call it on every candidate.
        data = fetchAccountData(det.Resolution.MessageProgramID, det.Resolution.AccountDiscriminator)

    case bridgesolana.LegTypeDestination:
        // The bytes are the instruction data of the bridge's
        // destination call (e.g. CCTP's ReceiveMessage) executed
        // by det.Resolution.MessageProgramID in this transaction.
        data = fetchInstructionData(det.Resolution.MessageProgramID)
    }

    correlationID, matched, err := det.Resolution.Resolve(data)
    // Always check err before matched. matched=false with err!=nil
    // means the discriminator matched but the body parse failed —
    // logging it surfaces malformed on-chain data; checking only
    // matched would silently drop the error.
    if err != nil {
        log.Printf("resolve %s: %v", det.BridgeName, err)
        continue
    }
    if !matched {
        // Source leg: the candidate account didn't match the
        // expected discriminator — try the next one.
        // Destination leg: the bytes you passed don't look like the
        // expected destination instruction data.
        continue
    }
    fmt.Printf("%s %s leg, correlation %s\n",
        det.BridgeName, det.BridgeLegType, correlationID)
}

Both legs gate parsing on the leading 8-byte Anchor discriminator. Source legs use matched=false as a scan-and-skip signal so callers can iterate candidate accounts cheaply. Destination legs use it as a wrong-data-type guard — there is no candidate iteration to do, so matched=false on a destination resolve indicates the caller fed the wrong bytes (e.g. account data instead of instruction data) and should be logged.

Coverage

Bridge solana
CCTP V1 source
CCTP V1 destination
CCTP V2 source
CCTP V2 destination

API

type BridgeDetector struct{ /* ... */ }

func NewBridgeDetector() (*BridgeDetector, error)
func (d *BridgeDetector) Detect(logs []string) []Detection
func (d *BridgeDetector) ProgramIDs() []string

type Detection struct {
    BridgeName        string
    BridgeDescription string
    BridgeLegType     LegType
    CorrelationID     string      // populated for log-mode detections
    Resolution        *Resolution // non-nil for instruction-mode detections
}

type Resolution struct {
    MessageProgramID     string
    AccountDiscriminator [8]byte // source legs only (zero for destination)
}

func (r *Resolution) Resolve(data []byte) (id string, matched bool, err error)

type LegType string
const (
    LegTypeSource      LegType = "source"
    LegTypeDestination LegType = "destination"
)

A BridgeDetector is read-only after construction and safe to share across goroutines.

How it works

Bridge configurations are embedded JSON, one file per protocol (currently config/cctp.json). Each entry declares the program ID, the event or instruction name, and how to extract the correlation ID from the decoded payload — by fixed-offset uint64/uint32/bytes32 fields, or as a keccak256 hash of the message tail.

NewBridgeDetector builds two O(1) lookup maps:

  • discriminator → log-mode subscription
  • (programID, instructionName) → instruction-mode subscription

Detect is a streaming scan of the log lines that tracks the program invocation stack, so instruction names are only matched within their owning program's context.

For instruction-mode source legs, the correlation ID lives in an on-chain account created by the source instruction (CCTP, for example, calls this the MessageSent account, with V1 and V2 layouts of different sizes). For destination legs, it lives in the destination instruction's data (CCTP's ReceiveMessage). The per-bridge config supplies the discriminators and header sizes; Resolution.Resolve verifies them, decodes the trailing Vec<u8> body, and runs the configured correlation extraction in one step.

License

MIT. See LICENSE.

Documentation

Overview

Package bridgesolana detects cross-chain bridge events from Solana program logs.

Build a BridgeDetector once at process start, then hand it the raw log strings from a Solana transaction's metadata. BridgeDetector.Detect returns one Detection per matched bridge event.

A Detection carries the bridge identity (name, description, leg type) and one of two payloads:

  • A populated CorrelationID — extracted in-band from an Anchor "Program data:" event. The detection is fully resolved.
  • A non-nil Resolution — the bridge fires through a CPI without emitting an event. The caller fetches the relevant on-chain data via RPC and passes it to Resolution.Resolve to obtain the correlation ID.

Bridge configurations are embedded JSON, currently covering Circle CCTP V1 and V2 source/destination legs.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BridgeDetector

type BridgeDetector struct {
	// contains filtered or unexported fields
}

BridgeDetector scans Solana program logs and emits Detection records. It is read-only after construction and safe to share across goroutines.

func NewBridgeDetector

func NewBridgeDetector() (*BridgeDetector, error)

NewBridgeDetector builds a detector from the embedded bridge configs.

Example

ExampleNewBridgeDetector shows how to construct a detector. It is cheap and safe to call once at process start.

package main

import (
	"fmt"

	"github.com/miradorlabs/bridgesolana"
)

func main() {
	_, err := bridgesolana.NewBridgeDetector()
	fmt.Println(err)
}
Output:
<nil>

func (*BridgeDetector) Detect

func (d *BridgeDetector) Detect(logs []string) []Detection

Detect scans logs (the raw "Program ..." strings from a single Solana transaction's metadata) and returns one Detection per matched bridge event. The detector tracks the program invocation stack across log lines, so both "Program data:" events and "Program log: Instruction:" lines are scoped to the program currently executing.

A Detection with a non-empty CorrelationID is fully resolved. A Detection with a non-nil Resolution requires the caller to fetch the relevant account or instruction data via RPC and call the package helpers to extract the correlation ID.

Example

ExampleBridgeDetector_Detect shows the common path: pass the raw program log strings from a Solana transaction and read off each detection. A Detection with a populated CorrelationID is fully resolved; one with a non-nil Resolution requires the caller to fetch the relevant account or instruction data from RPC and pass it to Resolution.Resolve.

package main

import (
	"fmt"

	"github.com/miradorlabs/bridgesolana"
)

func main() {
	d, _ := bridgesolana.NewBridgeDetector()

	logs := []string{
		"Program CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd invoke [1]",
		"Program log: Instruction: ReceiveMessage",
		"Program CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd success",
	}

	for _, det := range d.Detect(logs) {
		fmt.Printf("%s %s leg (needs RPC follow-up: %t)\n",
			det.BridgeName, det.BridgeLegType, det.Resolution != nil)
	}
}
Output:
cctp destination leg (needs RPC follow-up: true)

func (*BridgeDetector) ProgramIDs

func (d *BridgeDetector) ProgramIDs() []string

ProgramIDs returns the unique set of Solana program IDs the detector matches against, sorted for stable output. Pass these to LogsSubscribeMentions / SubscribeProgramLogs to receive every transaction the detector can recognize.

type Detection

type Detection struct {
	BridgeName        string
	BridgeDescription string
	BridgeLegType     LegType
	CorrelationID     string
	Resolution        *Resolution
}

Detection is a single bridge event extracted from a Solana transaction's program logs.

CorrelationID is populated when the detector could extract it directly from the logs (Anchor "Program data:" events). Otherwise Resolution is non-nil and the caller must fetch the relevant on-chain data via RPC and pass it to Resolution.Resolve to obtain the correlation ID.

type LegType

type LegType string

LegType identifies whether a detected bridge event is the source leg or the destination leg of a cross-chain transfer.

const (
	LegTypeSource      LegType = "source"
	LegTypeDestination LegType = "destination"
)

type Resolution

type Resolution struct {
	// MessageProgramID is the program that holds the on-chain data
	// containing the correlation ID — a MessageSent account for source
	// legs, the ReceiveMessage instruction data for destination legs.
	MessageProgramID string

	// AccountDiscriminator is the 8-byte Anchor account discriminator
	// used to identify the relevant account among the transaction's
	// accounts. Set for source legs only; zero for destination legs,
	// which dispatches Resolve to the instruction-data parser.
	AccountDiscriminator [8]byte
	// contains filtered or unexported fields
}

Resolution describes how to obtain the correlation ID for an instruction-mode detection that did not carry it in-band. The caller uses MessageProgramID (and AccountDiscriminator on source legs) to locate the on-chain bytes, then hands them to Resolution.Resolve.

func (*Resolution) Resolve added in v0.2.0

func (r *Resolution) Resolve(data []byte) (id string, matched bool, err error)

Resolve extracts the correlation ID from raw on-chain data.

Both legs gate parsing on the leading 8-byte Anchor discriminator and return ("", false, nil) when it does not match. For source legs the scan-and-skip semantic lets callers walk a transaction's candidate accounts cheaply; for destination legs matched=false indicates the caller fed the wrong instruction data and should be treated as a data-shape failure.

matched=false can mean two distinct things: discriminator mismatch (err=nil) or the discriminator matched but the body parse failed (err!=nil). Callers must check err alongside matched — writing `if !matched { continue }` alone silently drops parse errors.

For source legs (AccountDiscriminator non-zero), pass the full source account data including its leading 8-byte Anchor discriminator. Resolve verifies the discriminator, parses the account body, and returns (id, true, nil) on success.

For destination legs (AccountDiscriminator zero), pass the full destination instruction data including its leading 8-byte Anchor instruction discriminator. Resolve verifies the discriminator, extracts the message bytes, and returns (id, true, nil) on success.

Resolve dispatches between source and destination by AccountDiscriminator being non-zero. This encodes the "source = account, destination = instruction data" pattern that fits every Anchor bridge surveyed so far. A future bridge with an account-backed destination leg would need an explicit LegType field on Resolution.

Data-shape errors past the discriminator gate (truncated payload, malformed correlation field) return ("", false, err).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL