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
66 changes: 63 additions & 3 deletions chain_capabilities/stellar/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package actions

import (
"context"
"encoding/hex"
"errors"
"fmt"
"math"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/types"
stellartypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/stellar"
"github.com/smartcontractkit/chainlink-framework/multinode"
"github.com/stellar/go-stellar-sdk/xdr"

capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common"
commonmon "github.com/smartcontractkit/capabilities/chain_capabilities/common/monitoring"
Expand Down Expand Up @@ -90,16 +92,15 @@ func (s *Stellar) Close() error {
func (s *Stellar) GetLatestLedger(ctx context.Context, metadata capabilities.RequestMetadata, _ *stellarcap.GetLatestLedgerRequest) (*capabilities.ResponseAndMetadata[*stellarcap.GetLatestLedgerResponse], caperrors.Error) {
s.lggr.Debug("Received GetLatestLedger request")

observe := func(_ context.Context, height *ctypes.ChainHeight) (*stellarcap.GetLatestLedgerResponse, error) {
observe := func(ctx context.Context, height *ctypes.ChainHeight) (*stellarcap.GetLatestLedgerResponse, error) {
if height == nil || height.Latest <= 0 {
return nil, fmt.Errorf("no agreed chain height available for GetLatestLedger consensus")
}
// stellar ledger sequences are uint32 on-chain
if height.Latest > math.MaxUint32 {
return nil, fmt.Errorf("agreed ledger sequence %d exceeds uint32", height.Latest)
}
// TODO PLEX-3243 implement a by sequence ledger fetch to populate block metadata
return &stellarcap.GetLatestLedgerResponse{Sequence: uint32(height.Latest)}, nil
return s.fetchLatestLedgerMetadata(ctx, uint32(height.Latest))
}

request := ctypes.NewLockableToBlockHashableRequest(
Expand Down Expand Up @@ -209,6 +210,65 @@ func (s *Stellar) Info() (capabilities.CapabilityInfo, error) {
return capabilities.CapabilityInfo{}, nil
}

func (s *Stellar) fetchLatestLedgerMetadata(ctx context.Context, latestLedger uint32) (*stellarcap.GetLatestLedgerResponse, error) {
resp, err := s.GetLedgers(ctx, stellartypes.GetLedgersRequest{
StartLedger: latestLedger,
Pagination: &stellartypes.LedgerPaginationOptions{Limit: 1},
})
if err != nil {
return nil, fmt.Errorf("failed to GetLedgers: %w", err)
}

if len(resp.Ledgers) == 0 {
return nil, fmt.Errorf("no ledger returned for sequence %d", latestLedger)
}
ledger := resp.Ledgers[0]
if ledger.Sequence != latestLedger {
return nil, fmt.Errorf("rpc returned ledger %d, expected %d", ledger.Sequence, latestLedger)
}

out := &stellarcap.GetLatestLedgerResponse{
Sequence: latestLedger,
LedgerCloseTime: ledger.LedgerCloseTime,
}

hash, err := hex.DecodeString(ledger.Hash)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: in all other caps, we have conversion from service reply to capability reply in common

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to pull in the stellar xdr library to make the decoding easier, I can't do that in chainlink-common

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I could do a hybrid or just have everything in one place

if err != nil {
return nil, fmt.Errorf("decode ledger hash %q: %w", ledger.Hash, err)
}
out.Hash = hash

// extract encoded data for cleaner sdk response (ok when fetching one block)
if ledger.LedgerHeaderXDR != "" {
var hist xdr.LedgerHeaderHistoryEntry
if err = xdr.SafeUnmarshalBase64(ledger.LedgerHeaderXDR, &hist); err != nil {
return nil, fmt.Errorf("failed to decode ledger header xdr: %w", err)
}
headerBin, err := hist.Header.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal ledger header: %w", err)
}
out.LedgerHeaderXdr = headerBin
out.ProtocolVersion = uint32(hist.Header.LedgerVersion)
}

if ledger.LedgerMetadataXDR != "" {
var meta xdr.LedgerCloseMeta
if err = xdr.SafeUnmarshalBase64(ledger.LedgerMetadataXDR, &meta); err != nil {
return nil, fmt.Errorf("failed to decode ledger metadata xdr: %w", err)
}
if v2, ok := meta.GetV2(); ok {
metaBin, err := v2.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal ledger close meta v2: %w", err)
}
out.LedgerMetadataXdr = metaBin
}
}

return out, nil
}

func isUserError(err error) bool {
return !errors.Is(err, context.DeadlineExceeded) && !isStellarNodeInfraError(err)
}
Expand Down
47 changes: 47 additions & 0 deletions chain_capabilities/stellar/actions/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package actions

import (
"context"
"encoding/hex"
"errors"
"fmt"
"testing"

"github.com/stellar/go-stellar-sdk/xdr"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -120,10 +122,55 @@ func TestGetLatestLedger(t *testing.T) {
helper := newMockedStellar(t)
helper.stellar.handler = testConsensusHandler{handle: runLockableToBlockHandle(&ctypes.ChainHeight{Latest: 123})}

// The RPC returns a LedgerHeaderHistoryEntry and a LedgerCloseMeta union; the
// capability response carries the inner LedgerHeader and the V2 close-meta arm.
hist := xdr.LedgerHeaderHistoryEntry{
Header: xdr.LedgerHeader{LedgerVersion: 22, LedgerSeq: 123},
}
headerB64, err := xdr.MarshalBase64(hist)
require.NoError(t, err)
wantHeaderBin, err := hist.Header.MarshalBinary()
require.NoError(t, err)

txSet, err := xdr.NewGeneralizedTransactionSet(1, xdr.TransactionSetV1{})
require.NoError(t, err)
v2 := xdr.LedgerCloseMetaV2{TxSet: txSet}
closeMeta, err := xdr.NewLedgerCloseMeta(2, v2)
require.NoError(t, err)
metaB64, err := xdr.MarshalBase64(closeMeta)
require.NoError(t, err)
wantMetaBin, err := v2.MarshalBinary()
require.NoError(t, err)

const hashHex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
wantHash, err := hex.DecodeString(hashHex)
require.NoError(t, err)

helper.stellarService.EXPECT().
GetLedgers(mock.Anything, stellartypes.GetLedgersRequest{
StartLedger: 123,
Pagination: &stellartypes.LedgerPaginationOptions{Limit: 1},
}).
Return(stellartypes.GetLedgersResponse{
Ledgers: []stellartypes.LedgerInfo{{
Sequence: 123,
Hash: hashHex,
LedgerCloseTime: 456,
LedgerHeaderXDR: headerB64,
LedgerMetadataXDR: metaB64,
}},
}, nil).
Once()

resp, err := helper.stellar.GetLatestLedger(t.Context(), capabilities.RequestMetadata{}, &stellarcap.GetLatestLedgerRequest{})
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, uint32(123), resp.Response.GetSequence())
require.Equal(t, int64(456), resp.Response.GetLedgerCloseTime())
require.Equal(t, wantHash, resp.Response.GetHash())
require.Equal(t, uint32(22), resp.Response.GetProtocolVersion())
require.Equal(t, wantHeaderBin, resp.Response.GetLedgerHeaderXdr())
require.Equal(t, wantMetaBin, resp.Response.GetLedgerMetadataXdr())
})

t.Run("no agreed height", func(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions chain_capabilities/stellar/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.26.2

require (
github.com/smartcontractkit/capabilities/libs v0.0.0-20260713160126-ac9205412b4c
github.com/smartcontractkit/chainlink-common v0.11.2-0.20260701155917-1446a98ed330
github.com/smartcontractkit/chainlink-common v0.11.2-0.20260714160921-4033d0253977
github.com/stellar/go-stellar-sdk v0.5.0
)

Expand Down Expand Up @@ -83,7 +83,7 @@ require (
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b
github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 // indirect
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect
github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect
github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d
Expand Down
8 changes: 4 additions & 4 deletions chain_capabilities/stellar/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading