Skip to content

Commit a0d9f8e

Browse files
committed
geolocation: add set-result-destination CLI command and Go SDK support
Rust SDK command, CLI with client-side destination validation, display in user get, Go SDK deserialization with backwards compatibility.
1 parent 569f815 commit a0d9f8e

10 files changed

Lines changed: 487 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ All notable changes to this project will be documented in this file.
3434
- Allow pending users with subs to be deleted
3535
- Geolocation
3636
- Standardize CLI flag naming: probe mutation commands use `--probe` (was `--code`) accepting pubkey or code; rename `--signing-keypair``--signing-pubkey` and `--target-pk``--target-signing-pubkey`; add `--json-compact` to `get` commands
37+
- Add optional result destination to `GeolocationUser` so LocationOffsets can be sent to an alternate endpoint instead of the target IP; supports both IP and domain destinations (e.g., `185.199.108.1:9000` or `results.example.com:9000`); includes `SetResultDestination` onchain instruction, CLI `user set-result-destination` command, and Go SDK deserialization (backwards-compatible with existing accounts)
3738
- geoprobe-target can now store LocationOffset messages in ClickHouse
3839
- Add ICMP pinger to geoprobe-agent for measuring outbound ICMP targets with interleaved batch send/receive, integrated into the existing measurement cycle alongside TWAMP
3940
- Remove `--additional-parent`, `--additional-targets`, `--additional-icmp-targets`, and `--allowed-pubkeys` CLI flags from geoprobe-agent; all configuration now comes from onchain state via parent and target discovery

client/doublezero-geolocation-cli/src/cli/user.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use doublezero_cli::geolocation::user::{
33
add_target::AddTargetCliCommand, create::CreateGeolocationUserCliCommand,
44
delete::DeleteGeolocationUserCliCommand, get::GetGeolocationUserCliCommand,
55
list::ListGeolocationUserCliCommand, remove_target::RemoveTargetCliCommand,
6+
set_result_destination::SetResultDestinationCliCommand,
67
update_payment_status::UpdatePaymentStatusCliCommand,
78
};
89

@@ -26,6 +27,8 @@ pub enum UserCommands {
2627
AddTarget(AddTargetCliCommand),
2728
/// Remove a target from a user
2829
RemoveTarget(RemoveTargetCliCommand),
30+
/// Set result destination for geolocation results
31+
SetResultDestination(SetResultDestinationCliCommand),
2932
/// Update payment status (foundation-only)
3033
UpdatePayment(UpdatePaymentStatusCliCommand),
3134
}

client/doublezero-geolocation-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ fn main() -> eyre::Result<()> {
121121
UserCommands::List(args) => args.execute(&client, &mut handle),
122122
UserCommands::AddTarget(args) => args.execute(&client, &mut handle),
123123
UserCommands::RemoveTarget(args) => args.execute(&client, &mut handle),
124+
UserCommands::SetResultDestination(args) => args.execute(&client, &mut handle),
124125
UserCommands::UpdatePayment(args) => args.execute(&client, &mut handle),
125126
},
126127
Command::InitConfig(args) => args.execute(&client, &mut handle),

sdk/geolocation/go/state.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,15 @@ type KeyedGeolocationUser struct {
327327
}
328328

329329
type GeolocationUser struct {
330-
AccountType AccountType // 1 byte
331-
Owner solana.PublicKey // 32 bytes
332-
Code string // 4-byte length prefix + UTF-8 bytes
333-
TokenAccount solana.PublicKey // 32 bytes
334-
PaymentStatus GeolocationPaymentStatus // 1 byte
335-
Billing GeolocationBillingConfig // 1 + 16 = 17 bytes
336-
Status GeolocationUserStatus // 1 byte
337-
Targets []GeolocationTarget // 4-byte count + 71*N bytes
330+
AccountType AccountType // 1 byte
331+
Owner solana.PublicKey // 32 bytes
332+
Code string // 4-byte length prefix + UTF-8 bytes
333+
TokenAccount solana.PublicKey // 32 bytes
334+
PaymentStatus GeolocationPaymentStatus // 1 byte
335+
Billing GeolocationBillingConfig // 1 + 16 = 17 bytes
336+
Status GeolocationUserStatus // 1 byte
337+
Targets []GeolocationTarget // 4-byte count + 71*N bytes
338+
ResultDestination string // 4-byte length prefix + UTF-8 bytes (empty = unset)
338339
}
339340

340341
func (g *GeolocationUser) Serialize(w io.Writer) error {
@@ -369,6 +370,9 @@ func (g *GeolocationUser) Serialize(w io.Writer) error {
369370
return err
370371
}
371372
}
373+
if err := enc.Encode(g.ResultDestination); err != nil {
374+
return err
375+
}
372376
return nil
373377
}
374378

@@ -446,5 +450,10 @@ func (g *GeolocationUser) Deserialize(data []byte) error {
446450
return err
447451
}
448452
}
453+
// ResultDestination is appended; old accounts without it default to empty string.
454+
if err := dec.Decode(&g.ResultDestination); err != nil {
455+
g.ResultDestination = ""
456+
return nil
457+
}
449458
return nil
450459
}

sdk/geolocation/go/state_test.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ func TestSDK_Geolocation_State_GeolocationUser_RoundTrip(t *testing.T) {
190190
GeoProbePK: solana.NewWallet().PublicKey(),
191191
},
192192
},
193+
ResultDestination: "185.199.108.1:9000",
193194
}
194195

195196
var buf bytes.Buffer
@@ -208,6 +209,7 @@ func TestSDK_Geolocation_State_GeolocationUser_RoundTrip(t *testing.T) {
208209
require.Len(t, decoded.Targets, 2)
209210
require.Equal(t, original.Targets[0], decoded.Targets[0])
210211
require.Equal(t, original.Targets[1], decoded.Targets[1])
212+
require.Equal(t, original.ResultDestination, decoded.ResultDestination)
211213
}
212214

213215
func TestSDK_Geolocation_State_GeolocationUser_EmptyTargets(t *testing.T) {
@@ -226,8 +228,9 @@ func TestSDK_Geolocation_State_GeolocationUser_EmptyTargets(t *testing.T) {
226228
LastDeductionDzEpoch: 0,
227229
},
228230
},
229-
Status: geolocation.GeolocationUserStatusSuspended,
230-
Targets: []geolocation.GeolocationTarget{},
231+
Status: geolocation.GeolocationUserStatusSuspended,
232+
Targets: []geolocation.GeolocationTarget{},
233+
ResultDestination: "",
231234
}
232235

233236
var buf bytes.Buffer
@@ -242,6 +245,50 @@ func TestSDK_Geolocation_State_GeolocationUser_EmptyTargets(t *testing.T) {
242245
require.Equal(t, geolocation.GeolocationPaymentStatusDelinquent, decoded.PaymentStatus)
243246
}
244247

248+
func TestSDK_Geolocation_State_GeolocationUser_BackwardCompat_NoResultDestination(t *testing.T) {
249+
t.Parallel()
250+
251+
original := &geolocation.GeolocationUser{
252+
AccountType: geolocation.AccountTypeGeolocationUser,
253+
Owner: solana.NewWallet().PublicKey(),
254+
Code: "old-user",
255+
TokenAccount: solana.NewWallet().PublicKey(),
256+
PaymentStatus: geolocation.GeolocationPaymentStatusPaid,
257+
Billing: geolocation.GeolocationBillingConfig{
258+
Variant: geolocation.BillingConfigFlatPerEpoch,
259+
FlatPerEpoch: geolocation.FlatPerEpochConfig{
260+
Rate: 1000,
261+
LastDeductionDzEpoch: 42,
262+
},
263+
},
264+
Status: geolocation.GeolocationUserStatusActivated,
265+
Targets: []geolocation.GeolocationTarget{
266+
{
267+
TargetType: geolocation.GeoLocationTargetTypeOutbound,
268+
IPAddress: [4]uint8{8, 8, 8, 8},
269+
LocationOffsetPort: 8923,
270+
TargetPK: solana.PublicKey{},
271+
GeoProbePK: solana.NewWallet().PublicKey(),
272+
},
273+
},
274+
ResultDestination: "",
275+
}
276+
277+
var buf bytes.Buffer
278+
require.NoError(t, original.Serialize(&buf))
279+
280+
// Truncate the trailing 4 bytes (empty Borsh string = 4-byte length prefix)
281+
// to simulate old data without the result_destination field.
282+
data := buf.Bytes()[:buf.Len()-4]
283+
284+
var decoded geolocation.GeolocationUser
285+
require.NoError(t, decoded.Deserialize(data))
286+
287+
require.Equal(t, original.Owner, decoded.Owner)
288+
require.Equal(t, original.Targets[0], decoded.Targets[0])
289+
require.Equal(t, "", decoded.ResultDestination)
290+
}
291+
245292
func TestSDK_Geolocation_State_GeolocationTarget_RoundTrip(t *testing.T) {
246293
t.Parallel()
247294

smartcontract/cli/src/geoclicommand.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use doublezero_sdk::{
1111
add_target::AddTargetCommand, create::CreateGeolocationUserCommand,
1212
delete::DeleteGeolocationUserCommand, get::GetGeolocationUserCommand,
1313
list::ListGeolocationUserCommand, remove_target::RemoveTargetCommand,
14+
set_result_destination::SetResultDestinationCommand,
1415
update_payment_status::UpdatePaymentStatusCommand,
1516
},
1617
programconfig::init::InitProgramConfigCommand,
@@ -53,6 +54,7 @@ pub trait GeoCliCommand {
5354
) -> eyre::Result<HashMap<Pubkey, GeolocationUser>>;
5455
fn add_target(&self, cmd: AddTargetCommand) -> eyre::Result<Signature>;
5556
fn remove_target(&self, cmd: RemoveTargetCommand) -> eyre::Result<Signature>;
57+
fn set_result_destination(&self, cmd: SetResultDestinationCommand) -> eyre::Result<Signature>;
5658
fn update_payment_status(&self, cmd: UpdatePaymentStatusCommand) -> eyre::Result<Signature>;
5759

5860
fn resolve_exchange_pk(&self, pubkey_or_code: String) -> eyre::Result<Pubkey>;
@@ -155,6 +157,10 @@ impl GeoCliCommand for GeoCliCommandImpl<'_> {
155157
cmd.execute(self.client)
156158
}
157159

160+
fn set_result_destination(&self, cmd: SetResultDestinationCommand) -> eyre::Result<Signature> {
161+
cmd.execute(self.client)
162+
}
163+
158164
fn update_payment_status(&self, cmd: UpdatePaymentStatusCommand) -> eyre::Result<Signature> {
159165
cmd.execute(self.client)
160166
}

smartcontract/cli/src/geolocation/user/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pub mod delete;
44
pub mod get;
55
pub mod list;
66
pub mod remove_target;
7+
pub mod set_result_destination;
78
pub mod update_payment_status;

0 commit comments

Comments
 (0)