Skip to content

Commit 9d4f46e

Browse files
authored
feat: role-management bindings & permission docs (#56)
## What's inside - Go & Python helpers for `GrantRole`, `RevokeRole`, `AreMembersOf`, `ListRoleMembers`. - `TNClient` exposes matching methods + new typed dicts. - Bump `sdk-go` to v0.3.2; add `eth_account` for tests. - Integration suite: role grant/revoke happy-path + permission gate; fixtures auto-whitelist writer role. - Docs: short call-outs that stream deployment needs `system:network_writer`; no self-grant guidance. ## Issue - fix #51 - fix #52 ## Validation `pytest` green;
1 parent 47753af commit 9d4f46e

File tree

20 files changed

+712
-40
lines changed

20 files changed

+712
-40
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ client.deploy_stream(composite_stream_id, STREAM_TYPE_COMPOSED)
129129

130130
For the full example, refer to the [Complex Example README](./examples/complex_example/README.md).
131131

132+
## Role-based Permissions
133+
134+
> **Heads-up:** Deploying streams requires the `system:network_writer` role; reading public streams is permissionless.
135+
136+
Don't have the role? Contact the TRUF.NETWORK team to get whitelisted.
137+
138+
Check your role status using the **Role Management** APIs in the [API Reference](./docs/api-reference.md#role-management).
139+
132140
## Stream Creation and Management
133141

134142
### Stream Types

bindings/bindings.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,10 @@ func convertToString(val any) string {
832832
return v.String()
833833
case civil.Date:
834834
return v.String()
835+
case util.EthereumAddress:
836+
return v.Address()
837+
case *util.EthereumAddress:
838+
return v.Address()
835839
case fmt.Stringer:
836840
return v.String()
837841
default:
@@ -928,3 +932,123 @@ func BatchFilterStreamsByExistence(client *tnclient.Client, locators []types.Str
928932
}
929933
return output, nil
930934
}
935+
936+
// helper to convert slice of hex wallet strings to []util.EthereumAddress
937+
func strSliceToEthAddrs(wallets []string) ([]util.EthereumAddress, error) {
938+
out := make([]util.EthereumAddress, len(wallets))
939+
for i, w := range wallets {
940+
addr, err := util.NewEthereumAddressFromString(w)
941+
if err != nil {
942+
return nil, err
943+
}
944+
out[i] = addr
945+
}
946+
return out, nil
947+
}
948+
949+
// GrantRole grants a role to multiple wallets.
950+
func GrantRole(client *tnclient.Client, owner string, roleName string, wallets []string) (string, error) {
951+
ctx := context.Background()
952+
953+
roleMgmt, err := client.LoadRoleManagementActions()
954+
if err != nil {
955+
return "", errors.Wrap(err, "error loading role management actions")
956+
}
957+
958+
addrs, err := strSliceToEthAddrs(wallets)
959+
if err != nil {
960+
return "", errors.Wrap(err, "invalid wallet address")
961+
}
962+
963+
input := types.GrantRoleInput{
964+
Owner: owner,
965+
RoleName: roleName,
966+
Wallets: addrs,
967+
}
968+
969+
txHash, err := roleMgmt.GrantRole(ctx, input)
970+
if err != nil {
971+
return "", errors.Wrap(err, "error granting role")
972+
}
973+
return txHash.String(), nil
974+
}
975+
976+
// RevokeRole revokes a role from multiple wallets.
977+
func RevokeRole(client *tnclient.Client, owner string, roleName string, wallets []string) (string, error) {
978+
ctx := context.Background()
979+
980+
roleMgmt, err := client.LoadRoleManagementActions()
981+
if err != nil {
982+
return "", errors.Wrap(err, "error loading role management actions")
983+
}
984+
985+
addrs, err := strSliceToEthAddrs(wallets)
986+
if err != nil {
987+
return "", errors.Wrap(err, "invalid wallet address")
988+
}
989+
990+
input := types.RevokeRoleInput{
991+
Owner: owner,
992+
RoleName: roleName,
993+
Wallets: addrs,
994+
}
995+
996+
txHash, err := roleMgmt.RevokeRole(ctx, input)
997+
if err != nil {
998+
return "", errors.Wrap(err, "error revoking role")
999+
}
1000+
return txHash.String(), nil
1001+
}
1002+
1003+
// AreMembersOf checks if a list of wallets are members of a specific role.
1004+
func AreMembersOf(client *tnclient.Client, owner string, roleName string, wallets []string) ([]map[string]string, error) {
1005+
ctx := context.Background()
1006+
1007+
roleMgmt, err := client.LoadRoleManagementActions()
1008+
if err != nil {
1009+
return nil, errors.Wrap(err, "error loading role management actions")
1010+
}
1011+
1012+
addrs, err := strSliceToEthAddrs(wallets)
1013+
if err != nil {
1014+
return nil, errors.Wrap(err, "invalid wallet address")
1015+
}
1016+
1017+
input := types.AreMembersOfInput{
1018+
Owner: owner,
1019+
RoleName: roleName,
1020+
Wallets: addrs,
1021+
}
1022+
1023+
results, err := roleMgmt.AreMembersOf(ctx, input)
1024+
if err != nil {
1025+
return nil, errors.Wrap(err, "error checking role members")
1026+
}
1027+
1028+
return recordsToMapSlice(results), nil
1029+
}
1030+
1031+
// ListRoleMembers lists the current members of a role with optional pagination.
1032+
// It returns a slice of map[string]string where each map contains `Wallet`, `GrantedAt`, and `GrantedBy`.
1033+
func ListRoleMembers(client *tnclient.Client, owner string, roleName string, limit int, offset int) ([]map[string]string, error) {
1034+
ctx := context.Background()
1035+
1036+
roleMgmt, err := client.LoadRoleManagementActions()
1037+
if err != nil {
1038+
return nil, errors.Wrap(err, "error loading role management actions")
1039+
}
1040+
1041+
input := types.ListRoleMembersInput{
1042+
Owner: owner,
1043+
RoleName: roleName,
1044+
Limit: limit,
1045+
Offset: offset,
1046+
}
1047+
1048+
results, err := roleMgmt.ListRoleMembers(ctx, input)
1049+
if err != nil {
1050+
return nil, errors.Wrap(err, "error listing role members")
1051+
}
1052+
1053+
return recordsToMapSlice(results), nil
1054+
}

docs/api-reference.md

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ market_index_stream_id = generate_stream_id('market_index')
4343

4444
## Stream Deployment
4545

46+
> **Note: Stream Deployment Permissions**
47+
>
48+
> Deploying new streams on the TRUF.NETWORK requires the `system:network_writer` role.
49+
>
50+
> If you're interested in deploying streams, please contact the TRUF.NETWORK team for assistance.
51+
4652
### `client.deploy_stream(stream_id: str, stream_type: str) -> str`
4753
Deploys a new stream to the TRUF.NETWORK.
4854

@@ -166,8 +172,90 @@ tx_hash = client.set_taxonomy(
166172
)
167173
```
168174

175+
## Role Management
176+
177+
> Only wallets with manager privileges (e.g. `system:network_writers_manager`) can grant or revoke roles. Regular users should request access from the TRUF.NETWORK team.
178+
179+
### `client.grant_role(owner: str, role_name: str, wallets: List[str]) -> str`
180+
Grants a specified role to a list of wallet addresses.
181+
182+
#### Parameters
183+
- `owner: str` - The owner of the role (e.g., 'system' or an Ethereum address).
184+
- `role_name: str` - The name of the role to grant.
185+
- `wallets: List[str]` - A list of wallet addresses to grant the role to.
186+
187+
#### Returns
188+
- `str` - Transaction hash of the role grant operation.
189+
190+
#### Example
191+
```python
192+
# Grant the system:network_writer role to a specific wallet
193+
tx_hash = client.grant_role(
194+
"system",
195+
"network_writer",
196+
["0xAbC...123"]
197+
)
198+
```
199+
200+
### `client.revoke_role(owner: str, role_name: str, wallets: List[str]) -> str`
201+
Revokes a specified role from a list of wallet addresses.
202+
203+
#### Parameters
204+
- `owner: str` - The owner of the role.
205+
- `role_name: str` - The name of the role to revoke.
206+
- `wallets: List[str]` - A list of wallet addresses from which to revoke the role.
207+
208+
#### Returns
209+
- `str` - Transaction hash of the role revocation operation.
210+
211+
#### Example
212+
```python
213+
tx_hash = client.revoke_role(
214+
"system",
215+
"network_writer",
216+
["0xAbC...123"]
217+
)
218+
```
219+
220+
### `client.are_members_of(owner: str, role_name: str, wallets: List[str]) -> List[Dict]`
221+
Checks if a list of wallets are members of a specific role.
222+
223+
#### Parameters
224+
- `owner: str` - The owner of the role to check against.
225+
- `role_name: str` - The name of the role.
226+
- `wallets: List[str]` - A list of wallet addresses to check.
227+
228+
#### Returns
229+
- `List[Dict]` - A list of objects, each containing:
230+
- `wallet: str` - The wallet address checked.
231+
- `is_member: bool` - True if the wallet is a member, false otherwise.
232+
233+
#### Example
234+
```python
235+
wallets_to_check = ["0xAbC...123", "0xDeF...456"]
236+
membership_status = client.are_members_of(
237+
"system",
238+
"network_writer",
239+
wallets_to_check
240+
)
241+
# Example output:
242+
# [
243+
# {'wallet': '0xabc...123', 'is_member': True},
244+
# {'wallet': '0xdef...456', 'is_member': False}
245+
# ]
246+
```
247+
169248
## Visibility and Permissions
170249

250+
### System vs. User Roles
251+
252+
| Role Namespace | Example | Who can create/manage | Typical purpose |
253+
|----------------|---------|-----------------------|-----------------|
254+
| `system:` | `system:network_writer` | Core protocol maintainers | Gate network-wide operations (e.g. create streams) |
255+
| `<wallet>:` | `0x1234…abcd:pro_subscribers` | The wallet prefix (owner) | Business-specific read/write groups |
256+
257+
> Tip: You can list all roles owned by a wallet with `client.list_role_members(owner, role_name)`.
258+
171259
### `client.set_read_visibility(stream_id: str, visibility: str) -> str`
172260
Controls stream read access.
173261

@@ -206,8 +294,8 @@ client.wait_for_tx(tx_hash)
206294
```
207295

208296
## Performance Recommendations
209-
- Use batch record insertions
210-
- Handle errors with specific exception handling
297+
- Use `batch_insert_records` for multiple records to one or more streams to reduce network overhead and transaction costs.
298+
- Handle errors with specific exception handling to build robust applications.
211299

212300
## SDK Compatibility
213301
- Minimum Python Version: 3.8

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ Date: 2024-01-16, Value: 106.50
7373

7474
## Notes
7575

76+
> **Permission note:** This script only *reads* public streams, so no roles are required. Deploying streams needs the `system:network_writer` role; contact the TRUF.NETWORK team to obtain it.
77+
7678
- Always handle private keys securely
7779
- Be mindful of rate limits
7880
- This is a basic example; adapt for your specific use case

examples/complex_example/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This example demonstrates the full lifecycle of stream management in the TRUF.NE
1717
- Python 3.8+
1818
- `venv` module (usually comes with Python standard library)
1919
- A valid private key for the TRUF.NETWORK gateway
20+
- Your wallet must **hold `system:network_writer`** or the deployment steps will fail. Contact the TRUF.NETWORK team if you need access.
2021

2122
## Setup
2223

examples/complex_example/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def create_example_streams(client):
1515
5. Inserting records into primitive streams
1616
6. Reading records from streams
1717
7. Cleaning up (destroying) streams
18+
19+
⚠️ **Permissions:** Running this example requires the calling wallet to
20+
hold the `system:network_writer` role. Contact the TRUF.NETWORK team to
21+
obtain the role before executing the script.
1822
"""
1923
# Generate unique stream IDs
2024
market_stream_id = generate_stream_id("market_performance")

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9
99
github.com/kwilteam/kwil-db/core v0.4.2-0.20250506000241-da9d3ddea45e
1010
github.com/pkg/errors v0.9.1
11-
github.com/trufnetwork/sdk-go v0.3.1-0.20250605170548-90336b5849e0
11+
github.com/trufnetwork/sdk-go v0.3.2-0.20250611160829-e760d3bd55e1
1212
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463
1313
)
1414

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
4444
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4545
github.com/trufnetwork/sdk-go v0.3.1-0.20250605170548-90336b5849e0 h1:POu08dKYKli/SALB1btYO1WhqFEZNGhi29yIMb5d2NA=
4646
github.com/trufnetwork/sdk-go v0.3.1-0.20250605170548-90336b5849e0/go.mod h1:vcSgIR+ZRpkUJZ3DWaQVj0YSaPYkfcCPLd332Oo6RUM=
47+
github.com/trufnetwork/sdk-go v0.3.2-0.20250610130152-da8b60294d3f h1:NlLw5UykCxuxvPYgI/XTD1RintMDSfLZP5kUyp8KlMg=
48+
github.com/trufnetwork/sdk-go v0.3.2-0.20250610130152-da8b60294d3f/go.mod h1:vcSgIR+ZRpkUJZ3DWaQVj0YSaPYkfcCPLd332Oo6RUM=
49+
github.com/trufnetwork/sdk-go v0.3.2-0.20250611160829-e760d3bd55e1 h1:t1gmok1YGKI8WxQ3s/igQ7reRAmKQzUczx0p7Qe8JII=
50+
github.com/trufnetwork/sdk-go v0.3.2-0.20250611160829-e760d3bd55e1/go.mod h1:vcSgIR+ZRpkUJZ3DWaQVj0YSaPYkfcCPLd332Oo6RUM=
4751
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
4852
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
4953
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ dev = [
2020
"pytest-mock>=3.14.0",
2121
"requests>=2.31.0",
2222
"pybindgen",
23-
"python-dotenv"
23+
"python-dotenv",
24+
"eth_account>=0.8.0"
2425
]
2526

2627
[project.urls]

0 commit comments

Comments
 (0)