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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file.
- Add influxdb, prometheus, and device-health-oracle containers
- SDK
- Commands for setting global config, activating devices, updating devices, and closing device accounts now manage resource accounts.
- Serviceability: return error when GetProgramAccounts returns empty result instead of silently returning empty data
- Smartcontract
- feat(smartcontract): RFC 11 activation for User entity
- Device Health Oracle
Expand Down
36 changes: 26 additions & 10 deletions e2e/funder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,28 @@ func TestE2E_Funder(t *testing.T) {
require.NoError(t, err)
funderPK := funderPrivateKey.PublicKey()

// Check that the errors metric only contains "funder_account_balance_below_minimum" errors,
// which occur on startup while waiting for the manager/funder account to be funded.
// Check that the errors metric only contains expected startup errors:
// - "get_recipients": occurs when the serviceability program isn't ready yet
// - "funder_account_balance_below_minimum": occurs while waiting for the manager/funder account to be funded
metricsClient := dn.Funder.GetMetricsClient()
require.NoError(t, metricsClient.WaitForReady(ctx, 3*time.Second))
require.NoError(t, metricsClient.Fetch(ctx))
errors := metricsClient.GetCounterValues("doublezero_funder_errors_total")
require.NotNil(t, errors)
require.Len(t, errors, 1)
require.Equal(t, "funder_account_balance_below_minimum", errors[0].Labels["error_type"])
prevFunderAccountBalanceBelowMinimumCount := int(errors[0].Value)
allowedStartupErrors := map[string]bool{
"get_recipients": true,
"funder_account_balance_below_minimum": true,
}
for _, e := range errors {
require.True(t, allowedStartupErrors[e.Labels["error_type"]], "unexpected error type during startup: %s", e.Labels["error_type"])
}
var prevFunderAccountBalanceBelowMinimumCount int
for _, e := range errors {
if e.Labels["error_type"] == "funder_account_balance_below_minimum" {
prevFunderAccountBalanceBelowMinimumCount = int(e.Value)
break
}
}

// Check the funder account balance metric.
require.NoError(t, metricsClient.Fetch(ctx))
Expand Down Expand Up @@ -146,12 +158,16 @@ func TestE2E_Funder(t *testing.T) {
require.NoError(t, metricsClient.Fetch(ctx))
errors = metricsClient.GetCounterValues("doublezero_funder_errors_total")
require.NotNil(t, errors)
require.Len(t, errors, 1)
require.Equal(t, "funder_account_balance_below_minimum", errors[0].Labels["error_type"])
if int(errors[0].Value) > prevFunderAccountBalanceBelowMinimumCount {
return true
for _, e := range errors {
if e.Labels["error_type"] == "funder_account_balance_below_minimum" {
if int(e.Value) > prevFunderAccountBalanceBelowMinimumCount {
return true
}
log.Debug("--> Waiting for funder account balance below minimum error to increase", "account", funderPK, "prevCount", prevFunderAccountBalanceBelowMinimumCount, "currentCount", int(e.Value))
return false
}
}
log.Debug("--> Waiting for funder account balance below minimum error to increase", "account", funderPK, "prevCount", prevFunderAccountBalanceBelowMinimumCount, "currentCount", int(errors[0].Value))
log.Debug("--> Waiting for funder account balance below minimum error to appear", "account", funderPK)
return false
}, 60*time.Second, 5*time.Second)

Expand Down
10 changes: 8 additions & 2 deletions e2e/internal/devnet/smartcontract_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (dn *Devnet) InitSmartContract(ctx context.Context) error {
doublezero --version
doublezero init
echo

doublezero global-config authority set --activator-authority me --sentinel-authority me
echo

Expand All @@ -97,7 +97,7 @@ func (dn *Devnet) InitSmartContract(ctx context.Context) error {
doublezero global-config get
echo

doublezero global-config authority set --activator-authority me --sentinel-authority me
doublezero global-config authority set --activator-authority me --sentinel-authority me

# Populate location information onchain.
echo "==> Populating location information onchain"
Expand Down Expand Up @@ -144,6 +144,12 @@ func (dn *Devnet) InitSmartContract(ctx context.Context) error {
err = poll.Until(ctx, func() (bool, error) {
data, err := client.GetProgramData(ctx)
if err != nil {
// GetProgramData returns an error when the program has no accounts yet,
// which is expected before initialization completes. Continue polling.
if strings.Contains(err.Error(), "GetProgramAccounts returned empty result") {
dn.log.Debug("--> Waiting for program accounts to be created")
return false, nil
}
return false, fmt.Errorf("failed to load serviceability program client: %w", err)
}
config := data.Config
Expand Down
5 changes: 5 additions & 0 deletions smartcontract/sdk/go/serviceability/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package serviceability

import (
"context"
"fmt"

"github.com/gagliardetto/solana-go"
)
Expand Down Expand Up @@ -37,6 +38,10 @@ func (c *Client) GetProgramData(ctx context.Context) (*ProgramData, error) {
return nil, err
}

if len(out) == 0 {
return nil, fmt.Errorf("GetProgramAccounts returned empty result for program %s", c.programID)
}

// We need to re-init these fields to prevent appending if this client is reused
// and Load() is called multiple times.
config := Config{}
Expand Down
26 changes: 24 additions & 2 deletions smartcontract/sdk/go/serviceability/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,15 @@ var programconfigPayload = `
`

type mockSolanaClient struct {
payload string
pubkey solana.PublicKey
payload string
pubkey solana.PublicKey
returnEmpty bool
}

func (m *mockSolanaClient) GetProgramAccounts(context.Context, solana.PublicKey) (rpc.GetProgramAccountsResult, error) {
if m.returnEmpty {
return []*rpc.KeyedAccount{}, nil
}
data, err := hex.DecodeString(strings.ReplaceAll(m.payload, "\n", ""))
if err != nil {
return nil, err
Expand Down Expand Up @@ -431,3 +435,21 @@ func TestSDK_Serviceability_GetProgramData(t *testing.T) {

}
}

func TestSDK_Serviceability_GetProgramData_EmptyResult(t *testing.T) {
programID := solana.MustPublicKeyFromBase58("11111111111111111111111111111111")
client := &Client{
rpc: &mockSolanaClient{returnEmpty: true},
programID: programID,
}

_, err := client.GetProgramData(t.Context())
if err == nil {
t.Fatal("expected error for empty GetProgramAccounts result, got nil")
}

expectedErrSubstring := "GetProgramAccounts returned empty result"
if !strings.Contains(err.Error(), expectedErrSubstring) {
t.Fatalf("expected error to contain %q, got: %v", expectedErrSubstring, err)
}
}
Loading