Skip to content

Commit 8d32a80

Browse files
authored
Add billing rate helpers and historical query (#1058)
* feat: add methods to get TFT billing rate from current and historical blocks
1 parent 0f66e0c commit 8d32a80

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
8+
substrate "github.com/threefoldtech/tfchain/clients/tfchain-client-go"
9+
)
10+
11+
func main() {
12+
endpoint := flag.String("endpoint", "wss://tfchain.grid.tf/ws", "TFChain WebSocket endpoint")
13+
block := flag.Uint64("block", 0, "Optional block number for historical billing rate (0 = latest)")
14+
flag.Parse()
15+
16+
mgr := substrate.NewManager(*endpoint)
17+
s, err := mgr.Substrate()
18+
if err != nil {
19+
log.Fatalf("connect failed: %v", err)
20+
}
21+
defer s.Close()
22+
23+
// Historical billing rate if requested
24+
if *block > 0 {
25+
rateAt, err := s.GetTFTBillingRateAt(*block)
26+
if err != nil {
27+
log.Fatalf("GetTFTBillingRateAt(%d) failed: %v", *block, err)
28+
}
29+
fmt.Printf("Billing Rate at block %d (mUSD, clamped): %d\n", *block, uint32(rateAt))
30+
} else {
31+
// Latest billing rate
32+
rate, err := s.GetTFTBillingRate()
33+
if err != nil {
34+
log.Fatalf("GetTFTBillingRate failed: %v", err)
35+
}
36+
fmt.Printf("Latest Billing Rate (mUSD, clamped): %d\n", uint32(rate))
37+
}
38+
}

clients/tfchain-client-go/price.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,142 @@ func (s *Substrate) GetAverageTFTPrice() (price types.U32, err error) {
9797

9898
return
9999
}
100+
101+
// GetTFTBillingRate returns the current billing rate (in mUSD) used on-chain,
102+
// computed as AverageTftPrice clamped between MinTftPrice and MaxTftPrice.
103+
func (s *Substrate) GetTFTBillingRate() (rate types.U32, err error) {
104+
cl, meta, err := s.GetClient()
105+
if err != nil {
106+
return
107+
}
108+
109+
// Build keys
110+
keyAvg, err := types.CreateStorageKey(meta, "TFTPriceModule", "AverageTftPrice")
111+
if err != nil {
112+
return rate, errors.Wrap(err, "failed to create storage key (AverageTftPrice)")
113+
}
114+
keyMin, err := types.CreateStorageKey(meta, "TFTPriceModule", "MinTftPrice")
115+
if err != nil {
116+
return rate, errors.Wrap(err, "failed to create storage key (MinTftPrice)")
117+
}
118+
keyMax, err := types.CreateStorageKey(meta, "TFTPriceModule", "MaxTftPrice")
119+
if err != nil {
120+
return rate, errors.Wrap(err, "failed to create storage key (MaxTftPrice)")
121+
}
122+
123+
// Read latest values
124+
var avg, min, max types.U32
125+
ok, err := cl.RPC.State.GetStorageLatest(keyAvg, &avg)
126+
if err != nil {
127+
return rate, errors.Wrap(err, "failed to read AverageTftPrice")
128+
}
129+
if !ok {
130+
return rate, errors.Wrap(ErrNotFound, "AverageTftPrice not found")
131+
}
132+
133+
ok, err = cl.RPC.State.GetStorageLatest(keyMin, &min)
134+
if err != nil {
135+
return rate, errors.Wrap(err, "failed to read MinTftPrice")
136+
}
137+
if !ok {
138+
return rate, errors.Wrap(ErrNotFound, "MinTftPrice not found")
139+
}
140+
141+
ok, err = cl.RPC.State.GetStorageLatest(keyMax, &max)
142+
if err != nil {
143+
return rate, errors.Wrap(err, "failed to read MaxTftPrice")
144+
}
145+
if !ok {
146+
return rate, errors.Wrap(ErrNotFound, "MaxTftPrice not found")
147+
}
148+
149+
rate = clampTFTPrice(avg, min, max)
150+
return
151+
}
152+
153+
// GetTFTBillingRateAt returns the billing rate (in mUSD) at a specific block number,
154+
// computed as AverageTftPrice clamped between MinTftPrice and MaxTftPrice at that block.
155+
func (s *Substrate) GetTFTBillingRateAt(block uint64) (rate types.U32, err error) {
156+
cl, _, err := s.GetClient()
157+
if err != nil {
158+
return
159+
}
160+
161+
// Resolve block hash
162+
bh, err := cl.RPC.Chain.GetBlockHash(block)
163+
if err != nil {
164+
return rate, errors.Wrap(err, "failed to resolve block hash")
165+
}
166+
167+
// Metadata at block
168+
metaAtBlock, err := cl.RPC.State.GetMetadata(bh)
169+
if err != nil {
170+
return rate, errors.Wrap(err, "failed to get metadata at block")
171+
}
172+
173+
// Keys at block
174+
keyAvg, err := types.CreateStorageKey(metaAtBlock, "TFTPriceModule", "AverageTftPrice")
175+
if err != nil {
176+
return rate, errors.Wrap(err, "failed to create storage key (AverageTftPrice)")
177+
}
178+
keyMin, err := types.CreateStorageKey(metaAtBlock, "TFTPriceModule", "MinTftPrice")
179+
if err != nil {
180+
return rate, errors.Wrap(err, "failed to create storage key (MinTftPrice)")
181+
}
182+
keyMax, err := types.CreateStorageKey(metaAtBlock, "TFTPriceModule", "MaxTftPrice")
183+
if err != nil {
184+
return rate, errors.Wrap(err, "failed to create storage key (MaxTftPrice)")
185+
}
186+
187+
// Read at block
188+
var avg, min, max types.U32
189+
raw, err := cl.RPC.State.GetStorageRaw(keyAvg, bh)
190+
if err != nil {
191+
return rate, errors.Wrap(err, "failed to get AverageTftPrice at block")
192+
}
193+
if len(*raw) == 0 {
194+
return rate, errors.Wrap(ErrNotFound, "AverageTftPrice not found at block")
195+
}
196+
if err := Decode(*raw, &avg); err != nil {
197+
return rate, errors.Wrap(err, "failed to decode AverageTftPrice")
198+
}
199+
200+
raw, err = cl.RPC.State.GetStorageRaw(keyMin, bh)
201+
if err != nil {
202+
return rate, errors.Wrap(err, "failed to get MinTftPrice at block")
203+
}
204+
if len(*raw) == 0 {
205+
return rate, errors.Wrap(ErrNotFound, "MinTftPrice not found at block")
206+
}
207+
if err := Decode(*raw, &min); err != nil {
208+
return rate, errors.Wrap(err, "failed to decode MinTftPrice")
209+
}
210+
211+
raw, err = cl.RPC.State.GetStorageRaw(keyMax, bh)
212+
if err != nil {
213+
return rate, errors.Wrap(err, "failed to get MaxTftPrice at block")
214+
}
215+
if len(*raw) == 0 {
216+
return rate, errors.Wrap(ErrNotFound, "MaxTftPrice not found at block")
217+
}
218+
if err := Decode(*raw, &max); err != nil {
219+
return rate, errors.Wrap(err, "failed to decode MaxTftPrice")
220+
}
221+
222+
// Clamp
223+
rate = clampTFTPrice(avg, min, max)
224+
return
225+
}
226+
227+
// clampTFTPrice mirrors the on-chain clamping logic used by pallet-smart-contract
228+
// tft_price = max(AverageTftPrice, MinTftPrice) then min(_, MaxTftPrice)
229+
func clampTFTPrice(avg, min, max types.U32) types.U32 {
230+
rate := avg
231+
if rate < min {
232+
rate = min
233+
}
234+
if rate > max {
235+
rate = max
236+
}
237+
return rate
238+
}

0 commit comments

Comments
 (0)