Skip to content

Commit 6efa2cd

Browse files
committed
TFwallet + Updating rust client
1 parent c29e0b0 commit 6efa2cd

File tree

11 files changed

+3111
-3202
lines changed

11 files changed

+3111
-3202
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# tfwallet
2+
3+
A simple CLI tool to interact with TFChain wallets.
4+
5+
## Build
6+
7+
```bash
8+
./build.sh
9+
```
10+
11+
## Usage
12+
13+
Set your private key via environment variable (mnemonic or hex seed):
14+
15+
```bash
16+
export TFCHAIN_KEY="your mnemonic phrase here"
17+
```
18+
19+
### Show wallet info
20+
21+
```bash
22+
./tfwallet
23+
```
24+
25+
Output:
26+
```toml
27+
address = "5EFH3jsZyriLXZ13GtyeBPKoaEUvBYpnpvaMgk9bZ9JPfFvJ"
28+
twin_id = 2
29+
is_hoster = false
30+
31+
[balance]
32+
free = "528404.4863972"
33+
reserved = "0.0000000"
34+
free_utft = "5284044863972"
35+
```
36+
37+
For hosters, it also shows farm and node information.
38+
39+
### Send TFT
40+
41+
```bash
42+
./tfwallet sendto <address> tft <amount>
43+
```
44+
45+
Example:
46+
```bash
47+
./tfwallet sendto 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY tft 10.5
48+
```
49+
50+
Output:
51+
```toml
52+
transferred = "10.5"
53+
to = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
54+
amount_utft = 105000000
55+
```
56+
57+
## Networks
58+
59+
The tool connects to TFChain mainnet (`wss://tfchain.grid.tf/ws`).
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
"os"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/centrifuge/go-substrate-rpc-client/v4/types"
11+
"github.com/rs/zerolog"
12+
substrate "github.com/threefoldtech/tfchain/clients/tfchain-client-go"
13+
)
14+
15+
func init() {
16+
zerolog.SetGlobalLevel(zerolog.Disabled)
17+
}
18+
19+
const (
20+
mainnetURL = "wss://tfchain.grid.tf/ws"
21+
tftDivisor = 10_000_000
22+
)
23+
24+
type WalletInfo struct {
25+
Address string `toml:"address"`
26+
TwinID uint32 `toml:"twin_id,omitempty"`
27+
Balance BalanceInfo `toml:"balance"`
28+
IsHoster bool `toml:"is_hoster"`
29+
Farm *FarmInfo `toml:"farm,omitempty"`
30+
Nodes []NodeInfo `toml:"nodes,omitempty"`
31+
}
32+
33+
type BalanceInfo struct {
34+
Free string `toml:"free"`
35+
Reserved string `toml:"reserved"`
36+
FreeUTFT string `toml:"free_utft"`
37+
}
38+
39+
type FarmInfo struct {
40+
ID uint32 `toml:"id"`
41+
Name string `toml:"name"`
42+
DedicatedFarm bool `toml:"dedicated_farm"`
43+
}
44+
45+
type NodeInfo struct {
46+
ID uint32 `toml:"id"`
47+
FarmID uint32 `toml:"farm_id"`
48+
TwinID uint32 `toml:"twin_id"`
49+
City string `toml:"city"`
50+
Country string `toml:"country"`
51+
CRU uint64 `toml:"cru"`
52+
MRU uint64 `toml:"mru"`
53+
SRU uint64 `toml:"sru"`
54+
HRU uint64 `toml:"hru"`
55+
}
56+
57+
func main() {
58+
key := os.Getenv("TFCHAIN_KEY")
59+
if key == "" {
60+
fmt.Fprintln(os.Stderr, "error: TFCHAIN_KEY environment variable is not set")
61+
os.Exit(1)
62+
}
63+
64+
key = strings.TrimSpace(key)
65+
66+
identity, err := createIdentity(key)
67+
if err != nil {
68+
fmt.Fprintf(os.Stderr, "error: failed to create identity: %v\n", err)
69+
os.Exit(1)
70+
}
71+
72+
mgr := substrate.NewManager(mainnetURL)
73+
sub, err := mgr.Substrate()
74+
if err != nil {
75+
fmt.Fprintf(os.Stderr, "error: failed to connect to TFChain: %v\n", err)
76+
os.Exit(1)
77+
}
78+
defer sub.Close()
79+
80+
// Check for subcommand
81+
if len(os.Args) > 1 {
82+
switch os.Args[1] {
83+
case "sendto":
84+
cmdSendTo(sub, identity, os.Args[2:])
85+
return
86+
default:
87+
fmt.Fprintf(os.Stderr, "error: unknown command: %s\n", os.Args[1])
88+
os.Exit(1)
89+
}
90+
}
91+
92+
// Default: show wallet info
93+
info, err := getWalletInfo(sub, identity)
94+
if err != nil {
95+
fmt.Fprintf(os.Stderr, "error: failed to get wallet info: %v\n", err)
96+
os.Exit(1)
97+
}
98+
99+
printTOML(info)
100+
}
101+
102+
func cmdSendTo(sub *substrate.Substrate, identity substrate.Identity, args []string) {
103+
if len(args) < 3 {
104+
fmt.Fprintln(os.Stderr, "usage: tfwallet sendto <address> tft <amount>")
105+
os.Exit(1)
106+
}
107+
108+
destAddr := args[0]
109+
if args[1] != "tft" {
110+
fmt.Fprintln(os.Stderr, "error: only 'tft' is supported")
111+
os.Exit(1)
112+
}
113+
amountStr := args[2]
114+
115+
// Parse amount (supports decimal like 10.5)
116+
amountUTFT, err := parseTFTAmount(amountStr)
117+
if err != nil {
118+
fmt.Fprintf(os.Stderr, "error: invalid amount: %v\n", err)
119+
os.Exit(1)
120+
}
121+
122+
// Parse destination address
123+
destAccount, err := substrate.FromAddress(destAddr)
124+
if err != nil {
125+
fmt.Fprintf(os.Stderr, "error: invalid destination address: %v\n", err)
126+
os.Exit(1)
127+
}
128+
129+
// Transfer
130+
err = sub.Transfer(identity, amountUTFT, destAccount)
131+
if err != nil {
132+
fmt.Fprintf(os.Stderr, "error: transfer failed: %v\n", err)
133+
os.Exit(1)
134+
}
135+
136+
fmt.Printf("transferred = %q\n", amountStr)
137+
fmt.Printf("to = %q\n", destAddr)
138+
fmt.Printf("amount_utft = %d\n", amountUTFT)
139+
}
140+
141+
// parseTFTAmount parses a TFT amount string (e.g., "10", "10.5", "0.001") to uTFT
142+
func parseTFTAmount(s string) (uint64, error) {
143+
s = strings.TrimSpace(s)
144+
145+
// Handle decimal
146+
parts := strings.Split(s, ".")
147+
if len(parts) > 2 {
148+
return 0, fmt.Errorf("invalid number format")
149+
}
150+
151+
whole := parts[0]
152+
if whole == "" {
153+
whole = "0"
154+
}
155+
156+
wholeVal, err := strconv.ParseUint(whole, 10, 64)
157+
if err != nil {
158+
return 0, err
159+
}
160+
161+
result := wholeVal * tftDivisor
162+
163+
if len(parts) == 2 {
164+
decimal := parts[1]
165+
// Pad or truncate to 7 decimal places
166+
if len(decimal) > 7 {
167+
decimal = decimal[:7]
168+
}
169+
for len(decimal) < 7 {
170+
decimal += "0"
171+
}
172+
decVal, err := strconv.ParseUint(decimal, 10, 64)
173+
if err != nil {
174+
return 0, err
175+
}
176+
result += decVal
177+
}
178+
179+
return result, nil
180+
}
181+
182+
func createIdentity(key string) (substrate.Identity, error) {
183+
if strings.HasPrefix(key, "0x") {
184+
identity, err := substrate.NewIdentityFromSr25519Phrase(key)
185+
if err == nil {
186+
return identity, nil
187+
}
188+
return substrate.NewIdentityFromEd25519Phrase(key)
189+
}
190+
191+
identity, err := substrate.NewIdentityFromSr25519Phrase(key)
192+
if err == nil {
193+
return identity, nil
194+
}
195+
196+
return substrate.NewIdentityFromEd25519Phrase(key)
197+
}
198+
199+
func getWalletInfo(sub *substrate.Substrate, identity substrate.Identity) (*WalletInfo, error) {
200+
info := &WalletInfo{
201+
Address: identity.Address(),
202+
}
203+
204+
account, err := sub.GetAccount(identity)
205+
if err != nil && err != substrate.ErrAccountNotFound {
206+
return nil, fmt.Errorf("failed to get account: %w", err)
207+
}
208+
209+
freeBalance := account.Data.Free
210+
reservedBalance := account.Data.Reserved
211+
212+
info.Balance = BalanceInfo{
213+
Free: formatTFT(freeBalance),
214+
Reserved: formatTFT(reservedBalance),
215+
FreeUTFT: freeBalance.String(),
216+
}
217+
218+
twinID, err := sub.GetTwinByPubKey(identity.PublicKey())
219+
if err == nil && twinID > 0 {
220+
info.TwinID = twinID
221+
222+
twin, err := sub.GetTwin(twinID)
223+
if err == nil && twin != nil {
224+
nodeID, err := sub.GetNodeByTwinID(twinID)
225+
if err == nil && nodeID > 0 {
226+
info.IsHoster = true
227+
228+
node, err := sub.GetNode(nodeID)
229+
if err == nil && node != nil {
230+
farm, err := sub.GetFarm(uint32(node.FarmID))
231+
if err == nil && farm != nil {
232+
info.Farm = &FarmInfo{
233+
ID: uint32(farm.ID),
234+
Name: farm.Name,
235+
DedicatedFarm: farm.DedicatedFarm,
236+
}
237+
238+
nodeIDs, err := sub.GetNodes(uint32(farm.ID))
239+
if err == nil {
240+
for _, nid := range nodeIDs {
241+
n, err := sub.GetNode(nid)
242+
if err == nil && n != nil {
243+
info.Nodes = append(info.Nodes, NodeInfo{
244+
ID: uint32(n.ID),
245+
FarmID: uint32(n.FarmID),
246+
TwinID: uint32(n.TwinID),
247+
City: n.Location.City,
248+
Country: n.Location.Country,
249+
CRU: uint64(n.Resources.CRU),
250+
MRU: uint64(n.Resources.MRU),
251+
SRU: uint64(n.Resources.SRU),
252+
HRU: uint64(n.Resources.HRU),
253+
})
254+
}
255+
}
256+
}
257+
}
258+
}
259+
}
260+
}
261+
}
262+
263+
return info, nil
264+
}
265+
266+
func formatTFT(amount types.U128) string {
267+
if amount.Int == nil {
268+
return "0.0000000"
269+
}
270+
271+
divisor := big.NewInt(tftDivisor)
272+
whole := new(big.Int).Div(amount.Int, divisor)
273+
remainder := new(big.Int).Mod(amount.Int, divisor)
274+
275+
return fmt.Sprintf("%s.%07d", whole.String(), remainder.Int64())
276+
}
277+
278+
func printTOML(info *WalletInfo) {
279+
fmt.Printf("address = %q\n", info.Address)
280+
if info.TwinID > 0 {
281+
fmt.Printf("twin_id = %d\n", info.TwinID)
282+
}
283+
fmt.Printf("is_hoster = %t\n", info.IsHoster)
284+
fmt.Println()
285+
286+
fmt.Println("[balance]")
287+
fmt.Printf("free = %q\n", info.Balance.Free)
288+
fmt.Printf("reserved = %q\n", info.Balance.Reserved)
289+
fmt.Printf("free_utft = %q\n", info.Balance.FreeUTFT)
290+
291+
if info.Farm != nil {
292+
fmt.Println()
293+
fmt.Println("[farm]")
294+
fmt.Printf("id = %d\n", info.Farm.ID)
295+
fmt.Printf("name = %q\n", info.Farm.Name)
296+
fmt.Printf("dedicated_farm = %t\n", info.Farm.DedicatedFarm)
297+
}
298+
299+
if len(info.Nodes) > 0 {
300+
fmt.Println()
301+
for i, node := range info.Nodes {
302+
fmt.Println("[[nodes]]")
303+
fmt.Printf("id = %d\n", node.ID)
304+
fmt.Printf("farm_id = %d\n", node.FarmID)
305+
fmt.Printf("twin_id = %d\n", node.TwinID)
306+
fmt.Printf("city = %q\n", node.City)
307+
fmt.Printf("country = %q\n", node.Country)
308+
fmt.Printf("cru = %d\n", node.CRU)
309+
fmt.Printf("mru = %d\n", node.MRU)
310+
fmt.Printf("sru = %d\n", node.SRU)
311+
fmt.Printf("hru = %d\n", node.HRU)
312+
if i < len(info.Nodes)-1 {
313+
fmt.Println()
314+
}
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)