From c5e81884942f6bedd7be370f3b99cdb00251bc3a Mon Sep 17 00:00:00 2001 From: littleblackcloud <163544315+littleblackcloud@users.noreply.github.com> Date: Fri, 8 Nov 2024 02:30:57 +0100 Subject: [PATCH 1/4] Add flag to split coins into parts --- client/cmd/split.go | 143 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 14 deletions(-) diff --git a/client/cmd/split.go b/client/cmd/split.go index 3def31f2..87fb2067 100644 --- a/client/cmd/split.go +++ b/client/cmd/split.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "context" "encoding/hex" "fmt" @@ -8,24 +9,113 @@ import ( "os" "strings" + "github.com/iden3/go-iden3-crypto/poseidon" "github.com/shopspring/decimal" "github.com/spf13/cobra" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" ) +func getCoinAmount(coinaddr []byte) *big.Int { + conn, err := GetGRPCClient() + if err != nil { + panic(err) + } + defer conn.Close() + + client := protobufs.NewNodeServiceClient(conn) + peerId := GetPeerIDFromConfig(NodeConfig) + privKey, err := GetPrivKeyFromConfig(NodeConfig) + if err != nil { + panic(err) + } + + pub, err := privKey.GetPublic().Raw() + if err != nil { + panic(err) + } + + addr, err := poseidon.HashBytes([]byte(peerId)) + if err != nil { + panic(err) + } + + addrBytes := addr.FillBytes(make([]byte, 32)) + resp, err := client.GetTokensByAccount( + context.Background(), + &protobufs.GetTokensByAccountRequest{ + Address: addrBytes, + }, + ) + if err != nil { + panic(err) + } + + if len(resp.Coins) != len(resp.FrameNumbers) { + panic("invalid response from RPC") + } + + altAddr, err := poseidon.HashBytes([]byte(pub)) + if err != nil { + panic(err) + } + + altAddrBytes := altAddr.FillBytes(make([]byte, 32)) + resp2, err := client.GetTokensByAccount( + context.Background(), + &protobufs.GetTokensByAccountRequest{ + Address: altAddrBytes, + }, + ) + if err != nil { + panic(err) + } + + if len(resp.Coins) != len(resp.FrameNumbers) { + panic("invalid response from RPC") + } + + var amount *big.Int + for i, coin := range resp.Coins { + if bytes.Equal(resp.Addresses[i], coinaddr) { + amount = new(big.Int).SetBytes(coin.Amount) + } + } + for i, coin := range resp2.Coins { + if bytes.Equal(resp.Addresses[i], coinaddr) { + amount = new(big.Int).SetBytes(coin.Amount) + } + } + return amount +} + +var parts int64 var splitCmd = &cobra.Command{ Use: "split", Short: "Splits a coin into multiple coins", Long: `Splits a coin into multiple coins: - + split ... - + split -p/--parts + OfCoin - the address of the coin to split Amounts - the sets of amounts to split + Parts - the number of parts to split the coin into `, Run: func(cmd *cobra.Command, args []string) { - if len(args) < 3 { - fmt.Println("invalid command") + if len(args) < 3 && parts == 1 { + fmt.Println("did you forget to specify and ?") + os.Exit(1) + } + if len(args) < 1 && parts > 1 { + fmt.Println("did you forget to specify ?") + os.Exit(1) + } + if len(args) > 1 && parts > 1 { + fmt.Println("-p/--parts can't be combined with ") + os.Exit(1) + } + if parts > 100 { + fmt.Println("too many parts, maximum is 100") os.Exit(1) } @@ -40,18 +130,42 @@ var splitCmd = &cobra.Command{ } payload = append(payload, coinaddr...) - conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + // split the coin into parts amounts := [][]byte{} - for _, amt := range args[1:] { - amount, err := decimal.NewFromString(amt) - if err != nil { - fmt.Println("invalid amount") - os.Exit(1) + if parts > 1 { + // get the amount of the coin to be split + coinAmount := getCoinAmount(coinaddr) + + // split the coin amount into parts + amount := new(big.Int).Div(coinAmount, big.NewInt(parts)) + for i := int64(0); i < parts; i++ { + amountBytes := amount.FillBytes(make([]byte, 32)) + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // if there is a remainder, we need to add it as a separate amount + // because the amounts must sum to the original coin amount + remainder := new(big.Int).Mod(coinAmount, big.NewInt(parts)) + if remainder.Cmp(big.NewInt(0)) != 0 { + remainderBytes := remainder.FillBytes(make([]byte, 32)) + amounts = append(amounts, remainderBytes) + payload = append(payload, remainderBytes...) + } + } else { + // split the coin into the user provided amounts + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + for _, amt := range args[1:] { + amount, err := decimal.NewFromString(amt) + if err != nil { + fmt.Println("invalid amount, must be a decimal number like 0.02 or 2") + os.Exit(1) + } + amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) } - amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) - amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) - amounts = append(amounts, amountBytes) - payload = append(payload, amountBytes...) } conn, err := GetGRPCClient() @@ -100,5 +214,6 @@ var splitCmd = &cobra.Command{ } func init() { + splitCmd.Flags().Int64VarP(&parts, "parts", "p", 1, "the number of parts to split the coin into") tokenCmd.AddCommand(splitCmd) } From ee82b0a0096c924b142d9ed5735f34bfba07b1cb Mon Sep 17 00:00:00 2001 From: littleblackcloud <163544315+littleblackcloud@users.noreply.github.com> Date: Fri, 8 Nov 2024 21:09:06 +0100 Subject: [PATCH 2/4] Refactor split to allow specifying amount of each part --- client/cmd/split.go | 273 ++++++++++++++++++++++++++++---------------- 1 file changed, 174 insertions(+), 99 deletions(-) diff --git a/client/cmd/split.go b/client/cmd/split.go index 87fb2067..7fd2bd65 100644 --- a/client/cmd/split.go +++ b/client/cmd/split.go @@ -15,91 +15,46 @@ import ( "source.quilibrium.com/quilibrium/monorepo/node/protobufs" ) -func getCoinAmount(coinaddr []byte) *big.Int { - conn, err := GetGRPCClient() - if err != nil { - panic(err) - } - defer conn.Close() - - client := protobufs.NewNodeServiceClient(conn) - peerId := GetPeerIDFromConfig(NodeConfig) - privKey, err := GetPrivKeyFromConfig(NodeConfig) - if err != nil { - panic(err) - } - - pub, err := privKey.GetPublic().Raw() - if err != nil { - panic(err) - } - - addr, err := poseidon.HashBytes([]byte(peerId)) - if err != nil { - panic(err) - } - - addrBytes := addr.FillBytes(make([]byte, 32)) - resp, err := client.GetTokensByAccount( - context.Background(), - &protobufs.GetTokensByAccountRequest{ - Address: addrBytes, - }, - ) - if err != nil { - panic(err) - } - - if len(resp.Coins) != len(resp.FrameNumbers) { - panic("invalid response from RPC") - } - - altAddr, err := poseidon.HashBytes([]byte(pub)) - if err != nil { - panic(err) - } - - altAddrBytes := altAddr.FillBytes(make([]byte, 32)) - resp2, err := client.GetTokensByAccount( - context.Background(), - &protobufs.GetTokensByAccountRequest{ - Address: altAddrBytes, - }, - ) - if err != nil { - panic(err) - } - - if len(resp.Coins) != len(resp.FrameNumbers) { - panic("invalid response from RPC") - } - - var amount *big.Int - for i, coin := range resp.Coins { - if bytes.Equal(resp.Addresses[i], coinaddr) { - amount = new(big.Int).SetBytes(coin.Amount) - } - } - for i, coin := range resp2.Coins { - if bytes.Equal(resp.Addresses[i], coinaddr) { - amount = new(big.Int).SetBytes(coin.Amount) - } - } - return amount -} - -var parts int64 +var parts int +var partAmount string var splitCmd = &cobra.Command{ Use: "split", Short: "Splits a coin into multiple coins", Long: `Splits a coin into multiple coins: split ... - split -p/--parts + split <--parts PARTS> [--part-amount AMOUNT] OfCoin - the address of the coin to split Amounts - the sets of amounts to split - Parts - the number of parts to split the coin into + + Example - Split a coin into the specified amounts: + $ qclient token coins + 1.000000000000 QUIL (Coin 0x1234) + $ qclient token split 0x1234 0.5 0.25 0.25 + $ qclient token coins + 0.250000000000 QUIL (Coin 0x1111) + 0.250000000000 QUIL (Coin 0x2222) + 0.500000000000 QUIL (Coin 0x3333) + + Example - Split a coin into three parts: + $ qclient token coins + 1.000000000000 QUIL (Coin 0x1234) + $ qclient token split 0x1234 --parts 3 + $ qclient token coins + 0.000000000250 QUIL (Coin 0x1111) + 0.333333333250 QUIL (Coin 0x2222) + 0.333333333250 QUIL (Coin 0x3333) + 0.333333333250 QUIL (Coin 0x4444) + + Example - Split a coin into three parts using the specified amounts: + $ qclient token coins + 1.000000000000 QUIL (Coin 0x1234) + $ qclient token split 0x1234 --parts 2 --part-amount 0.35 + $ qclient token coins + 0.300000000000 QUIL (Coin 0x1111) + 0.350000000000 QUIL (Coin 0x2222) + 0.350000000000 QUIL (Coin 0x3333) `, Run: func(cmd *cobra.Command, args []string) { if len(args) < 3 && parts == 1 { @@ -114,6 +69,10 @@ var splitCmd = &cobra.Command{ fmt.Println("-p/--parts can't be combined with ") os.Exit(1) } + if len(args) > 1 && partAmount != "" { + fmt.Println("-a/--part-amount can't be combined with ") + os.Exit(1) + } if parts > 100 { fmt.Println("too many parts, maximum is 100") os.Exit(1) @@ -130,31 +89,15 @@ var splitCmd = &cobra.Command{ } payload = append(payload, coinaddr...) - // split the coin into parts + // Get the amount of the coin to be split + totalAmount := getCoinAmount(coinaddr) + amounts := [][]byte{} - if parts > 1 { - // get the amount of the coin to be split - coinAmount := getCoinAmount(coinaddr) - - // split the coin amount into parts - amount := new(big.Int).Div(coinAmount, big.NewInt(parts)) - for i := int64(0); i < parts; i++ { - amountBytes := amount.FillBytes(make([]byte, 32)) - amounts = append(amounts, amountBytes) - payload = append(payload, amountBytes...) - } - // if there is a remainder, we need to add it as a separate amount - // because the amounts must sum to the original coin amount - remainder := new(big.Int).Mod(coinAmount, big.NewInt(parts)) - if remainder.Cmp(big.NewInt(0)) != 0 { - remainderBytes := remainder.FillBytes(make([]byte, 32)) - amounts = append(amounts, remainderBytes) - payload = append(payload, remainderBytes...) - } - } else { - // split the coin into the user provided amounts + // Split the coin into the user specified amounts + if parts == 1 { conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + inputAmount := new(big.Int) for _, amt := range args[1:] { amount, err := decimal.NewFromString(amt) if err != nil { @@ -162,10 +105,68 @@ var splitCmd = &cobra.Command{ os.Exit(1) } amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + inputAmount = inputAmount.Add(inputAmount, amount.BigInt()) amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) amounts = append(amounts, amountBytes) payload = append(payload, amountBytes...) } + + // Check if the user specified amounts sum to the total amount of the coin + if inputAmount.Cmp(totalAmount) != 0 { + fmt.Println("the specified amounts must sum to the total amount of the coin") + os.Exit(1) + } + } + + // Split the coin into parts + if parts > 1 && partAmount == "" { + amount := new(big.Int).Div(totalAmount, big.NewInt(int64(parts))) + amountBytes := amount.FillBytes(make([]byte, 32)) + for i := int64(0); i < int64(parts); i++ { + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // If there is a remainder, we need to add it as a separate amount + // because the amounts must sum to the original coin amount. + remainder := new(big.Int).Mod(totalAmount, big.NewInt(int64(parts))) + if remainder.Cmp(big.NewInt(0)) != 0 { + remainderBytes := remainder.FillBytes(make([]byte, 32)) + amounts = append(amounts, remainderBytes) + payload = append(payload, remainderBytes...) + } + } + + // Split the coin into parts of the user specified amount + if parts > 1 && partAmount != "" { + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + amount, err := decimal.NewFromString(partAmount) + if err != nil { + fmt.Println("invalid amount, must be a decimal number like 0.02 or 2") + os.Exit(1) + } + amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + inputAmount := new(big.Int).Mul(amount.BigInt(), big.NewInt(int64(parts))) + amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) + for i := int64(0); i < int64(parts); i++ { + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // If there is a remainder, we need to add it as a separate amount + // because the amounts must sum to the original coin amount. + remainder := new(big.Int).Sub(totalAmount, inputAmount) + if remainder.Cmp(big.NewInt(0)) != 0 { + remainderBytes := remainder.FillBytes(make([]byte, 32)) + amounts = append(amounts, remainderBytes) + payload = append(payload, remainderBytes...) + } + + // Check if the user specified amounts sum to the total amount of the coin + if new(big.Int).Add(inputAmount, new(big.Int).Abs(remainder)).Cmp(totalAmount) != 0 { + fmt.Println("the specified amounts must sum to the total amount of the coin") + os.Exit(1) + } } conn, err := GetGRPCClient() @@ -214,6 +215,80 @@ var splitCmd = &cobra.Command{ } func init() { - splitCmd.Flags().Int64VarP(&parts, "parts", "p", 1, "the number of parts to split the coin into") + splitCmd.Flags().IntVarP(&parts, "parts", "p", 1, "number of parts to split the coin into") + splitCmd.Flags().StringVarP(&partAmount, "part-amount", "a", "", "amount of each part") tokenCmd.AddCommand(splitCmd) } + +func getCoinAmount(coinaddr []byte) *big.Int { + conn, err := GetGRPCClient() + if err != nil { + panic(err) + } + defer conn.Close() + + client := protobufs.NewNodeServiceClient(conn) + peerId := GetPeerIDFromConfig(NodeConfig) + privKey, err := GetPrivKeyFromConfig(NodeConfig) + if err != nil { + panic(err) + } + + pub, err := privKey.GetPublic().Raw() + if err != nil { + panic(err) + } + + addr, err := poseidon.HashBytes([]byte(peerId)) + if err != nil { + panic(err) + } + + addrBytes := addr.FillBytes(make([]byte, 32)) + resp, err := client.GetTokensByAccount( + context.Background(), + &protobufs.GetTokensByAccountRequest{ + Address: addrBytes, + }, + ) + if err != nil { + panic(err) + } + + if len(resp.Coins) != len(resp.FrameNumbers) { + panic("invalid response from RPC") + } + + altAddr, err := poseidon.HashBytes([]byte(pub)) + if err != nil { + panic(err) + } + + altAddrBytes := altAddr.FillBytes(make([]byte, 32)) + resp2, err := client.GetTokensByAccount( + context.Background(), + &protobufs.GetTokensByAccountRequest{ + Address: altAddrBytes, + }, + ) + if err != nil { + panic(err) + } + + if len(resp.Coins) != len(resp.FrameNumbers) { + panic("invalid response from RPC") + } + + var amount *big.Int + for i, coin := range resp.Coins { + if bytes.Equal(resp.Addresses[i], coinaddr) { + amount = new(big.Int).SetBytes(coin.Amount) + } + } + for i, coin := range resp2.Coins { + if bytes.Equal(resp.Addresses[i], coinaddr) { + amount = new(big.Int).SetBytes(coin.Amount) + } + } + return amount +} From 41f987927b2dfdee2b076be608f95fee0b682f9e Mon Sep 17 00:00:00 2001 From: littleblackcloud <163544315+littleblackcloud@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:43:00 +0100 Subject: [PATCH 3/4] Fix examples --- client/cmd/split.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/cmd/split.go b/client/cmd/split.go index 7fd2bd65..82fa0ea6 100644 --- a/client/cmd/split.go +++ b/client/cmd/split.go @@ -47,7 +47,9 @@ var splitCmd = &cobra.Command{ 0.333333333250 QUIL (Coin 0x3333) 0.333333333250 QUIL (Coin 0x4444) - Example - Split a coin into three parts using the specified amounts: + **Note:** Coin 0x1111 is the remainder. + + Example - Split a coin into two parts using the specified amounts: $ qclient token coins 1.000000000000 QUIL (Coin 0x1234) $ qclient token split 0x1234 --parts 2 --part-amount 0.35 @@ -55,6 +57,8 @@ var splitCmd = &cobra.Command{ 0.300000000000 QUIL (Coin 0x1111) 0.350000000000 QUIL (Coin 0x2222) 0.350000000000 QUIL (Coin 0x3333) + + **Note:** Coin 0x1111 is the remainder. `, Run: func(cmd *cobra.Command, args []string) { if len(args) < 3 && parts == 1 { From c1cd95c284426734b14374fc0e5ff5f3fbbe1158 Mon Sep 17 00:00:00 2001 From: littleblackcloud <163544315+littleblackcloud@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:27:04 +0100 Subject: [PATCH 4/4] Refactor split command to allow testing of split operations --- client/cmd/split.go | 135 +++++++++++++---------- client/cmd/split_test.go | 232 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 58 deletions(-) create mode 100644 client/cmd/split_test.go diff --git a/client/cmd/split.go b/client/cmd/split.go index 82fa0ea6..98446c6c 100644 --- a/client/cmd/split.go +++ b/client/cmd/split.go @@ -100,75 +100,23 @@ var splitCmd = &cobra.Command{ // Split the coin into the user specified amounts if parts == 1 { - conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) - inputAmount := new(big.Int) - for _, amt := range args[1:] { - amount, err := decimal.NewFromString(amt) - if err != nil { - fmt.Println("invalid amount, must be a decimal number like 0.02 or 2") - os.Exit(1) - } - amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) - inputAmount = inputAmount.Add(inputAmount, amount.BigInt()) - amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) - amounts = append(amounts, amountBytes) - payload = append(payload, amountBytes...) - } - - // Check if the user specified amounts sum to the total amount of the coin - if inputAmount.Cmp(totalAmount) != 0 { - fmt.Println("the specified amounts must sum to the total amount of the coin") + amounts, payload, err = Split(args[1:], amounts, payload, totalAmount) + if err != nil { + fmt.Println(err) os.Exit(1) } } // Split the coin into parts if parts > 1 && partAmount == "" { - amount := new(big.Int).Div(totalAmount, big.NewInt(int64(parts))) - amountBytes := amount.FillBytes(make([]byte, 32)) - for i := int64(0); i < int64(parts); i++ { - amounts = append(amounts, amountBytes) - payload = append(payload, amountBytes...) - } - - // If there is a remainder, we need to add it as a separate amount - // because the amounts must sum to the original coin amount. - remainder := new(big.Int).Mod(totalAmount, big.NewInt(int64(parts))) - if remainder.Cmp(big.NewInt(0)) != 0 { - remainderBytes := remainder.FillBytes(make([]byte, 32)) - amounts = append(amounts, remainderBytes) - payload = append(payload, remainderBytes...) - } + amounts, payload = SplitIntoParts(amounts, payload, totalAmount, parts) } // Split the coin into parts of the user specified amount if parts > 1 && partAmount != "" { - conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) - amount, err := decimal.NewFromString(partAmount) + amounts, payload, err = SplitIntoPartsAmount(amounts, payload, totalAmount, parts, partAmount) if err != nil { - fmt.Println("invalid amount, must be a decimal number like 0.02 or 2") - os.Exit(1) - } - amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) - inputAmount := new(big.Int).Mul(amount.BigInt(), big.NewInt(int64(parts))) - amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) - for i := int64(0); i < int64(parts); i++ { - amounts = append(amounts, amountBytes) - payload = append(payload, amountBytes...) - } - - // If there is a remainder, we need to add it as a separate amount - // because the amounts must sum to the original coin amount. - remainder := new(big.Int).Sub(totalAmount, inputAmount) - if remainder.Cmp(big.NewInt(0)) != 0 { - remainderBytes := remainder.FillBytes(make([]byte, 32)) - amounts = append(amounts, remainderBytes) - payload = append(payload, remainderBytes...) - } - - // Check if the user specified amounts sum to the total amount of the coin - if new(big.Int).Add(inputAmount, new(big.Int).Abs(remainder)).Cmp(totalAmount) != 0 { - fmt.Println("the specified amounts must sum to the total amount of the coin") + fmt.Println(err) os.Exit(1) } } @@ -224,6 +172,77 @@ func init() { tokenCmd.AddCommand(splitCmd) } +func Split(args []string, amounts [][]byte, payload []byte, totalAmount *big.Int) ([][]byte, []byte, error) { + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + inputAmount := new(big.Int) + for _, amt := range args { + amount, err := decimal.NewFromString(amt) + if err != nil { + return nil, nil, fmt.Errorf("invalid amount, must be a decimal number like 0.02 or 2") + } + amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + inputAmount = inputAmount.Add(inputAmount, amount.BigInt()) + amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // Check if the user specified amounts sum to the total amount of the coin + if inputAmount.Cmp(totalAmount) != 0 { + return nil, nil, fmt.Errorf("the specified amounts must sum to the total amount of the coin") + } + return amounts, payload, nil +} + +func SplitIntoParts(amounts [][]byte, payload []byte, totalAmount *big.Int, parts int) ([][]byte, []byte) { + amount := new(big.Int).Div(totalAmount, big.NewInt(int64(parts))) + amountBytes := amount.FillBytes(make([]byte, 32)) + for i := int64(0); i < int64(parts); i++ { + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // If there is a remainder, we need to add it as a separate amount + // because the amounts must sum to the original coin amount. + remainder := new(big.Int).Mod(totalAmount, big.NewInt(int64(parts))) + if remainder.Cmp(big.NewInt(0)) != 0 { + remainderBytes := remainder.FillBytes(make([]byte, 32)) + amounts = append(amounts, remainderBytes) + payload = append(payload, remainderBytes...) + } + return amounts, payload +} + +func SplitIntoPartsAmount(amounts [][]byte, payload []byte, totalAmount *big.Int, parts int, partAmount string) ([][]byte, []byte, error) { + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + amount, err := decimal.NewFromString(partAmount) + if err != nil { + return nil, nil, fmt.Errorf("invalid amount, must be a decimal number like 0.02 or 2") + } + amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + inputAmount := new(big.Int).Mul(amount.BigInt(), big.NewInt(int64(parts))) + amountBytes := amount.BigInt().FillBytes(make([]byte, 32)) + for i := int64(0); i < int64(parts); i++ { + amounts = append(amounts, amountBytes) + payload = append(payload, amountBytes...) + } + + // If there is a remainder, we need to add it as a separate amount + // because the amounts must sum to the original coin amount. + remainder := new(big.Int).Sub(totalAmount, inputAmount) + if remainder.Cmp(big.NewInt(0)) != 0 { + remainderBytes := remainder.FillBytes(make([]byte, 32)) + amounts = append(amounts, remainderBytes) + payload = append(payload, remainderBytes...) + } + + // Check if the user specified amounts sum to the total amount of the coin + if new(big.Int).Add(inputAmount, new(big.Int).Abs(remainder)).Cmp(totalAmount) != 0 { + return nil, nil, fmt.Errorf("the specified amounts must sum to the total amount of the coin") + } + return amounts, payload, nil +} + func getCoinAmount(coinaddr []byte) *big.Int { conn, err := GetGRPCClient() if err != nil { diff --git a/client/cmd/split_test.go b/client/cmd/split_test.go new file mode 100644 index 00000000..ff03241d --- /dev/null +++ b/client/cmd/split_test.go @@ -0,0 +1,232 @@ +package cmd_test + +import ( + "encoding/hex" + "math/big" + "reflect" + "strings" + "testing" + + "github.com/shopspring/decimal" + "source.quilibrium.com/quilibrium/monorepo/client/cmd" +) + +func TestSplit(t *testing.T) { + tests := []struct { + name string + args []string + totalAmount string + amounts [][]byte + payload []byte + expectError bool + }{ + { + name: "Valid split - specified amounts", + args: []string{"0x1234", "0.5", "0.25", "0.25"}, + totalAmount: "1.0", + amounts: [][]byte{ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 40, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0}, + }, + payload: []byte{ + 115, 112, 108, 105, 116, + 18, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 40, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0, + }, + expectError: false, + }, + { + name: "Invalid split - amounts do not sum to the total amount of the coin", + args: []string{"0x1234", "0.5", "0.25"}, + totalAmount: "1.0", + amounts: [][]byte{}, + payload: []byte{}, + expectError: true, + }, + { + name: "Invalid split - amounts exceed total amount of the coin", + args: []string{"0x1234", "0.5", "1"}, + totalAmount: "1.0", + amounts: [][]byte{}, + payload: []byte{}, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + payload := []byte("split") + coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x") + coinaddr, err := hex.DecodeString(coinaddrHex) + if err != nil { + panic(err) + } + payload = append(payload, coinaddr...) + + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + totalAmount, _ := decimal.NewFromString(tc.totalAmount) + totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + + amounts := [][]byte{} + + if tc.expectError { + _, _, err = cmd.Split(tc.args[1:], amounts, payload, totalAmount.BigInt()) + if err == nil { + t.Errorf("want error for invalid split, got nil") + } + } else { + amounts, payload, err = cmd.Split(tc.args[1:], amounts, payload, totalAmount.BigInt()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(tc.amounts, amounts) { + t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts) + } + if !reflect.DeepEqual(tc.payload, payload) { + t.Errorf("expected payloads: %v, got: %v", tc.payload, payload) + } + } + }) + } +} + +func TestSplitParts(t *testing.T) { + tests := []struct { + name string + args []string + parts int + totalAmount string + amounts [][]byte + payload []byte + }{ + { + name: "Valid split - into parts", + args: []string{"0x1234"}, + parts: 3, + totalAmount: "1.0", + amounts: [][]byte{ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, + }, + payload: []byte{ + 115, 112, 108, 105, 116, + 18, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + payload := []byte("split") + coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x") + coinaddr, err := hex.DecodeString(coinaddrHex) + if err != nil { + panic(err) + } + payload = append(payload, coinaddr...) + + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + totalAmount, _ := decimal.NewFromString(tc.totalAmount) + totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + + amounts := [][]byte{} + + amounts, payload = cmd.SplitIntoParts(amounts, payload, totalAmount.BigInt(), tc.parts) + if !reflect.DeepEqual(tc.amounts, amounts) { + t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts) + } + if !reflect.DeepEqual(tc.payload, payload) { + t.Errorf("expected payloads: %v, got: %v", tc.payload, payload) + } + }) + } +} + +func TestSplitIntoPartsAmount(t *testing.T) { + tests := []struct { + name string + args []string + parts int + partAmount string + totalAmount string + amounts [][]byte + payload []byte + expectError bool + }{ + { + name: "Valid split - into parts of specified amount", + args: []string{"0x1234"}, + parts: 2, + partAmount: "0.35", + totalAmount: "1.0", + amounts: [][]byte{ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 13, 24, 0}, + }, + payload: []byte{ + 115, 112, 108, 105, 116, + 18, 52, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 13, 24, 0, + }, + expectError: false, + }, + { + name: "Invalid split - amounts exceed total amount of the coin", + args: []string{"0x1234"}, + parts: 3, + partAmount: "0.5", + totalAmount: "1.0", + amounts: [][]byte{}, + payload: []byte{}, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + payload := []byte("split") + coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x") + coinaddr, err := hex.DecodeString(coinaddrHex) + if err != nil { + panic(err) + } + payload = append(payload, coinaddr...) + + conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) + totalAmount, _ := decimal.NewFromString(tc.totalAmount) + totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0)) + + amounts := [][]byte{} + + if tc.expectError { + _, _, err = cmd.SplitIntoPartsAmount(amounts, payload, totalAmount.BigInt(), tc.parts, tc.partAmount) + if err == nil { + t.Errorf("want error for invalid split, got nil") + } + } else { + amounts, payload, err = cmd.SplitIntoPartsAmount(amounts, payload, totalAmount.BigInt(), tc.parts, tc.partAmount) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(tc.amounts, amounts) { + t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts) + } + if !reflect.DeepEqual(tc.payload, payload) { + t.Errorf("expected payloads: %v, got: %v", tc.payload, payload) + } + } + }) + } +}