Skip to content

feat: Add persistent shard and realm support to Client #1395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 4 additions & 1 deletion examples/construct_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func main() {
}

// Set network for customClient which uses the above custom network
customClient := hiero.ClientForNetwork(customNetwork)
customClient, err := hiero.ClientForNetworkV2(customNetwork)
if err != nil {
panic(fmt.Sprintf("%v : error creating client for network", err))
}
// Setting NetworkName for the CustomClient, is only needed if you need to validate ID checksums
customClient.SetLedgerID(*hiero.NewLedgerIDTestnet())

Expand Down
90 changes: 73 additions & 17 deletions sdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
networkUpdateContext context.Context
cancelNetworkUpdate context.CancelFunc
logger Logger
shard uint64
realm uint64
}

// TransactionSigner is a closure or function that defines how transactions will be signed
Expand All @@ -65,15 +67,15 @@

// ClientForMirrorNetwork constructs a client given a set of mirror network nodes.
func ClientForMirrorNetwork(mirrorNetwork []string) (*Client, error) {
return ClientForMirrorNetworkWithRealmAndShard(mirrorNetwork, 0, 0)
return ClientForMirrorNetworkWithShardAndRealm(mirrorNetwork, 0, 0)
}

// ClientForMirrorNetworkWithRealmAndShard constructs a client given a set of mirror network nodes and the realm/shard of the address book.
func ClientForMirrorNetworkWithRealmAndShard(mirrorNetwork []string, realm uint64, shard uint64) (*Client, error) {
// constructs a client given a set of mirror network nodes and the shard/realm of the address book.
func ClientForMirrorNetworkWithShardAndRealm(mirrorNetwork []string, shard uint64, realm uint64) (*Client, error) {
net := _NewNetwork()
client := _NewClient(net, mirrorNetwork, nil, true)
client := _NewClient(net, mirrorNetwork, nil, true, shard, realm)
addressbook, err := NewAddressBookQuery().
SetFileID(GetAddressBookFileIDFor(realm, shard)).
SetFileID(GetAddressBookFileIDFor(shard, realm)).
Execute(client)
if err != nil {
return nil, fmt.Errorf("failed to query address book: %v", err)
Expand All @@ -82,41 +84,78 @@
return client, nil
}

// Deprecated: Use ClientForMirrorNetworkWithShardAndRealm instead.
func ClientForMirrorNetworkWithRealmAndShard(mirrorNetwork []string, realm uint64, shard uint64) (*Client, error) {
return ClientForMirrorNetworkWithShardAndRealm(mirrorNetwork, shard, realm)

Check warning on line 89 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L88-L89

Added lines #L88 - L89 were not covered by tests
}

// ClientForNetwork constructs a client given a set of nodes.
// Deprecated: Use ClientForNetworkV2 instead.
func ClientForNetwork(network map[string]AccountID) *Client {
net := _NewNetwork()
client := _NewClient(net, []string{}, nil, true)
client := _NewClient(net, []string{}, nil, true, 0, 0)

Check warning on line 96 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L96

Added line #L96 was not covered by tests
_ = client.SetNetwork(network)
return client
}

// ClientForNetworkV2 constructs a client given a set of nodes.
func ClientForNetworkV2(network map[string]AccountID) (*Client, error) {
isValidNetwork := true
var shard uint64
var realm uint64

for _, accountID := range network {
if shard == 0 {
shard = accountID.Shard
}
if realm == 0 {
realm = accountID.Realm
}
if shard != accountID.Shard || realm != accountID.Realm {
isValidNetwork = false
}
}

if !isValidNetwork {
return nil, errors.New("network is not valid, all nodes must be in the same shard and realm")
}

net := _NewNetwork()
client := _NewClient(net, []string{}, nil, true, shard, realm)
err := client.SetNetwork(network)
if err != nil {
return nil, err
}

Check warning on line 128 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L127-L128

Added lines #L127 - L128 were not covered by tests
return client, nil
}

// ClientForMainnet returns a preconfigured client for use with the standard
// Hiero mainnet.
// Most users will want to set an _Operator account with .SetOperator so
// transactions can be automatically given TransactionIDs and signed.
func ClientForMainnet() *Client {
return _NewClient(*_NetworkForMainnet(mainnetNodes._ToMap()), mainnetMirror, NewLedgerIDMainnet(), true)
return _NewClient(*_NetworkForMainnet(mainnetNodes._ToMap()), mainnetMirror, NewLedgerIDMainnet(), true, 0, 0)
}

// ClientForTestnet returns a preconfigured client for use with the standard
// Hiero testnet.
// Most users will want to set an _Operator account with .SetOperator so
// transactions can be automatically given TransactionIDs and signed.
func ClientForTestnet() *Client {
return _NewClient(*_NetworkForTestnet(testnetNodes._ToMap()), testnetMirror, NewLedgerIDTestnet(), true)
return _NewClient(*_NetworkForTestnet(testnetNodes._ToMap()), testnetMirror, NewLedgerIDTestnet(), true, 0, 0)
}

// ClientForPreviewnet returns a preconfigured client for use with the standard
// Hiero previewnet.
// Most users will want to set an _Operator account with .SetOperator so
// transactions can be automatically given TransactionIDs and signed.
func ClientForPreviewnet() *Client {
return _NewClient(*_NetworkForPreviewnet(previewnetNodes._ToMap()), previewnetMirror, NewLedgerIDPreviewnet(), true)
return _NewClient(*_NetworkForPreviewnet(previewnetNodes._ToMap()), previewnetMirror, NewLedgerIDPreviewnet(), true, 0, 0)

Check warning on line 153 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L153

Added line #L153 was not covered by tests
}

// newClient takes in a map of _Node addresses to their respective IDS (_Network)
// and returns a Client instance which can be used to
func _NewClient(network _Network, mirrorNetwork []string, ledgerId *LedgerID, shouldScheduleNetworkUpdate bool) *Client {
func _NewClient(network _Network, mirrorNetwork []string, ledgerId *LedgerID, shouldScheduleNetworkUpdate bool, shard uint64, realm uint64) *Client {
ctx, cancel := context.WithCancel(context.Background())
logger := NewLogger("hiero-sdk-go", LogLevel(os.Getenv("HEDERA_SDK_GO_LOG_LEVEL")))
var defaultLogger Logger = logger
Expand All @@ -134,6 +173,8 @@
networkUpdateContext: ctx,
cancelNetworkUpdate: cancel,
logger: defaultLogger,
shard: shard,
realm: realm,
}

client.SetMirrorNetwork(mirrorNetwork)
Expand All @@ -153,7 +194,7 @@

func (client *Client) _UpdateAddressBook() {
addressbook, err := NewAddressBookQuery().
SetFileID(FileIDForAddressBook()).
SetFileID(GetAddressBookFileIDFor(client.shard, client.realm)).
Execute(client)
if err == nil && len(addressbook.NodeAddresses) > 0 {
client.SetNetworkFromAddressBook(addressbook)
Expand Down Expand Up @@ -203,7 +244,10 @@
network := make(map[string]AccountID)
network["127.0.0.1:50213"] = AccountID{Account: 3}
mirror := []string{"127.0.0.1:5600"}
client := ClientForNetwork(network)
client, err := ClientForNetworkV2(network)
if err != nil {
return nil, err
}

Check warning on line 250 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L247-L250

Added lines #L247 - L250 were not covered by tests
client.SetMirrorNetwork(mirror)
return client, nil
default:
Expand All @@ -220,6 +264,8 @@
type _ClientConfig struct {
Network interface{} `json:"network"`
MirrorNetwork interface{} `json:"mirrorNetwork"`
Shard uint64 `json:"shard"`
Realm uint64 `json:"realm"`
Operator *_ConfigOperator `json:"operator"`
}

Expand Down Expand Up @@ -293,20 +339,20 @@
return client, errors.New("mirrorNetwork is expected to be either string or an array of strings")
}
}
client = _NewClient(network, arr, nil, shouldScheduleNetworkUpdate)
client = _NewClient(network, arr, nil, shouldScheduleNetworkUpdate, clientConfig.Shard, clientConfig.Realm)
case string:
if len(mirror) > 0 {
switch mirror {
case string(NetworkNameMainnet):
client = _NewClient(network, mainnetMirror, NewLedgerIDMainnet(), shouldScheduleNetworkUpdate)
client = _NewClient(network, mainnetMirror, NewLedgerIDMainnet(), shouldScheduleNetworkUpdate, clientConfig.Shard, clientConfig.Realm)

Check warning on line 347 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L347

Added line #L347 was not covered by tests
case string(NetworkNameTestnet):
client = _NewClient(network, testnetMirror, NewLedgerIDTestnet(), shouldScheduleNetworkUpdate)
client = _NewClient(network, testnetMirror, NewLedgerIDTestnet(), shouldScheduleNetworkUpdate, clientConfig.Shard, clientConfig.Realm)
case string(NetworkNamePreviewnet):
client = _NewClient(network, previewnetMirror, NewLedgerIDPreviewnet(), shouldScheduleNetworkUpdate)
client = _NewClient(network, previewnetMirror, NewLedgerIDPreviewnet(), shouldScheduleNetworkUpdate, clientConfig.Shard, clientConfig.Realm)

Check warning on line 351 in sdk/client.go

View check run for this annotation

Codecov / codecov/patch

sdk/client.go#L351

Added line #L351 was not covered by tests
}
}
case nil:
client = _NewClient(network, []string{}, nil, true)
client = _NewClient(network, []string{}, nil, true, clientConfig.Shard, clientConfig.Realm)
default:
return client, errors.New("mirrorNetwork is expected to be a string, an array of strings or nil")
}
Expand Down Expand Up @@ -509,6 +555,16 @@
return client.mirrorNetwork._GetNetwork()
}

// GetShard returns the shard for the Client.
func (client *Client) GetShard() uint64 {
return client.shard
}

// GetRealm returns the realm for the Client.
func (client *Client) GetRealm() uint64 {
return client.realm
}

// SetTransportSecurity sets if transport security should be used to connect to consensus nodes.
// If transport security is enabled all connections to consensus nodes will use TLS, and
// the server's certificate hash will be compared to the hash stored in the NodeAddressBook
Expand Down
20 changes: 12 additions & 8 deletions sdk/client_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ func TestIntegrationClientCanExecuteSerializedTransactionFromAnotherClient(t *te
t.Parallel()
env := NewIntegrationTestEnv(t)
defer CloseIntegrationTestEnv(env, nil)
client2 := ClientForNetwork(env.Client.GetNetwork())
client2, err := ClientForNetworkV2(env.Client.GetNetwork())
require.NoError(t, err)
client2.SetOperator(env.OperatorID, env.OperatorKey)

tx, err := NewTransferTransaction().AddHbarTransfer(env.OperatorID, HbarFromTinybar(-1)).
Expand Down Expand Up @@ -51,7 +52,8 @@ func TestIntegrationClientCanFailGracefullyWhenDoesNotHaveNodeOfAnotherClient(t
address: {Account: 99},
}

client2 := ClientForNetwork(network)
client2, err := ClientForNetworkV2(network)
require.NoError(t, err)
client2.SetOperator(env.OperatorID, env.OperatorKey)

// Create a transaction with a node using original client
Expand All @@ -78,7 +80,7 @@ func DisabledTestIntegrationClientPingAllBadNetwork(t *testing.T) { // nolint
netwrk := _NewNetwork()
netwrk.SetNetwork(env.Client.GetNetwork())

tempClient := _NewClient(netwrk, env.Client.GetMirrorNetwork(), env.Client.GetLedgerID(), true)
tempClient := _NewClient(netwrk, env.Client.GetMirrorNetwork(), env.Client.GetLedgerID(), true, 0, 0)
tempClient.SetOperator(env.OperatorID, env.OperatorKey)

tempClient.SetMaxNodeAttempts(1)
Expand Down Expand Up @@ -133,7 +135,7 @@ func TestClientInitWithMirrorNetwork(t *testing.T) {
assert.Equal(t, mirrorNetworkString, mirrorNetwork[0])
assert.NotEmpty(t, client.GetNetwork())

client, err = ClientForMirrorNetworkWithRealmAndShard([]string{mirrorNetworkString}, 0, 0)
client, err = ClientForMirrorNetworkWithShardAndRealm([]string{mirrorNetworkString}, 0, 0)
require.NoError(t, err)

mirrorNetwork = client.GetMirrorNetwork()
Expand All @@ -142,20 +144,22 @@ func TestClientInitWithMirrorNetwork(t *testing.T) {
assert.NotEmpty(t, client.GetNetwork())
}

func TestClientForMirrorNetworkWithRealmAndShard(t *testing.T) {
func TestClientIntegrationForMirrorNetworkWithShardAndRealm(t *testing.T) {
t.Parallel()

mirrorNetworkString := "testnet.mirrornode.hedera.com:443"
client, err := ClientForMirrorNetworkWithRealmAndShard([]string{mirrorNetworkString}, 0, 0)
client, err := ClientForMirrorNetworkWithShardAndRealm([]string{mirrorNetworkString}, 0, 0)
require.NoError(t, err)
require.NotNil(t, client)

// TODO enable when we have non-zero realm and shard env
// client, err = ClientForMirrorNetworkWithRealmAndShard([]string{mirrorNetworkString}, 5, 3)
// client, err = ClientForMirrorNetworkWithShardAndRealm([]string{mirrorNetworkString}, 5, 3)
// require.NoError(t, err)
// require.NotNil(t, client)
// require.Equal(t, uint64(5), client.GetShard())
// require.Equal(t, uint64(3), client.GetRealm())

client, err = ClientForMirrorNetworkWithRealmAndShard([]string{}, 0, 0)
client, err = ClientForMirrorNetworkWithShardAndRealm([]string{}, 0, 0)
require.Nil(t, client)
assert.Contains(t, err.Error(), "failed to query address book: no healthy nodes")
}
44 changes: 44 additions & 0 deletions sdk/client_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func TestUnitClientFromConfig(t *testing.T) {
assert.NotNil(t, client)
assert.True(t, len(client.network.network) > 0)
assert.Nil(t, client.operator)
assert.Equal(t, uint64(3), client.GetShard())
assert.Equal(t, uint64(5), client.GetRealm())
}

func TestUnitClientFromConfigWithOperator(t *testing.T) {
Expand Down Expand Up @@ -321,3 +323,45 @@ func TestUnitClientClientFromConfigWithoutScheduleNetworkUpdate(t *testing.T) {
assert.True(t, len(client.network.network) > 0)
assert.Equal(t, time.Duration(0), client.GetNetworkUpdatePeriod())
}

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

network := _NewNetwork()
client := _NewClient(network, []string{}, NewLedgerIDTestnet(), true, 1, 2)
assert.Equal(t, uint64(1), client.GetShard())
assert.Equal(t, uint64(2), client.GetRealm())
}

func TestUnitClientForNetworkV2(t *testing.T) {
t.Parallel()
network := map[string]AccountID{
"127.0.0.1:50211": {Account: 3, Shard: 1, Realm: 2},
"127.0.0.1:50212": {Account: 4, Shard: 1, Realm: 2},
"127.0.0.1:50213": {Account: 5, Shard: 1, Realm: 2},
}
client, err := ClientForNetworkV2(network)
require.NoError(t, err)
assert.Equal(t, uint64(1), client.GetShard())
assert.Equal(t, uint64(2), client.GetRealm())

network = map[string]AccountID{
"127.0.0.1:50211": {Account: 3, Shard: 2, Realm: 2},
"127.0.0.1:50212": {Account: 4, Shard: 1, Realm: 2},
"127.0.0.1:50213": {Account: 5, Shard: 1, Realm: 2},
}

client, err = ClientForNetworkV2(network)
require.Error(t, err)
assert.Equal(t, err.Error(), "network is not valid, all nodes must be in the same shard and realm")

network = map[string]AccountID{
"127.0.0.1:50211": {Account: 3, Shard: 1, Realm: 1},
"127.0.0.1:50212": {Account: 4, Shard: 1, Realm: 2},
"127.0.0.1:50213": {Account: 5, Shard: 1, Realm: 2},
}

client, err = ClientForNetworkV2(network)
require.Error(t, err)
assert.Equal(t, err.Error(), "network is not valid, all nodes must be in the same shard and realm")
}
6 changes: 3 additions & 3 deletions sdk/file_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func FileIDForExchangeRate() FileID {
}

// GetAddressBookFileIDFor returns the public node address book FileID for the given realm and shard.
func GetAddressBookFileIDFor(realm uint64, shard uint64) FileID {
func GetAddressBookFileIDFor(shard uint64, realm uint64) FileID {
return FileID{
Shard: shard,
Realm: realm,
Expand All @@ -43,7 +43,7 @@ func GetAddressBookFileIDFor(realm uint64, shard uint64) FileID {
}

// GetFeeScheduleFileIDFor returns the fee schedule FileID for the given realm and shard.
func GetFeeScheduleFileIDFor(realm uint64, shard uint64) FileID {
func GetFeeScheduleFileIDFor(shard uint64, realm uint64) FileID {
return FileID{
Shard: shard,
Realm: realm,
Expand All @@ -52,7 +52,7 @@ func GetFeeScheduleFileIDFor(realm uint64, shard uint64) FileID {
}

// GetExchangeRatesFileIDFor returns the exchange rates FileID for the given realm and shard.
func GetExchangeRatesFileIDFor(realm uint64, shard uint64) FileID {
func GetExchangeRatesFileIDFor(shard uint64, realm uint64) FileID {
return FileID{
Shard: shard,
Realm: realm,
Expand Down
6 changes: 3 additions & 3 deletions sdk/file_id_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestUnitGetAddressBookFileIDFor(t *testing.T) {
assert.Equal(t, uint64(102), fileID.File)
assert.Equal(t, FileIDForAddressBook(), fileID)

fileID = GetAddressBookFileIDFor(5, 3)
fileID = GetAddressBookFileIDFor(3, 5)
assert.Equal(t, uint64(3), fileID.Shard)
assert.Equal(t, uint64(5), fileID.Realm)
assert.Equal(t, uint64(102), fileID.File)
Expand All @@ -86,7 +86,7 @@ func TestUnitGetFeeScheduleFileIDFor(t *testing.T) {
assert.Equal(t, uint64(111), fileID.File)
assert.Equal(t, FileIDForFeeSchedule(), fileID)

fileID = GetFeeScheduleFileIDFor(5, 3)
fileID = GetFeeScheduleFileIDFor(3, 5)
assert.Equal(t, uint64(3), fileID.Shard)
assert.Equal(t, uint64(5), fileID.Realm)
assert.Equal(t, uint64(111), fileID.File)
Expand All @@ -101,7 +101,7 @@ func TestUnitGetExchangeRatesFileIDFor(t *testing.T) {
assert.Equal(t, uint64(112), fileID.File)
assert.Equal(t, FileIDForExchangeRate(), fileID)

fileID = GetExchangeRatesFileIDFor(5, 3)
fileID = GetExchangeRatesFileIDFor(3, 5)
assert.Equal(t, uint64(3), fileID.Shard)
assert.Equal(t, uint64(5), fileID.Realm)
assert.Equal(t, uint64(112), fileID.File)
Expand Down
3 changes: 2 additions & 1 deletion sdk/node_create_transaction_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ func TestIntegrationCanExecuteNodeCreateTransaction(t *testing.T) {
// Set the network
network := make(map[string]AccountID)
network["localhost:50211"] = AccountID{Account: 3}
client := ClientForNetwork(network)
client, err := ClientForNetworkV2(network)
require.NoError(t, err)
mirror := []string{"localhost:5600"}
client.SetMirrorNetwork(mirror)

Expand Down
Loading
Loading