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
17 changes: 17 additions & 0 deletions chain_capabilities/stellar/.mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
dir: "{{ .InterfaceDir }}"
mockname: "{{ .InterfaceName }}_mock"
outpkg: actions
inpackage: true
filename: "test_{{ .InterfaceName | snakecase }}_mock.go"
fail-on-missing: true
resolve-type-alias: false
packages:
github.com/smartcontractkit/chainlink-common/pkg/types:
config:
dir: actions
mockname: "StellarService_mock"
outpkg: actions
inpackage: true
filename: "test_stellar_service_mock.go"
interfaces:
StellarService:
1 change: 1 addition & 0 deletions chain_capabilities/stellar/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.25.3
134 changes: 99 additions & 35 deletions chain_capabilities/stellar/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,87 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors"
stellarcap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/stellar"
commoncfg "github.com/smartcontractkit/chainlink-common/pkg/config"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
"github.com/smartcontractkit/chainlink-common/pkg/types"
stellartypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/stellar"
"github.com/smartcontractkit/chainlink-framework/multinode"

capcommon "github.com/smartcontractkit/capabilities/chain_capabilities/common"
commonmon "github.com/smartcontractkit/capabilities/chain_capabilities/common/monitoring"
"github.com/smartcontractkit/capabilities/libs/chainconsensus"
ctypes "github.com/smartcontractkit/capabilities/libs/chainconsensus/types"

ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule"
"github.com/smartcontractkit/capabilities/chain_capabilities/stellar/metering"
"github.com/smartcontractkit/capabilities/chain_capabilities/stellar/monitoring"
"github.com/smartcontractkit/capabilities/libs/chainconsensus"
ctypes "github.com/smartcontractkit/capabilities/libs/chainconsensus/types"
)

// Stellar implements the CRE capability actions for the Stellar chain
// Stellar implements the CRE capability actions for the Stellar chain.
type Stellar struct {
types.StellarService
handler chainconsensus.RequestHandler
lggr logger.SugaredLogger
messageBuilder *monitoring.MessageBuilder
beholderProcessor beholder.ProtoProcessor
chainSelector uint64
handler chainconsensus.RequestHandler
lggr logger.SugaredLogger
messageBuilder *monitoring.MessageBuilder
beholderProcessor beholder.ProtoProcessor
chainSelector uint64
forwarderClient CREForwarderClient
forwarderLookbackLedgers int64
reportSizeLimit limits.BoundLimiter[commoncfg.Size]
transmissionScheduler ts.TransmissionScheduler
}

// NewStellar builds the Stellar capability actions.
func NewStellar(
Comment thread
Krish-vemula marked this conversation as resolved.
service types.StellarService,
forwarderAddress string,
forwarderLookbackLedgers int64,
lggr logger.Logger,
limitsFactory limits.Factory,
transmissionScheduler ts.TransmissionScheduler,
chainSelector uint64,
handler chainconsensus.RequestHandler,
messageBuilder *monitoring.MessageBuilder,
beholderProcessor beholder.ProtoProcessor,
) (*Stellar, error) {
return &Stellar{
StellarService: service,
handler: handler,
lggr: logger.Sugared(lggr),
messageBuilder: messageBuilder,
beholderProcessor: beholderProcessor,
chainSelector: chainSelector,
}, nil
if service == nil {
return nil, fmt.Errorf("stellar service is required")
}

st := &Stellar{
StellarService: service,
handler: handler,
lggr: logger.Sugared(lggr),
messageBuilder: messageBuilder,
beholderProcessor: beholderProcessor,
chainSelector: chainSelector,
forwarderClient: newForwarderClient(service, lggr, forwarderAddress, forwarderLookbackLedgers),
forwarderLookbackLedgers: forwarderLookbackLedgers,
transmissionScheduler: transmissionScheduler,
}
return st, st.initLimiters(limitsFactory)
}

func (s *Stellar) initLimiters(limitsFactory limits.Factory) (err error) {
s.reportSizeLimit, err = limits.MakeUpperBoundLimiter(limitsFactory, cresettings.Default.PerWorkflow.ChainWrite.ReportSizeLimit)
return err
}

func (s *Stellar) Close() error {
return services.CloseAll(s.reportSizeLimit)
}

func (s *Stellar) GetLatestLedger(ctx context.Context, _ capabilities.RequestMetadata, _ *stellarcap.GetLatestLedgerRequest) (*capabilities.ResponseAndMetadata[*stellarcap.GetLatestLedgerResponse], caperrors.Error) {
resp, err := s.StellarService.GetLatestLedger(ctx)
if err != nil {
return nil, GetError(err, false)
}
protoResp, err := stellarcap.ConvertGetLatestLedgerResponseToProto(resp)
if err != nil {
return nil, GetError(err, false)
}
return &capabilities.ResponseAndMetadata[*stellarcap.GetLatestLedgerResponse]{Response: protoResp}, nil
}

// ReadContract performs a consensus read of a read-only Soroban contract call.
Expand All @@ -59,7 +100,7 @@ func (s *Stellar) ReadContract(
metadata capabilities.RequestMetadata,
input *stellarcap.ReadContractRequest,
) (*capabilities.ResponseAndMetadata[*stellarcap.ReadContractResponse], caperrors.Error) {
request, err := stellarcap.ConvertReadContractRequestFromProto(input)
request, err := convertReadContractRequestFromProto(input)
if err != nil {
return nil, caperrors.NewPublicUserError(fmt.Errorf("invalid request: %w", err), caperrors.InvalidArgument)
}
Expand All @@ -81,13 +122,13 @@ func (s *Stellar) ReadContract(
metadata.ReferenceID,
metering.GetResponseMetadata(metering.ReadContract),
func(ctx context.Context) (*stellarcap.ReadContractResponse, uint64, error) {
response, err := s.StellarService.ReadContract(ctx, request)
response, err := s.SimulateTransaction(ctx, request)
if err != nil {
return nil, 0, err
}

return &stellarcap.ReadContractResponse{
Result: response.Result,
Result: response.ReturnValueXDR,
LedgerSequence: response.LedgerSequence,
Error: response.Error,
}, uint64(response.LedgerSequence), nil
Expand All @@ -109,29 +150,52 @@ func (s *Stellar) ReadContract(
return responseAndMetadata, nil
}

func (s *Stellar) GetLatestLedger(
_ context.Context,
_ capabilities.RequestMetadata,
_ *stellarcap.GetLatestLedgerRequest,
) (*capabilities.ResponseAndMetadata[*stellarcap.GetLatestLedgerResponse], caperrors.Error) {
return nil, caperrors.NewPublicSystemError(errors.New("unimplemented"), caperrors.Unknown)
func convertReadContractRequestFromProto(p *stellarcap.ReadContractRequest) (stellartypes.SimulateTransactionRequest, error) {
if p == nil {
return stellartypes.SimulateTransactionRequest{}, fmt.Errorf("readContractRequest is nil")
}
if p.GetContractId() == "" {
return stellartypes.SimulateTransactionRequest{}, fmt.Errorf("contractID is required")
}
if p.GetFunction() == "" {
return stellartypes.SimulateTransactionRequest{}, fmt.Errorf("function is required")
}

pArgs := p.GetArgs()
args := make([]stellartypes.ScVal, len(pArgs))
for i, psv := range pArgs {
sv, err := stellarcap.ProtoToScVal(psv)
if err != nil {
return stellartypes.SimulateTransactionRequest{}, fmt.Errorf("args[%d]: %w", i, err)
}
args[i] = sv
}
return stellartypes.SimulateTransactionRequest{
ContractID: p.GetContractId(),
Function: p.GetFunction(),
Args: args,
SourceAccount: p.GetSourceAccount(),
}, nil
}

func (s *Stellar) isUserErrorWriteReport(err error) bool {
return strings.HasPrefix(err.Error(), capcommon.UserError)
}

func (s *Stellar) WriteReport(
_ context.Context,
_ capabilities.RequestMetadata,
_ *stellarcap.WriteReportRequest,
) (*capabilities.ResponseAndMetadata[*stellarcap.WriteReportReply], caperrors.Error) {
return nil, caperrors.NewPublicSystemError(errors.New("unimplemented"), caperrors.Unknown)
func (s *Stellar) Info() (capabilities.CapabilityInfo, error) {
return capabilities.CapabilityInfo{}, nil
}

func isUserError(err error) bool {
return !errors.Is(err, context.DeadlineExceeded) && !isStellarNodeInfraError(err)
}

// isStellarNodeInfraError reports whether err is a node-availability failure. It checks both
// error identity and the message substring because errors reach this function through LOOP gRPC ,
// which preserve only the gRPC status code and message — Go error identity (errors.Is) does not survive that round trip.
// error identity and the message substring because errors reach this function through LOOP gRPC,
// which preserves only the gRPC status code and message.
func isStellarNodeInfraError(err error) bool {
return errors.Is(err, multinode.ErrNodeError) || strings.Contains(err.Error(), multinode.ErrNodeError.Error())
}

var GetError = capcommon.GetError
var NewUserError = caperrors.NewPublicUserError
104 changes: 96 additions & 8 deletions chain_capabilities/stellar/actions/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors"
stellarcap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/stellar"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
"github.com/smartcontractkit/chainlink-common/pkg/types"
stellartypes "github.com/smartcontractkit/chainlink-common/pkg/types/chains/stellar"
"github.com/smartcontractkit/chainlink-common/pkg/types/mocks"
Expand All @@ -24,6 +25,7 @@ import (

ctypes "github.com/smartcontractkit/capabilities/libs/chainconsensus/types"

ts "github.com/smartcontractkit/capabilities/chain_capabilities/common/transmission_schedule"
"github.com/smartcontractkit/capabilities/chain_capabilities/stellar/monitoring"
)

Expand Down Expand Up @@ -65,6 +67,92 @@ func validReadContractRequest() *stellarcap.ReadContractRequest {
}
}

func TestNewStellar(t *testing.T) {
t.Parallel()

t.Run("nil service", func(t *testing.T) {
t.Parallel()
lggr := logger.Test(t)
_, err := NewStellar(
nil,
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
100,
lggr,
limits.Factory{Logger: lggr},
ts.TransmissionScheduler{},
1,
testConsensusHandler{handle: runVolatileHashableHandle},
monitoring.NewMessageBuilder(types.ChainInfo{}, capabilities.CapabilityInfo{}, ""),
nopBeholderProcessor{},
)
require.Error(t, err)
require.Contains(t, err.Error(), "stellar service is required")
})

t.Run("success", func(t *testing.T) {
t.Parallel()
lggr := logger.Test(t)
svc := mocks.NewStellarService(t)
st, err := NewStellar(
svc,
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
100,
lggr,
limits.Factory{Logger: lggr},
ts.TransmissionScheduler{},
1,
testConsensusHandler{handle: runVolatileHashableHandle},
monitoring.NewMessageBuilder(types.ChainInfo{}, capabilities.CapabilityInfo{}, ""),
nopBeholderProcessor{},
)
require.NoError(t, err)
require.NotNil(t, st)
require.NoError(t, st.Close())
})
}

func TestGetLatestLedger(t *testing.T) {
t.Parallel()

t.Run("happy path", func(t *testing.T) {
t.Parallel()
helper := newMockedStellar(t)
helper.stellarService.EXPECT().
GetLatestLedger(mock.Anything).
Return(stellartypes.GetLatestLedgerResponse{
Sequence: 123,
LedgerCloseTime: 1_700_000_000,
}, 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(1_700_000_000), resp.Response.GetLedgerCloseTime())
})

t.Run("service error", func(t *testing.T) {
t.Parallel()
helper := newMockedStellar(t)
helper.stellarService.EXPECT().
GetLatestLedger(mock.Anything).
Return(stellartypes.GetLatestLedgerResponse{}, errors.New("node unavailable")).
Once()

_, err := helper.stellar.GetLatestLedger(t.Context(), capabilities.RequestMetadata{}, &stellarcap.GetLatestLedgerRequest{})
require.Error(t, err)
})
}

func TestStellar_Info(t *testing.T) {
t.Parallel()
helper := newMockedStellar(t)
info, err := helper.stellar.Info()
require.NoError(t, err)
require.Equal(t, capabilities.CapabilityInfo{}, info)
}

func TestReadContract(t *testing.T) {
t.Parallel()

Expand All @@ -90,9 +178,9 @@ func TestReadContract(t *testing.T) {
const ledgerSeq uint32 = 52_000
const result = "AAAAAwAAAAA=" // base64 XDR
helper.stellarService.EXPECT().
ReadContract(mock.Anything, mock.Anything).
Return(stellartypes.ReadContractResponse{
Result: result,
SimulateTransaction(mock.Anything, mock.Anything).
Return(stellartypes.SimulateTransactionResponse{
ReturnValueXDR: result,
LedgerSequence: ledgerSeq,
}, nil).
Once()
Expand All @@ -114,8 +202,8 @@ func TestReadContract(t *testing.T) {
// Plain errors (e.g. invalid input surfaced by the relayer) default to user errors.
expectedErr := errors.New("failed to decode contract id")
helper.stellarService.EXPECT().
ReadContract(mock.Anything, mock.Anything).
Return(stellartypes.ReadContractResponse{}, expectedErr).
SimulateTransaction(mock.Anything, mock.Anything).
Return(stellartypes.SimulateTransactionResponse{}, expectedErr).
Once()

_, err := helper.stellar.ReadContract(t.Context(), capabilities.RequestMetadata{
Expand All @@ -135,10 +223,10 @@ func TestReadContract(t *testing.T) {

// Errors tagged by the relayer with multinode.ErrNodeError must survive the
// observation-error serialization round trip and stay classified as infra/system.
expectedErr := fmt.Errorf("failed to read contract: %w", multinode.ErrNodeError)
expectedErr := fmt.Errorf("failed to simulate transaction: %w", multinode.ErrNodeError)
helper.stellarService.EXPECT().
ReadContract(mock.Anything, mock.Anything).
Return(stellartypes.ReadContractResponse{}, expectedErr).
SimulateTransaction(mock.Anything, mock.Anything).
Return(stellartypes.SimulateTransactionResponse{}, expectedErr).
Once()

_, err := helper.stellar.ReadContract(t.Context(), capabilities.RequestMetadata{
Expand Down
Loading
Loading