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
82 changes: 80 additions & 2 deletions core/contractsapi/role_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package contractsapi

import (
"context"

kwilClientType "github.com/kwilteam/kwil-db/core/client/types"
"github.com/kwilteam/kwil-db/core/gatewayclient"
kwiltypes "github.com/kwilteam/kwil-db/core/types"
"github.com/pkg/errors"
"github.com/trufnetwork/sdk-go/core/types"
"github.com/trufnetwork/sdk-go/core/util"
)

// RoleManagement provides methods to interact with the role-based access control system.
Expand Down Expand Up @@ -35,14 +37,90 @@ func LoadRoleManagementActions(options NewRoleManagementOptions) (types.IRoleMan
// It calls the `grant_roles` SQL action.
func (r *RoleManagement) GrantRole(ctx context.Context, input types.GrantRoleInput, opts ...kwilClientType.TxOpt) (kwiltypes.Hash, error) {
return r._client.Execute(ctx, "", "grant_roles", [][]any{
{input.Owner, input.RoleName, input.Wallets}, // Use input.Wallets directly
{input.Owner, input.RoleName, util.EthereumAddressesToStrings(input.Wallets)},
}, opts...)
}

// RevokeRole revokes a role from multiple wallets.
// It calls the `revoke_roles` SQL action.
func (r *RoleManagement) RevokeRole(ctx context.Context, input types.RevokeRoleInput, opts ...kwilClientType.TxOpt) (kwiltypes.Hash, error) {
return r._client.Execute(ctx, "", "revoke_roles", [][]any{
{input.Owner, input.RoleName, input.Wallets}, // Use input.Wallets directly
{input.Owner, input.RoleName, util.EthereumAddressesToStrings(input.Wallets)},
}, opts...)
}

// AreMembersOf checks if the given wallets are members of a role.
// It calls the `are_members_of` SQL view action.
func (r *RoleManagement) AreMembersOf(ctx context.Context, input types.AreMembersOfInput) ([]types.RoleMembershipResult, error) {
// Perform view call
result, err := r._client.Call(ctx, "", "are_members_of", []any{
input.Owner,
input.RoleName,
util.EthereumAddressesToStrings(input.Wallets),
})
if err != nil {
return nil, errors.WithStack(err)
}
if result.Error != nil {
return nil, errors.New(*result.Error)
}

type membershipRaw struct {
Wallet string `json:"wallet"`
IsMember bool `json:"is_member"`
}
raw, err := DecodeCallResult[membershipRaw](result.QueryResult)
if err != nil {
return nil, errors.WithStack(err)
}
out := make([]types.RoleMembershipResult, len(raw))
for i, r := range raw {
addr, err := util.NewEthereumAddressFromString(r.Wallet)
if err != nil {
return nil, errors.WithStack(err)
}
out[i] = types.RoleMembershipResult{Wallet: addr, IsMember: r.IsMember}
}
return out, nil
}

// ListRoleMembers lists the members of a role with pagination.
// It calls the `list_role_members` SQL view action.
func (r *RoleManagement) ListRoleMembers(ctx context.Context, input types.ListRoleMembersInput) ([]types.RoleMember, error) {
// The SQL action enforces sensible defaults; pass provided limit/offset directly (can be zero).
result, err := r._client.Call(ctx, "", "list_role_members", []any{
input.Owner,
input.RoleName,
input.Limit,
input.Offset,
})
if err != nil {
return nil, errors.WithStack(err)
}
if result.Error != nil {
return nil, errors.New(*result.Error)
}

type memberRaw struct {
Wallet string `json:"wallet"`
GrantedAt int64 `json:"granted_at"`
GrantedBy string `json:"granted_by"`
}
raws, err := DecodeCallResult[memberRaw](result.QueryResult)
if err != nil {
return nil, errors.WithStack(err)
}
out := make([]types.RoleMember, len(raws))
for i, m := range raws {
wallet, err := util.NewEthereumAddressFromString(m.Wallet)
if err != nil {
return nil, errors.WithStack(err)
}
out[i] = types.RoleMember{
Wallet: wallet,
GrantedAt: m.GrantedAt,
GrantedBy: m.GrantedBy,
}
}
return out, nil
}
40 changes: 37 additions & 3 deletions core/types/role_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,53 @@ package types
import (
"context"

// Using the node/types for Kwil's Hash type for consistency
kwilClientType "github.com/kwilteam/kwil-db/core/client/types" // for TxOpt
"github.com/kwilteam/kwil-db/node/types"
"github.com/trufnetwork/sdk-go/core/util"
)

// GrantRoleInput represents the input for granting a role to multiple wallets.
type GrantRoleInput struct {
Owner string
RoleName string
Wallets []string // Changed to array of strings
Wallets []util.EthereumAddress
}

// RevokeRoleInput represents the input for revoking a role from multiple wallets.
type RevokeRoleInput struct {
Owner string
RoleName string
Wallets []string // Changed to array of strings
Wallets []util.EthereumAddress
}

// AreMembersOfInput represents the input for checking if wallets are members of a role.
// It supports batch checking of multiple wallets in a single call.
type AreMembersOfInput struct {
Owner string
RoleName string
Wallets []util.EthereumAddress
}

// ListRoleMembersInput represents the input for listing members of a role with pagination.
type ListRoleMembersInput struct {
Owner string
RoleName string
Limit int
Offset int
}

// RoleMembershipResult represents the result of an AreMembersOf query.
// Wallet holds the queried wallet address, and IsMember indicates membership status.
type RoleMembershipResult struct {
Wallet util.EthereumAddress `json:"wallet"`
IsMember bool `json:"is_member"`
}

// RoleMember represents a member of a role returned by list_role_members.
type RoleMember struct {
Wallet util.EthereumAddress `json:"wallet"`
GrantedAt int64 `json:"granted_at"`
GrantedBy string `json:"granted_by"`
}

// IRoleManagement defines the interface for interacting with the role-based access control system.
Expand All @@ -28,4 +58,8 @@ type IRoleManagement interface {
GrantRole(ctx context.Context, input GrantRoleInput, opts ...kwilClientType.TxOpt) (types.Hash, error)
// RevokeRole revokes a specified role from multiple wallets.
RevokeRole(ctx context.Context, input RevokeRoleInput, opts ...kwilClientType.TxOpt) (types.Hash, error)
// AreMembersOf checks if one or more wallets are members of a specified role.
AreMembersOf(ctx context.Context, input AreMembersOfInput) ([]RoleMembershipResult, error)
// ListRoleMembers returns the members of a role with pagination.
ListRoleMembers(ctx context.Context, input ListRoleMembersInput) ([]RoleMember, error)
}
10 changes: 10 additions & 0 deletions core/util/address_slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package util

// EthereumAddressesToStrings converts a slice of EthereumAddress to their lowercase hex string representation.
func EthereumAddressesToStrings(addrs []EthereumAddress) []string {
strs := make([]string, len(addrs))
for i, a := range addrs {
strs[i] = a.Address()
}
return strs
}
3 changes: 2 additions & 1 deletion docs/stream-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ The SDK provides an API for advanced partners and internal use to programmatical
- `Client.LoadRoleManagementActions()`: Loads the `IRoleManagement` interface.
- `IRoleManagement.GrantRole(ctx, GrantRoleInput, ...TxOpt)`: Grants a role to one or more wallets.
- `IRoleManagement.RevokeRole(ctx, RevokeRoleInput, ...TxOpt)`: Revokes a role from one or more wallets.
- `IRoleManagement.IsMemberOf(ctx, IsMemberOfInput)`: Checks if one or more wallets are members of a specific role.
- `IRoleManagement.AreMembersOf(ctx, AreMembersOfInput)`: Checks if one or more wallets are members of a specific role.
- `IRoleManagement.ListRoleMembers(ctx, ListRoleMembersInput)`: Lists current members of a role with optional pagination.

**Note:** For general stream creation, users should typically contact the TRUF.NETWORK team directly rather than attempting to manage the `system:network_writer` role themselves. The `system:network_writer` role is managed by `system:network_writers_manager`.

Expand Down
18 changes: 12 additions & 6 deletions tests/integration/batch_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ import (
"testing"
"time"

kwilcrypto "github.com/kwilteam/kwil-db/core/crypto"
"github.com/kwilteam/kwil-db/core/crypto/auth"
kwiltypes "github.com/kwilteam/kwil-db/core/types"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufnetwork/sdk-go/core/tnclient"
"github.com/trufnetwork/sdk-go/core/types"
"github.com/trufnetwork/sdk-go/core/util"
)

func TestBatchOperations(t *testing.T) {
ctx := context.Background()
fixture := NewServerFixture(t)
err := fixture.Setup()
t.Cleanup(func() {
fixture.Teardown()
})
require.NoError(t, err, "Failed to setup server fixture")

tnClient := fixture.Client()
require.NotNil(t, tnClient, "Client from fixture should not be nil")
deployerWallet, err := kwilcrypto.Secp256k1PrivateKeyFromHex(AnonWalletPK)
require.NoError(t, err, "failed to parse anon wallet private key")
tnClient, err := tnclient.NewClient(ctx, TestKwilProvider, tnclient.WithSigner(auth.GetUserSigner(deployerWallet)))
require.NoError(t, err, "failed to create client")

ctx := context.Background()
authorizeWalletToDeployStreams(t, ctx, fixture, deployerWallet)

t.Run("TestSequentialSmallBatches", func(t *testing.T) {
streamId := util.GenerateStreamId("test-sequential-small")
Expand Down Expand Up @@ -54,7 +60,7 @@ func TestBatchOperations(t *testing.T) {
txHashes := make([]kwiltypes.Hash, 0, numBatches)
startTime := time.Now()

for batch := 0; batch < numBatches; batch++ {
for batch := 0; batch <= numBatches; batch++ {
records := make([]types.InsertRecordInput, recordsPerBatch)
for i := 0; i < recordsPerBatch; i++ {
records[i] = types.InsertRecordInput{
Expand Down Expand Up @@ -126,7 +132,7 @@ func TestBatchOperations(t *testing.T) {
txHashes := make([]kwiltypes.Hash, 0, numBatches)
startTime := time.Now()

for batch := 0; batch < numBatches; batch++ {
for batch := 0; batch <= numBatches; batch++ {
records := make([]types.InsertRecordInput, recordsPerBatch)
for i := 0; i < recordsPerBatch; i++ {
records[i] = types.InsertRecordInput{
Expand Down Expand Up @@ -197,7 +203,7 @@ func TestBatchOperations(t *testing.T) {
txHashes := make([]kwiltypes.Hash, 0, numRecords)
startTime := time.Now()

for i := 0; i < numRecords; i++ {
for i := 0; i <= numRecords; i++ {
records := []types.InsertRecordInput{
{
DataProvider: streamLocator.DataProvider.Address(),
Expand Down
15 changes: 10 additions & 5 deletions tests/integration/batch_stream_creation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (
"testing"
"time"

// kwilcrypto "github.com/kwilteam/kwil-db/core/crypto" // Will be removed if not used elsewhere
// "github.com/kwilteam/kwil-db/core/crypto/auth" // Will be removed if not used elsewhere
kwilcrypto "github.com/kwilteam/kwil-db/core/crypto"
"github.com/kwilteam/kwil-db/core/crypto/auth"
kwiltypes "github.com/kwilteam/kwil-db/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufnetwork/sdk-go/core/tnclient"
"github.com/trufnetwork/sdk-go/core/types"
"github.com/trufnetwork/sdk-go/core/util"
)
Expand All @@ -24,10 +25,14 @@ func TestBatchDeployAndExistenceOperations(t *testing.T) {
})
require.NoError(t, err, "Failed to setup server fixture")

tnClient := fixture.Client()
require.NotNil(t, tnClient, "Client from fixture should not be nil")

ctx := context.Background()
deployerWallet, err := kwilcrypto.Secp256k1PrivateKeyFromHex(AnonWalletPK)
require.NoError(t, err, "failed to parse anon wallet private key")
tnClient, err := tnclient.NewClient(ctx, TestKwilProvider, tnclient.WithSigner(auth.GetUserSigner(deployerWallet)))
require.NoError(t, err, "failed to create client")

authorizeWalletToDeployStreams(t, ctx, fixture, deployerWallet)

signerAddress := tnClient.Address()

// =========================================================================
Expand Down
13 changes: 10 additions & 3 deletions tests/integration/composed_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"context"
"testing"

kwilcrypto "github.com/kwilteam/kwil-db/core/crypto"
"github.com/kwilteam/kwil-db/core/crypto/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufnetwork/sdk-go/core/tnclient"
"github.com/trufnetwork/sdk-go/core/types"
"github.com/trufnetwork/sdk-go/core/util"
)
Expand All @@ -17,17 +20,21 @@ import (
// TestComposedStream demonstrates the process of deploying, initializing, and querying
// a composed stream that aggregates data from multiple primitive streams in the TN using the TN SDK.
func TestComposedActions(t *testing.T) {
ctx := context.Background()
fixture := NewServerFixture(t)
err := fixture.Setup()
t.Cleanup(func() {
fixture.Teardown()
})
require.NoError(t, err, "Failed to setup server fixture")

tnClient := fixture.Client()
require.NotNil(t, tnClient, "Client from fixture should not be nil")
deployerWallet, err := kwilcrypto.Secp256k1PrivateKeyFromHex(AnonWalletPK)
require.NoError(t, err, "failed to parse anon wallet private key")
tnClient, err := tnclient.NewClient(ctx, TestKwilProvider, tnclient.WithSigner(auth.GetUserSigner(deployerWallet)))
require.NoError(t, err, "failed to create client")

authorizeWalletToDeployStreams(t, ctx, fixture, deployerWallet)

ctx := context.Background()
signerAddress := tnClient.Address()

// Generate a unique stream ID and locator for the composed stream and its child streams
Expand Down
14 changes: 10 additions & 4 deletions tests/integration/deploy_composed_streams_with_taxonomy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ import (
"time"

"github.com/golang-sql/civil"
kwilcrypto "github.com/kwilteam/kwil-db/core/crypto"
"github.com/kwilteam/kwil-db/core/crypto/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufnetwork/sdk-go/core/tnclient"
"github.com/trufnetwork/sdk-go/core/types"
"github.com/trufnetwork/sdk-go/core/util"
)

func TestDeployComposedStreamsWithTaxonomy(t *testing.T) {
ctx := context.Background()
fixture := NewServerFixture(t)
err := fixture.Setup()
t.Cleanup(func() {
fixture.Teardown()
})
require.NoError(t, err, "Failed to setup server fixture")

tnClient := fixture.Client()
require.NotNil(t, tnClient, "Client from fixture should not be nil")
deployerWallet, err := kwilcrypto.Secp256k1PrivateKeyFromHex(AnonWalletPK)
require.NoError(t, err, "failed to parse anon wallet private key")
tnClient, err := tnclient.NewClient(ctx, TestKwilProvider, tnclient.WithSigner(auth.GetUserSigner(deployerWallet)))
require.NoError(t, err, "failed to create client")

ctx := context.Background()
authorizeWalletToDeployStreams(t, ctx, fixture, deployerWallet)

// Generate unique stream IDs and locators
primitiveStreamId := util.GenerateStreamId("test-primitive-stream-one")
Expand Down Expand Up @@ -68,7 +74,7 @@ func TestDeployComposedStreamsWithTaxonomy(t *testing.T) {
waitTxToBeMinedWithSuccess(t, ctx, tnClient, deployTxHash)

// List all streams
streams, err := tnClient.ListStreams(ctx, types.ListStreamsInput{ BlockHeight: 0 })
streams, err := tnClient.ListStreams(ctx, types.ListStreamsInput{BlockHeight: 0})
assertNoErrorOrFail(t, err, "Failed to list all streams")

//Check that only the primitive and composed streams are listed
Expand Down
Loading