Skip to content

Commit 5c819eb

Browse files
Merge pull request #83 from Chia-Network/split-largest-coin
Add coin splitting command that uses largest coin
2 parents 8224f38 + e54e285 commit 5c819eb

5 files changed

Lines changed: 232 additions & 3 deletions

File tree

cmd/coins/coins.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package coins
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/chia-network/chia-tools/cmd"
7+
)
8+
9+
// coinsCmd represents the coins command
10+
var coinsCmd = &cobra.Command{
11+
Use: "coins",
12+
Short: "Utilities for working with chia coins",
13+
}
14+
15+
func init() {
16+
cmd.RootCmd.AddCommand(coinsCmd)
17+
}

cmd/coins/split-largest.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package coins
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/chia-network/go-chia-libs/pkg/rpc"
9+
"github.com/chia-network/go-chia-libs/pkg/types"
10+
"github.com/chia-network/go-modules/pkg/slogs"
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
13+
)
14+
15+
// splitLargestCmd represents the split-largest command
16+
var splitLargestCmd = &cobra.Command{
17+
Use: "split-largest",
18+
Short: "Find the largest coin in the wallet and split it into smaller coins",
19+
Example: `chia-tools coins split-largest --amount-per-coin 0.001 --number-of-coins 10
20+
chia-tools coins split-largest --id 1 --amount-per-coin 0.001 --number-of-coins 10 --fee 0.0001`,
21+
Run: func(cmd *cobra.Command, args []string) {
22+
amountPerCoinStr := viper.GetString("coins-amount-per-coin")
23+
numberOfCoins := viper.GetUint32("coins-number-of-coins")
24+
25+
if amountPerCoinStr == "" {
26+
slogs.Logr.Fatal("amount-per-coin must be specified")
27+
}
28+
if numberOfCoins == 0 {
29+
slogs.Logr.Fatal("number-of-coins must be greater than 0")
30+
}
31+
32+
SplitLargestCoin()
33+
},
34+
}
35+
36+
// convertXCHToMojos converts a decimal XCH amount to mojos
37+
func convertXCHToMojos(xchStr string) (uint64, error) {
38+
// Handle empty or zero values
39+
if xchStr == "" || xchStr == "0" {
40+
return 0, nil
41+
}
42+
43+
// Remove any trailing zeros after decimal point for cleaner parsing
44+
xchStr = strings.TrimRight(strings.TrimRight(xchStr, "0"), ".")
45+
46+
// If we end up with an empty string after trimming, it was just "0"
47+
if xchStr == "" {
48+
return 0, nil
49+
}
50+
51+
// Split by decimal point
52+
parts := strings.Split(xchStr, ".")
53+
if len(parts) > 2 {
54+
return 0, fmt.Errorf("invalid XCH amount format: %s", xchStr)
55+
}
56+
57+
// Parse the whole number part
58+
whole, err := strconv.ParseUint(parts[0], 10, 64)
59+
if err != nil {
60+
return 0, fmt.Errorf("invalid whole number part: %s", parts[0])
61+
}
62+
63+
// Convert to mojos (1 XCH = 1,000,000,000,000 mojos)
64+
mojos := whole * 1000000000000
65+
66+
// If there's a decimal part, handle it
67+
if len(parts) == 2 {
68+
decimalPart := parts[1]
69+
// Pad or truncate to 12 decimal places
70+
if len(decimalPart) > 12 {
71+
decimalPart = decimalPart[:12]
72+
} else {
73+
decimalPart = decimalPart + strings.Repeat("0", 12-len(decimalPart))
74+
}
75+
76+
decimalMojos, err := strconv.ParseUint(decimalPart, 10, 64)
77+
if err != nil {
78+
return 0, fmt.Errorf("invalid decimal part: %s", parts[1])
79+
}
80+
81+
mojos += decimalMojos
82+
}
83+
84+
return mojos, nil
85+
}
86+
87+
// SplitLargestCoin finds the largest coin in the wallet and splits it
88+
func SplitLargestCoin() {
89+
client, err := rpc.NewClient(rpc.ConnectionModeHTTP, rpc.WithAutoConfig())
90+
if err != nil {
91+
slogs.Logr.Fatal("error creating chia RPC client", "error", err)
92+
}
93+
94+
walletID := viper.GetUint32("coins-wallet-id")
95+
96+
slogs.Logr.Debug("Getting spendable coins", "wallet_id", walletID)
97+
98+
// Get spendable coins for the wallet
99+
spendableCoins, _, err := client.WalletService.GetSpendableCoins(&rpc.GetSpendableCoinsOptions{
100+
WalletID: walletID,
101+
})
102+
if err != nil {
103+
slogs.Logr.Fatal("error getting spendable coins", "error", err)
104+
}
105+
106+
if spendableCoins == nil || spendableCoins.ConfirmedRecords.IsAbsent() {
107+
slogs.Logr.Fatal("no spendable coins found for wallet", "wallet_id", walletID)
108+
}
109+
110+
records := spendableCoins.ConfirmedRecords.MustGet()
111+
if len(records) == 0 {
112+
slogs.Logr.Fatal("no spendable coins found in wallet", "wallet_id", walletID)
113+
}
114+
115+
// Find the largest coin from the spendable coins
116+
var largestCoin types.CoinRecord
117+
largestAmount := uint64(0)
118+
119+
for _, record := range records {
120+
if record.Coin.Amount > largestAmount {
121+
largestAmount = record.Coin.Amount
122+
largestCoin = record
123+
}
124+
}
125+
126+
if largestAmount == 0 {
127+
slogs.Logr.Fatal("no coins with value found in wallet", "wallet_id", walletID)
128+
}
129+
130+
coinID := largestCoin.Coin.ID().String()
131+
132+
slogs.Logr.Info("Found largest coin",
133+
"coin_id", coinID,
134+
"amount", largestCoin.Coin.Amount,
135+
"wallet_id", walletID)
136+
137+
// Convert string amounts to mojos
138+
amountPerCoinStr := viper.GetString("coins-amount-per-coin")
139+
feeStr := viper.GetString("coins-fee")
140+
141+
amountPerCoin, err := convertXCHToMojos(amountPerCoinStr)
142+
if err != nil {
143+
slogs.Logr.Fatal("error converting amount-per-coin to mojos", "error", err)
144+
}
145+
146+
fee, err := convertXCHToMojos(feeStr)
147+
if err != nil {
148+
slogs.Logr.Fatal("error converting fee to mojos", "error", err)
149+
}
150+
151+
numberOfCoins := viper.GetUint32("coins-number-of-coins")
152+
153+
// Validate that we have enough in the coin to split
154+
totalNeeded := amountPerCoin*uint64(numberOfCoins) + fee
155+
if largestCoin.Coin.Amount < totalNeeded {
156+
slogs.Logr.Fatal("largest coin does not have enough value for split",
157+
"coin_amount", largestCoin.Coin.Amount,
158+
"total_needed", totalNeeded,
159+
"amount_per_coin", amountPerCoin,
160+
"number_of_coins", numberOfCoins,
161+
"fee", fee)
162+
}
163+
164+
slogs.Logr.Info("Splitting coin",
165+
"coin_id", coinID,
166+
"amount_per_coin", amountPerCoin,
167+
"number_of_coins", numberOfCoins,
168+
"fee", fee)
169+
170+
// Split the coin
171+
splitResponse, _, err := client.WalletService.SplitCoins(&rpc.SplitCoinsOptions{
172+
WalletID: walletID,
173+
TargetCoinID: coinID,
174+
AmountPerCoin: amountPerCoin,
175+
NumberOfCoins: numberOfCoins,
176+
Fee: fee,
177+
Push: true,
178+
})
179+
if err != nil {
180+
slogs.Logr.Fatal("error splitting coin", "error", err)
181+
}
182+
183+
if splitResponse == nil {
184+
slogs.Logr.Fatal("no response from split_coins")
185+
}
186+
187+
if splitResponse.TransactionID.IsPresent() {
188+
fmt.Printf("Successfully split coin. Transaction ID: %s\n", splitResponse.TransactionID.MustGet())
189+
} else {
190+
fmt.Println("Coin split initiated successfully")
191+
}
192+
}
193+
194+
func init() {
195+
splitLargestCmd.PersistentFlags().StringP("amount-per-coin", "a", "", "The amount of each newly created coin, in XCH or CAT units")
196+
splitLargestCmd.PersistentFlags().Uint32P("number-of-coins", "n", 0, "The number of coins we are creating")
197+
splitLargestCmd.PersistentFlags().Uint32P("id", "i", 1, "Id of the wallet to use")
198+
splitLargestCmd.PersistentFlags().StringP("fee", "m", "0", "Set the fees for the transaction, in XCH")
199+
200+
// Mark required flags
201+
cobra.CheckErr(splitLargestCmd.MarkPersistentFlagRequired("amount-per-coin"))
202+
cobra.CheckErr(splitLargestCmd.MarkPersistentFlagRequired("number-of-coins"))
203+
204+
// Bind flags to viper
205+
cobra.CheckErr(viper.BindPFlag("coins-amount-per-coin", splitLargestCmd.PersistentFlags().Lookup("amount-per-coin")))
206+
cobra.CheckErr(viper.BindPFlag("coins-number-of-coins", splitLargestCmd.PersistentFlags().Lookup("number-of-coins")))
207+
cobra.CheckErr(viper.BindPFlag("coins-wallet-id", splitLargestCmd.PersistentFlags().Lookup("id")))
208+
cobra.CheckErr(viper.BindPFlag("coins-fee", splitLargestCmd.PersistentFlags().Lookup("fee")))
209+
210+
coinsCmd.AddCommand(splitLargestCmd)
211+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
toolchain go1.24.1
66

77
require (
8-
github.com/chia-network/go-chia-libs v0.21.7
8+
github.com/chia-network/go-chia-libs v0.21.8
99
github.com/chia-network/go-modules v0.0.9
1010
github.com/spf13/cast v1.9.2
1111
github.com/spf13/cobra v1.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/chia-network/go-chia-libs v0.21.7 h1:/5IdVSIFsbSYzXvPd6Q3PIYnz56d9rejd4hQoVWwK/E=
2-
github.com/chia-network/go-chia-libs v0.21.7/go.mod h1:+RMorskgxwYzPGf2gIyW0k7FGDdLrrH4X5ATrrMreb0=
1+
github.com/chia-network/go-chia-libs v0.21.8 h1:ARgNMNpBWvvfpfxjywDyObmNHs/b1p7wnICKwe9MDsA=
2+
github.com/chia-network/go-chia-libs v0.21.8/go.mod h1:+RMorskgxwYzPGf2gIyW0k7FGDdLrrH4X5ATrrMreb0=
33
github.com/chia-network/go-modules v0.0.9 h1:9zC48Tsrw5jh1DMl6h0kbBL8A8daeeHVsnLxML6lTcg=
44
github.com/chia-network/go-modules v0.0.9/go.mod h1:+cCfPV528ieagnMDkfZYp44cr3an/uBYUTfxzoFKhAg=
55
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"github.com/chia-network/chia-tools/cmd"
55
_ "github.com/chia-network/chia-tools/cmd/certs"
6+
_ "github.com/chia-network/chia-tools/cmd/coins"
67
_ "github.com/chia-network/chia-tools/cmd/config"
78
_ "github.com/chia-network/chia-tools/cmd/datalayer"
89
_ "github.com/chia-network/chia-tools/cmd/debug"

0 commit comments

Comments
 (0)