Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
538c347
feat: implement node rewards calculation and API endpoint
0oM4R Jun 12, 2025
3ed761e
test: add test case for uptime percentage with decimal value
0oM4R Jun 12, 2025
929bad7
refactor: replace error string with proper error variable for invalid…
0oM4R Jun 15, 2025
bdcd279
docs: mention that the duration in seconds
0oM4R Jun 19, 2025
1dcc431
feat: implement node uptime calculation based on report history
0oM4R Jun 19, 2025
66b2a66
feat: add uptime percentage to rewards struct
0oM4R Jun 19, 2025
0ea416e
docs: add swagger docs for the rewards endpoint
0oM4R Jun 22, 2025
f60aa13
feat: show uptime percentage in the response of no rewards
0oM4R Jun 22, 2025
38e8082
fix: rename reward endpoint to rewards
0oM4R Jun 22, 2025
eb4aeca
feat: truncate reward values to 3 decimal places
0oM4R Jun 22, 2025
b39f7f7
feat: add uptime percentage to reward calculation in tests
0oM4R Jun 23, 2025
41c2c64
refactor: rename constants and functions to follow Go naming conventions
0oM4R Jun 23, 2025
6c5993c
Fix tests: pass duration as time.duration
0oM4R Jun 23, 2025
d80cbe0
refactor: rename and split reward calculation functions for better ma…
0oM4R Jun 23, 2025
0c6094d
feat: improve error handling for node uptime reports and add test cases
0oM4R Jun 23, 2025
04e5a9a
refactor: enhance loops
0oM4R Jun 23, 2025
ceff122
refactor: extract reward calculation logic into separate functions an…
0oM4R Jun 23, 2025
5e9f390
test: add comprehensive test suite for rewards calculation and uptime…
0oM4R Jun 23, 2025
f538295
revert adding main.go
0oM4R Jun 23, 2025
81906f9
docs: simplify uptime percentage calculation documentation with conci…
0oM4R Jun 23, 2025
27c3fe8
feat: add node capacity rewards calculation endpoint and types
0oM4R Jun 23, 2025
b69397d
fix node tests in client
Eslam-Nawara Jun 24, 2025
395a4fc
WIP: add rewards tests in client
0oM4R Jun 24, 2025
55e7ea8
remove extra println
Eslam-Nawara Jun 24, 2025
c4f1136
tests: add more test cases for the rewards
0oM4R Jun 24, 2025
10c4a79
refactor: standardize reward field names and JSON tags in NodeCapacit…
0oM4R Jun 24, 2025
e5e481c
fix node tests in client
Eslam-Nawara Jun 24, 2025
2465abc
WIP: add rewards tests in client
0oM4R Jun 24, 2025
2b5f964
remove extra println
Eslam-Nawara Jun 24, 2025
02e4c78
tests: add more test cases for the rewards
0oM4R Jun 24, 2025
6646f65
refactor: standardize reward field names and JSON tags in NodeCapacit…
0oM4R Jun 24, 2025
56109d3
Merge branch 'development_rewards' of github.com:threefoldtech/tfgrid…
0oM4R Jun 30, 2025
d0ce507
revert: remove the test file
0oM4R Jun 30, 2025
ff3487f
fix: use errors.Is() for proper error comparison in uptime calculatio…
0oM4R Jun 30, 2025
b407b77
refactor: replace floating point delta comparisons with exact truncat…
0oM4R Jun 30, 2025
cdd305f
test: replace InDelta with Equal for reward distribution percentage c…
0oM4R Jun 30, 2025
6a71bc3
refactor: simplify test assertions using require and assert packages
0oM4R Jun 30, 2025
2ae9121
fix: handle nil response in node rewards request
0oM4R Jun 30, 2025
311f49d
refactor: simplify error handling in node rewards and uptime calculat…
0oM4R Jun 30, 2025
b41cddc
Merge branch 'development' of github.com:threefoldtech/tfgridv4-sdk-g…
0oM4R Jul 14, 2025
724b052
lint
0oM4R Jul 14, 2025
036a1b2
refactor: simplify bytes conversion using explicit constants instead …
0oM4R Jul 14, 2025
f094176
refactor: consolidate reward calculation into single function with up…
0oM4R Jul 14, 2025
acfb746
test: replace assert with require for consistent error checking in re…
0oM4R Jul 14, 2025
9d4fd14
refactor: simplify reward test assertions by comparing entire struct
0oM4R Jul 14, 2025
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
36 changes: 36 additions & 0 deletions node-registrar/client/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ func (c *RegistrarClient) GetNodeByTwinID(id uint64) (node Node, err error) {
return c.getNodeByTwinID(id)
}

// GetNodeCapacityRewards calculates the reward for a node based on its capacity and uptime.
func (c *RegistrarClient) GetNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) {
return c.getNodeCapacityRewards(nodeID)
}

// ListNodes lists registered nodes details using (nodeID, twinID, farmID).
func (c *RegistrarClient) ListNodes(opts NodeFilter) (nodes []Node, err error) {
return c.listNodesWithFilter(opts)
Expand Down Expand Up @@ -320,6 +325,37 @@ func (c *RegistrarClient) getNodeByTwinID(id uint64) (node Node, err error) {
return nodes[0], nil
}

func (c *RegistrarClient) getNodeCapacityRewards(nodeID uint64) (reward NodeCapacityReward, err error) {
url, err := url.JoinPath(c.baseURL, "nodes", fmt.Sprint(nodeID), "rewards")
if err != nil {
return reward, errors.Wrap(err, "failed to construct rewards url")
}

resp, err := c.httpClient.Get(url)
if err != nil {
return reward, errors.Wrap(err, "failed to send request to rewards endpoint")
}
if resp == nil {
return reward, errors.New("no response received")
}

defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return reward, ErrorNodeNotFound
}

if resp.StatusCode != http.StatusOK {
err = parseResponseError(resp.Body)
return reward, errors.Wrap(err, "failed to get node rewards")
}

if err = json.NewDecoder(resp.Body).Decode(&reward); err != nil {
return reward, errors.Wrap(err, "failed to decode rewards response")
}

return
}

// NodeFilter represents filtering options for listing nodes
type NodeFilter struct {
NodeID *uint64
Expand Down
98 changes: 91 additions & 7 deletions node-registrar/client/node_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/base64"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -20,6 +21,10 @@ func TestRegistarNode(t *testing.T) {
FarmID: farmID,
}

keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
Expand Down Expand Up @@ -73,10 +78,10 @@ func TestUpdateNode(t *testing.T) {
var count int
require := require.New(t)

// publicKey, privateKey, err := aliceKeys()
// require.NoError(err)
// account.PublicKey = base64.StdEncoding.EncodeToString(publicKey)
//
keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
Expand Down Expand Up @@ -118,9 +123,9 @@ func TestGetNode(t *testing.T) {
var count int
require := require.New(t)

// publicKey, privateKey, err := aliceKeys()
// require.NoError(err)
// account.PublicKey = base64.StdEncoding.EncodeToString(publicKey)
keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
Expand All @@ -139,30 +144,109 @@ func TestGetNode(t *testing.T) {
require.NoError(err)

t.Run("test get node status not found", func(t *testing.T) {
count = 0
request = getNodeWithIDStatusNotFound
_, err = c.GetNode(nodeID)
require.Error(err)
})

t.Run("test get node, status ok", func(t *testing.T) {
count = 0
request = getNodeWithIDStatusOK
result, err := c.GetNode(nodeID)
require.NoError(err)
require.Equal(node, result)
})

t.Run("test get node with twin id", func(t *testing.T) {
count = 0
request = getNodeWithTwinID
result, err := c.GetNodeByTwinID(twinID)
require.NoError(err)
require.Equal(node, result)
})

t.Run("test list nodes of specific farm", func(t *testing.T) {
count = 0
request = listNodesInFarm
id := farmID
result, err := c.ListNodes(NodeFilter{FarmID: &id})
require.NoError(err)
require.Equal([]Node{node}, result)
})
}

func TestGetNodeCapacityRewards(t *testing.T) {
var request int
var count int
require := require.New(t)

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
_, err := w.Write(body)
require.NoError(err)
count++
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
require.NoError(err)

request = newClientWithNoAccount
c, err := NewRegistrarClient(baseURL)
require.NoError(err)

t.Run("test get node capacity rewards, status ok", func(t *testing.T) {
request = getNodeCapacityRewardsWithStatusOK
resp, err := c.GetNodeCapacityRewards(nodeID)
require.NoError(err)
require.Equal(NodeCapacityReward{}, resp)
})

t.Run("get node rewards for non-existing node", func(t *testing.T) {
request = getNodeCapacityRewardsWithStatusNotFound
_, err := c.GetNodeCapacityRewards(nodeID)
require.Error(err)
})

t.Run("no reports available, status UnprocessableEntity", func(t *testing.T) {
request = getNodeCapacityRewardsWithStatusUnprocessableEntity
res, err := c.GetNodeCapacityRewards(nodeID)
require.Error(err)
require.Equal(NodeCapacityReward{}, res)

})

t.Run("node with partial uptime rewards calculation", func(t *testing.T) {
request = getNodeCapacityRewardsWithPartialUptime
res, err := c.GetNodeCapacityRewards(nodeID)
require.NoError(err)
expected := NodeCapacityReward{
FarmerReward: 60.0,
TFReward: 20.0,
FPReward: 20.0,
Total: 100.0,
UpTimePercentage: 75.0,
}
require.Equal(expected, res)
// Verify reward distribution percentages are correct
require.Equal(0.6, res.FarmerReward/res.Total)
require.Equal(0.2, res.TFReward/res.Total)
require.Equal(0.2, res.FPReward/res.Total)
})

t.Run("bad request due to invalid node ID format", func(t *testing.T) {
request = getNodeCapacityRewardsWithBadRequest
res, err := c.GetNodeCapacityRewards(nodeID)
require.Error(err)
require.Equal(NodeCapacityReward{}, res)
})

t.Run("internal server error when calculating rewards", func(t *testing.T) {
request = getNodeCapacityRewardsWithServerError
res, err := c.GetNodeCapacityRewards(nodeID)
require.Error(err)
require.Equal(NodeCapacityReward{}, res)
})
}
8 changes: 8 additions & 0 deletions node-registrar/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@ type Location struct {
Longitude string `json:"longitude"`
Latitude string `json:"latitude"`
}

type NodeCapacityReward struct {
FarmerReward float64 `json:"FarmerReward"` // Reward amount for the node owner (60%)
TFReward float64 `json:"TFReward"` // Reward amount for Threefold Foundation (20%)
FPReward float64 `json:"FPReward"` // Reward amount for the Farming Pool (20%)
Total float64 `json:"Total"` // Total reward amount
UpTimePercentage float64 `json:"UptimePercentage"` // Node's uptime percentage
}
51 changes: 49 additions & 2 deletions node-registrar/client/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
var (
account = Account{TwinID: 1, Relays: []string{}, RMBEncKey: ""}
farm = Farm{FarmID: 1, FarmName: "freeFarm", TwinID: 1}
node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID}
node = Node{NodeID: 1, FarmID: farmID, TwinID: twinID, Resources: Resources{CRU: 2342}}
)

const (
Expand Down Expand Up @@ -44,6 +44,13 @@ const (
getNodeWithTwinID
listNodesInFarm

getNodeCapacityRewardsWithStatusOK
getNodeCapacityRewardsWithStatusNotFound
getNodeCapacityRewardsWithStatusUnprocessableEntity
getNodeCapacityRewardsWithPartialUptime
getNodeCapacityRewardsWithBadRequest
getNodeCapacityRewardsWithServerError

testMnemonic = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"

farmID uint64 = 1
Expand Down Expand Up @@ -220,6 +227,47 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti
require.NoError(err)
return http.StatusOK, resp

case getNodeCapacityRewardsWithStatusOK:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)
resp, err := json.Marshal(NodeCapacityReward{})
require.NoError(err)
return http.StatusOK, resp

case getNodeCapacityRewardsWithStatusNotFound:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)
return http.StatusNotFound, nil

case getNodeCapacityRewardsWithStatusUnprocessableEntity:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)
return http.StatusUnprocessableEntity, nil

case getNodeCapacityRewardsWithPartialUptime:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)
resp, err := json.Marshal(NodeCapacityReward{
FarmerReward: 60.0,
TFReward: 20.0,
FPReward: 20.0,
Total: 100.0,
UpTimePercentage: 75.0,
})
require.NoError(err)
return http.StatusOK, resp

case getNodeCapacityRewardsWithBadRequest:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)

return http.StatusBadRequest, nil

case getNodeCapacityRewardsWithServerError:
require.Equal("/v1/nodes/1/rewards", r.URL.Path)
require.Equal(http.MethodGet, r.Method)
return http.StatusInternalServerError, nil

// unauthorized requests
case newClientWithNoAccount,
getAccountWithPKStatusNotFount,
Expand All @@ -230,7 +278,6 @@ func serverHandler(r *http.Request, request, count int, require *require.Asserti
require.Equal(account.PublicKey, r.URL.Query().Get("public_key"))
require.Equal(http.MethodGet, r.Method)
return http.StatusNotFound, nil

}

return http.StatusNotAcceptable, nil
Expand Down
46 changes: 46 additions & 0 deletions node-registrar/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,52 @@ const docTemplate = `{
}
}
},
"/nodes/{node_id}/reward": {
"get": {
"description": "Retrieves reward calculation for a specific node based on resources and uptime",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"nodes"
],
"summary": "Get node monthly reward information",
"parameters": [
{
"type": "integer",
"description": "Node ID",
"name": "node_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Rewards details with the node uptime percentage",
"schema": {
"$ref": "#/definitions/server.Reward"
}
},
"400": {
"description": "Invalid node ID",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Node not found",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/nodes/{node_id}/uptime": {
"post": {
"description": "Submit uptime report for a node",
Expand Down
Loading
Loading