Skip to content
Draft
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 @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.

- SDK
- Add read-only Go SDK for `doublezero-geolocation` program with state deserialization, PDA derivation, and RPC client for querying geoprobe configuration
- Add write-side geolocation Go SDK: instruction builders for all 7 program instructions, transaction executor with finalization polling, and client write methods
- Client
- Increase default route liveness probe interval (TxMin/RxMin) from 300ms to 1s and raise MaxTxCeil from 1s to 3s to preserve backoff headroom

Expand Down
69 changes: 69 additions & 0 deletions sdk/geolocation/go/add_parent_device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package geolocation

import (
"fmt"

"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
)

type AddParentDeviceInstructionConfig struct {
Payer solana.PublicKey
ProbePK solana.PublicKey
DevicePK solana.PublicKey
ServiceabilityGlobalStatePK solana.PublicKey
}

func (c *AddParentDeviceInstructionConfig) Validate() error {
if c.Payer.IsZero() {
return fmt.Errorf("payer public key is required")
}
if c.ProbePK.IsZero() {
return fmt.Errorf("probe public key is required")
}
if c.DevicePK.IsZero() {
return fmt.Errorf("device public key is required")
}
if c.ServiceabilityGlobalStatePK.IsZero() {
return fmt.Errorf("serviceability global state public key is required")
}
return nil
}

func BuildAddParentDeviceInstruction(
programID solana.PublicKey,
config AddParentDeviceInstructionConfig,
) (solana.Instruction, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %w", err)
}

data, err := borsh.Serialize(struct {
Discriminator uint8
}{
Discriminator: uint8(AddParentDeviceInstructionIndex),
})
if err != nil {
return nil, fmt.Errorf("failed to serialize args: %w", err)
}

programConfigPDA, _, err := DeriveProgramConfigPDA(programID)
if err != nil {
return nil, fmt.Errorf("failed to derive program config PDA: %w", err)
}

accounts := []*solana.AccountMeta{
{PublicKey: config.ProbePK, IsSigner: false, IsWritable: true},
{PublicKey: config.DevicePK, IsSigner: false, IsWritable: false},
{PublicKey: programConfigPDA, IsSigner: false, IsWritable: false},
{PublicKey: config.ServiceabilityGlobalStatePK, IsSigner: false, IsWritable: false},
{PublicKey: config.Payer, IsSigner: true, IsWritable: true},
{PublicKey: solana.SystemProgramID, IsSigner: false, IsWritable: false},
}

return &solana.GenericInstruction{
ProgID: programID,
AccountValues: accounts,
DataBytes: data,
}, nil
}
155 changes: 144 additions & 11 deletions sdk/geolocation/go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,36 @@ var (
)

type Client struct {
log *slog.Logger
rpc RPCClient
programID solana.PublicKey
log *slog.Logger
rpc RPCClient
executor *executor
}

func New(log *slog.Logger, rpc RPCClient, programID solana.PublicKey) *Client {
func New(log *slog.Logger, rpc RPCClient, signer *solana.PrivateKey, programID solana.PublicKey) *Client {
return &Client{
log: log,
rpc: rpc,
programID: programID,
log: log,
rpc: rpc,
executor: NewExecutor(log, rpc, signer, programID),
}
}

func (c *Client) ProgramID() solana.PublicKey {
return c.programID
if c.executor == nil {
return solana.PublicKey{}
}
return c.executor.programID
}

func (c *Client) Signer() *solana.PrivateKey {
if c.executor == nil {
return nil
}
return c.executor.signer
}

// GetProgramConfig fetches the GeolocationProgramConfig account.
func (c *Client) GetProgramConfig(ctx context.Context) (*GeolocationProgramConfig, error) {
pda, _, err := DeriveProgramConfigPDA(c.programID)
pda, _, err := DeriveProgramConfigPDA(c.executor.programID)
if err != nil {
return nil, fmt.Errorf("failed to derive PDA: %w", err)
}
Expand All @@ -59,7 +69,7 @@ func (c *Client) GetProgramConfig(ctx context.Context) (*GeolocationProgramConfi

// GetGeoProbeByCode fetches a GeoProbe account by its code.
func (c *Client) GetGeoProbeByCode(ctx context.Context, code string) (*GeoProbe, error) {
pda, _, err := DeriveGeoProbePDA(c.programID, code)
pda, _, err := DeriveGeoProbePDA(c.executor.programID, code)
if err != nil {
return nil, fmt.Errorf("failed to derive PDA: %w", err)
}
Expand Down Expand Up @@ -95,7 +105,7 @@ func (c *Client) GetGeoProbes(ctx context.Context) ([]GeoProbe, error) {
},
}

accounts, err := c.rpc.GetProgramAccountsWithOpts(ctx, c.programID, opts)
accounts, err := c.rpc.GetProgramAccountsWithOpts(ctx, c.executor.programID, opts)
if err != nil {
return nil, fmt.Errorf("failed to get program accounts: %w", err)
}
Expand All @@ -111,3 +121,126 @@ func (c *Client) GetGeoProbes(ctx context.Context) ([]GeoProbe, error) {
}
return probes, nil
}

// InitProgramConfig initializes the geolocation program config.
func (c *Client) InitProgramConfig(
ctx context.Context,
config InitProgramConfigInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildInitProgramConfigInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, &ExecuteTransactionOptions{
SkipPreflight: true,
})
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// UpdateProgramConfig updates the geolocation program config.
func (c *Client) UpdateProgramConfig(
ctx context.Context,
config UpdateProgramConfigInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildUpdateProgramConfigInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, nil)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// CreateGeoProbe creates a new GeoProbe account.
func (c *Client) CreateGeoProbe(
ctx context.Context,
config CreateGeoProbeInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildCreateGeoProbeInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, &ExecuteTransactionOptions{
SkipPreflight: true,
})
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// UpdateGeoProbe updates an existing GeoProbe account.
func (c *Client) UpdateGeoProbe(
ctx context.Context,
config UpdateGeoProbeInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildUpdateGeoProbeInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, nil)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// DeleteGeoProbe deletes a GeoProbe account.
func (c *Client) DeleteGeoProbe(
ctx context.Context,
config DeleteGeoProbeInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildDeleteGeoProbeInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, nil)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// AddParentDevice adds a parent device to a GeoProbe.
func (c *Client) AddParentDevice(
ctx context.Context,
config AddParentDeviceInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildAddParentDeviceInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, nil)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}

// RemoveParentDevice removes a parent device from a GeoProbe.
func (c *Client) RemoveParentDevice(
ctx context.Context,
config RemoveParentDeviceInstructionConfig,
) (solana.Signature, *solanarpc.GetTransactionResult, error) {
instruction, err := BuildRemoveParentDeviceInstruction(c.executor.programID, config)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to build instruction: %w", err)
}

sig, res, err := c.executor.ExecuteTransaction(ctx, instruction, nil)
if err != nil {
return solana.Signature{}, nil, fmt.Errorf("failed to execute instruction: %w", err)
}
return sig, res, nil
}
18 changes: 12 additions & 6 deletions sdk/geolocation/go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
func TestSDK_Geolocation_Client_GetProgramConfig_HappyPath(t *testing.T) {
t.Parallel()

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

expected := &geolocation.GeolocationProgramConfig{
Expand All @@ -38,7 +39,7 @@ func TestSDK_Geolocation_Client_GetProgramConfig_HappyPath(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
got, err := client.GetProgramConfig(context.Background())
require.NoError(t, err)
require.Equal(t, expected.AccountType, got.AccountType)
Expand All @@ -48,6 +49,7 @@ func TestSDK_Geolocation_Client_GetProgramConfig_HappyPath(t *testing.T) {
func TestSDK_Geolocation_Client_GetProgramConfig_NotFound(t *testing.T) {
t.Parallel()

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

mockRPC := &mockRPCClient{
Expand All @@ -56,14 +58,15 @@ func TestSDK_Geolocation_Client_GetProgramConfig_NotFound(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
_, err := client.GetProgramConfig(context.Background())
require.ErrorIs(t, err, geolocation.ErrAccountNotFound)
}

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

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

expected := &geolocation.GeoProbe{
Expand Down Expand Up @@ -92,7 +95,7 @@ func TestSDK_Geolocation_Client_GetGeoProbeByCode_HappyPath(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
got, err := client.GetGeoProbeByCode(context.Background(), "ams-probe-01")
require.NoError(t, err)
require.Equal(t, expected.Code, got.Code)
Expand All @@ -102,6 +105,7 @@ func TestSDK_Geolocation_Client_GetGeoProbeByCode_HappyPath(t *testing.T) {
func TestSDK_Geolocation_Client_GetGeoProbeByCode_NotFound(t *testing.T) {
t.Parallel()

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

mockRPC := &mockRPCClient{
Expand All @@ -110,14 +114,15 @@ func TestSDK_Geolocation_Client_GetGeoProbeByCode_NotFound(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
_, err := client.GetGeoProbeByCode(context.Background(), "nonexistent")
require.ErrorIs(t, err, geolocation.ErrAccountNotFound)
}

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

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

probe1 := &geolocation.GeoProbe{
Expand Down Expand Up @@ -169,7 +174,7 @@ func TestSDK_Geolocation_Client_GetGeoProbes_HappyPath(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
probes, err := client.GetGeoProbes(context.Background())
require.NoError(t, err)
require.Len(t, probes, 2)
Expand All @@ -180,6 +185,7 @@ func TestSDK_Geolocation_Client_GetGeoProbes_HappyPath(t *testing.T) {
func TestSDK_Geolocation_Client_GetGeoProbes_Empty(t *testing.T) {
t.Parallel()

signer := solana.NewWallet().PrivateKey
programID := solana.NewWallet().PublicKey()

mockRPC := &mockRPCClient{
Expand All @@ -188,7 +194,7 @@ func TestSDK_Geolocation_Client_GetGeoProbes_Empty(t *testing.T) {
},
}

client := geolocation.New(slog.Default(), mockRPC, programID)
client := geolocation.New(slog.Default(), mockRPC, &signer, programID)
probes, err := client.GetGeoProbes(context.Background())
require.NoError(t, err)
require.Empty(t, probes)
Expand Down
13 changes: 13 additions & 0 deletions sdk/geolocation/go/constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package geolocation

// GeolocationInstructionType represents the type of geolocation instruction
type GeolocationInstructionType uint8

const (
InitProgramConfigInstructionIndex GeolocationInstructionType = 0
UpdateProgramConfigInstructionIndex GeolocationInstructionType = 1
CreateGeoProbeInstructionIndex GeolocationInstructionType = 2
UpdateGeoProbeInstructionIndex GeolocationInstructionType = 3
DeleteGeoProbeInstructionIndex GeolocationInstructionType = 4
AddParentDeviceInstructionIndex GeolocationInstructionType = 5
RemoveParentDeviceInstructionIndex GeolocationInstructionType = 6
)

// PDA seeds for geolocation program
const (
SeedPrefix = "doublezero"
Expand Down
Loading
Loading