diff --git a/chain_capabilities/stellar/actions/actions.go b/chain_capabilities/stellar/actions/actions.go index 3431601c0..22338c40d 100644 --- a/chain_capabilities/stellar/actions/actions.go +++ b/chain_capabilities/stellar/actions/actions.go @@ -2,6 +2,7 @@ package actions import ( "context" + "encoding/hex" "errors" "fmt" "math" @@ -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" @@ -90,7 +92,7 @@ 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") } @@ -98,8 +100,7 @@ func (s *Stellar) GetLatestLedger(ctx context.Context, metadata capabilities.Req 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( @@ -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) + 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) } diff --git a/chain_capabilities/stellar/actions/actions_test.go b/chain_capabilities/stellar/actions/actions_test.go index 7e20950a6..b02d699ef 100644 --- a/chain_capabilities/stellar/actions/actions_test.go +++ b/chain_capabilities/stellar/actions/actions_test.go @@ -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" @@ -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) { diff --git a/chain_capabilities/stellar/go.mod b/chain_capabilities/stellar/go.mod index 7560ffa2c..78acb7e57 100644 --- a/chain_capabilities/stellar/go.mod +++ b/chain_capabilities/stellar/go.mod @@ -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 ) @@ -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 diff --git a/chain_capabilities/stellar/go.sum b/chain_capabilities/stellar/go.sum index 1503f43e1..3368fd331 100644 --- a/chain_capabilities/stellar/go.sum +++ b/chain_capabilities/stellar/go.sum @@ -216,8 +216,8 @@ github.com/smartcontractkit/capabilities/libs v0.0.0-20260713160126-ac9205412b4c github.com/smartcontractkit/capabilities/libs v0.0.0-20260713160126-ac9205412b4c/go.mod h1:OQOpIgUGpq3/amLTES0M3B821ZuB6ky1yu/AGfOGd1c= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260701155917-1446a98ed330 h1:yAPPw1dowmKOswheTrHZn9QxE6gcMObXLWz3dhzhdHA= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260701155917-1446a98ed330/go.mod h1:ncZiIgraMh4F9lWDoJwB1TX395qZFR5bvnZcHbD0A1Q= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260714160921-4033d0253977 h1:3deFvPKyJCyPl7+L/tpAGILEAUkeq7vWnltlIJWvdMc= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260714160921-4033d0253977/go.mod h1:snfVBRRQTpC2x5O3bQHZe9SvJX5yv/SbG8oHkJTKLtE= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62 h1:o7vfwNQjQbMKQ9YsZFQOxvU7RMXD/wKnZsX5N9sDS3w= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260626151909-052e55e62e62/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260401162955-be2bc6b5264b h1:L1So1EDBDRET3j/TdV1Gjv3qWARoa/NPRaU7k4r30yA= @@ -230,8 +230,8 @@ github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16 h1:/vkKPJoweLkRd56V4YHGRAtTG4+/JAlgklGEfvH6l4c= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260709145319-7782fb89eb16/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9 h1:LQy2j2+TdKLSWsUTUYuqmQPn8kjqCLjGI3ZJYGtDc08= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528173149-f5b8336b19d9/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY=