From a6c6dec374a32c2be271b3a6801d28ddca6dabd0 Mon Sep 17 00:00:00 2001 From: anhductn2001 Date: Wed, 11 Jun 2025 16:49:54 +0700 Subject: [PATCH 1/3] support Kaspa --- cmd/consts/consts.go | 2 + cmd/consts/da.go | 21 +++ cmd/rollapp/setup/setup.go | 18 +++ data_layer/da_layer.go | 9 ++ data_layer/kaspa/kaspa.go | 264 +++++++++++++++++++++++++++++++++++++ data_layer/kaspa/utils.go | 47 +++++++ utils/roller/types.go | 2 +- utils/roller/validation.go | 2 +- 8 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 data_layer/kaspa/kaspa.go create mode 100644 data_layer/kaspa/utils.go diff --git a/cmd/consts/consts.go b/cmd/consts/consts.go index 578f8af1..ef590808 100644 --- a/cmd/consts/consts.go +++ b/cmd/consts/consts.go @@ -135,6 +135,7 @@ var Denoms = struct { LoadNetwork string Bnb string Walrus string + Kaspa string }{ Hub: "adym", HubIbcOnRollapp: "ibc/FECACB927EB3102CCCB240FFB3B6FCCEEB8D944C6FEA8DFF079650FEFF59781D", @@ -145,6 +146,7 @@ var Denoms = struct { LoadNetwork: "tLOAD", Bnb: "ubnb", Walrus: "wal", + Kaspa: "kas", } const ( diff --git a/cmd/consts/da.go b/cmd/consts/da.go index ef37ec8c..6ddbc99a 100644 --- a/cmd/consts/da.go +++ b/cmd/consts/da.go @@ -30,6 +30,7 @@ const ( Sui DAType = "sui" Walrus DAType = "walrus" Mock DAType = "mock" + Kaspa DAType = "kaspa" ) type DaNetwork string @@ -51,6 +52,8 @@ const ( AptosMainnet DaNetwork = "1" WalrusTestnet DaNetwork = "walrus-testnet" WalrusMainnet DaNetwork = "walrus-mainnet" // change this with correct mainnet id + KaspaTestnet DaNetwork = "kaspa-testnet" + KaspaMainnet DaNetwork = "kaspa-mainnet" ) var DaNetworks = map[string]DaData{ @@ -200,4 +203,22 @@ var DaNetworks = map[string]DaData{ StateNodes: []string{}, GasPrice: "", }, + string(KaspaTestnet): { + Backend: Kaspa, + ApiUrl: "https://api-tn10.kaspa.org", + ID: KaspaTestnet, + RpcUrl: "wss://testnet.kaspa.org/ws", + CurrentStateNode: "", + StateNodes: []string{}, + GasPrice: "", + }, + string(KaspaMainnet): { + Backend: Kaspa, + ApiUrl: "https://api.kaspa.org", + ID: KaspaMainnet, + RpcUrl: "wss://mainnet.kaspa.org/ws", + CurrentStateNode: "", + StateNodes: []string{}, + GasPrice: "", + }, } diff --git a/cmd/rollapp/setup/setup.go b/cmd/rollapp/setup/setup.go index f32f8c37..61ee1740 100644 --- a/cmd/rollapp/setup/setup.go +++ b/cmd/rollapp/setup/setup.go @@ -652,6 +652,24 @@ RollApp's IRO time: %v`, return } + // Append DA account address if available + if daAddress != nil { + addresses = append(addresses, keys.KeyInfo{ + Name: damanager.GetKeyName(), + Address: daAddress.Address, + }) + } + case consts.Kaspa: + // Initialize DAManager for Kaspa + damanager := datalayer.NewDAManager(consts.Kaspa, home, localRollerConfig.KeyringBackend, localRollerConfig.NodeType) + + // Retrieve DA account address + daAddress, err := damanager.GetDAAccountAddress() + if err != nil { + pterm.Error.Println("failed to get Kaspa account address: %w", err) + return + } + // Append DA account address if available if daAddress != nil { addresses = append(addresses, keys.KeyInfo{ diff --git a/data_layer/da_layer.go b/data_layer/da_layer.go index a2918a4a..c4aedb04 100644 --- a/data_layer/da_layer.go +++ b/data_layer/da_layer.go @@ -10,6 +10,7 @@ import ( "github.com/dymensionxyz/roller/data_layer/bnb" "github.com/dymensionxyz/roller/data_layer/celestia" "github.com/dymensionxyz/roller/data_layer/damock" + "github.com/dymensionxyz/roller/data_layer/kaspa" loadnetwork "github.com/dymensionxyz/roller/data_layer/loadnetwork" "github.com/dymensionxyz/roller/data_layer/sui" "github.com/dymensionxyz/roller/data_layer/walrus" @@ -60,6 +61,8 @@ func NewDAManager(datype consts.DAType, home string, kb consts.SupportedKeyringB dalayer = bnb.NewBnb(home) case consts.Walrus: dalayer = walrus.NewWalrus(home) + case consts.Kaspa: + dalayer = kaspa.NewKaspa(home) case consts.Local: dalayer = &damock.DAMock{} default: @@ -92,6 +95,8 @@ func GetDaInfo(env, daBackend string) (*consts.DaData, error) { daNetwork = string(consts.BnbTestnet) case string(consts.Walrus): daNetwork = string(consts.WalrusTestnet) + case string(consts.Kaspa): + daNetwork = string(consts.KaspaTestnet) default: return nil, fmt.Errorf("unsupported DA backend: %s", daBackend) } @@ -111,6 +116,8 @@ func GetDaInfo(env, daBackend string) (*consts.DaData, error) { daNetwork = string(consts.BnbMainnet) case string(consts.Walrus): daNetwork = string(consts.WalrusMainnet) + case string(consts.Kaspa): + daNetwork = string(consts.KaspaMainnet) default: return nil, fmt.Errorf("unsupported DA backend: %s", daBackend) } @@ -130,6 +137,8 @@ func GetDaInfo(env, daBackend string) (*consts.DaData, error) { daNetwork = string(consts.BnbTestnet) case string(consts.Walrus): daNetwork = string(consts.WalrusTestnet) + case string(consts.Kaspa): + daNetwork = string(consts.KaspaTestnet) default: return nil, fmt.Errorf("unsupported DA backend: %s", daBackend) } diff --git a/data_layer/kaspa/kaspa.go b/data_layer/kaspa/kaspa.go new file mode 100644 index 00000000..034b476a --- /dev/null +++ b/data_layer/kaspa/kaspa.go @@ -0,0 +1,264 @@ +package kaspa + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "os" + "os/exec" + + cosmossdkmath "cosmossdk.io/math" + cosmossdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/roller/cmd/consts" + "github.com/dymensionxyz/roller/utils/errorhandling" + "github.com/dymensionxyz/roller/utils/keys" + "github.com/dymensionxyz/roller/utils/roller" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/pterm/pterm" +) + +const ( + ConfigFileName = "kaspa.toml" + MnemonicEntropySize = 256 + requiredKAS = 1 +) + +type Kaspa struct { + Root string + PrivateKey string + Address string + RpcEndpoint string + ChainID uint32 + GrpcAddress string + Network string + ApiUrl string +} + +func (k *Kaspa) GetPrivateKey() (string, error) { + return k.PrivateKey, nil +} + +func (k *Kaspa) SetMetricsEndpoint(endpoint string) { +} + +func NewKaspa(root string) *Kaspa { + var daNetwork string + + rollerData, err := roller.LoadConfig(root) + errorhandling.PrettifyErrorIfExists(err) + + cfgPath := GetCfgFilePath(root) + kaspaConfig, err := loadConfigFromTOML(cfgPath) + if err != nil { + if rollerData.HubData.Environment == "mainnet" { + daNetwork = string(consts.KaspaMainnet) + kaspaConfig.ChainID = 1 + kaspaConfig.Network = "mainnet" + } else { + daNetwork = string(consts.KaspaTestnet) + kaspaConfig.ChainID = 2 + kaspaConfig.Network = "testnet" + } + + daData, exists := consts.DaNetworks[daNetwork] + if !exists { + pterm.Error.Printf("DA network configuration not found for: %s", daNetwork) + return &kaspaConfig + } + + kaspaConfig.RpcEndpoint = daData.RpcUrl + kaspaConfig.ApiUrl = daData.ApiUrl + kaspaConfig.GrpcAddress = "localhost:16210" + kaspaConfig.Root = root + + // Check for mnemonic in environment variable + mnemonic := os.Getenv("KASPA_MNEMONIC") + if mnemonic != "" { + // Use the provided mnemonic + kaspaConfig.PrivateKey = mnemonic + // For now, we'll use the provided address directly + kaspaConfig.Address = "kaspatest:qzwyrgapjnhtjqkxdrmp7fpm3yddw296v2ajv9nmgmw5k3z0r38guevxyk7j0" + } else { + useExistingKaspaWallet, _ := pterm.DefaultInteractiveConfirm.WithDefaultText( + "would you like to import an existing Kaspa wallet?", + ).Show() + + if useExistingKaspaWallet { + kaspaConfig.PrivateKey, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( + "> Enter your hex private key", + ).Show() + privateKey, err := crypto.HexToECDSA(kaspaConfig.PrivateKey) + if err != nil { + pterm.Error.Println("failed to parse private key from hex", err) + } + publicKey := privateKey.Public().(*ecdsa.PublicKey) + address := crypto.PubkeyToAddress(*publicKey).Hex() + + kaspaConfig.Address = address + } else { + privateKey, err := crypto.GenerateKey() + if err != nil { + pterm.Error.Println("failed to generate new private key", err) + } + + privateKeyBytes := crypto.FromECDSA(privateKey) + privateKeyHex := hex.EncodeToString(privateKeyBytes) + kaspaConfig.PrivateKey = privateKeyHex + + fmt.Printf("\t%s\n", kaspaConfig.PrivateKey) + fmt.Println() + fmt.Println(pterm.LightYellow("๐Ÿ’ก save this information and keep it safe")) + + publicKey := privateKey.Public().(*ecdsa.PublicKey) + address := crypto.PubkeyToAddress(*publicKey).Hex() + kaspaConfig.Address = address + } + } + + pterm.DefaultSection.WithIndentCharacter("๐Ÿ””").Println("Please fund your Kaspa addresses below") + pterm.DefaultBasicText.Println(pterm.LightGreen(kaspaConfig.Address)) + + for { + proceed, _ := pterm.DefaultInteractiveConfirm.WithDefaultValue(false). + WithDefaultText( + "press 'y' when the wallet is funded", + ).Show() + + if !proceed { + pterm.Error.Println("Kaspa addr needs to be funded") + continue + } + + balance, err := kaspaConfig.getBalance() + if err != nil { + pterm.Println("Error getting balance:", err) + continue + } + + if balance.Cmp(big.NewInt(0)) > 0 { + pterm.Println("Wallet funded with balance:", balance) + break + } + pterm.Error.Println("Kaspa wallet needs to be funded") + } + + err = writeConfigToTOML(cfgPath, kaspaConfig) + if err != nil { + panic(err) + } + } + return &kaspaConfig +} + +func (k *Kaspa) InitializeLightNodeConfig() (string, error) { + return "", nil +} + +func (k *Kaspa) GetDAAccountAddress() (*keys.KeyInfo, error) { + return &keys.KeyInfo{ + Address: k.Address, + }, nil +} + +func (k *Kaspa) GetRootDirectory() string { + return k.Root +} + +func (k *Kaspa) CheckDABalance() ([]keys.NotFundedAddressData, error) { + balance, err := k.getBalance() + if err != nil { + return nil, fmt.Errorf("failed to get DA balance: %w", err) + } + + exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(8), nil) // Kaspa has 8 decimals + required := new(big.Int).Mul(big.NewInt(requiredKAS), exp) + if required.Cmp(balance) > 0 { + return []keys.NotFundedAddressData{ + { + KeyName: k.GetKeyName(), + Address: k.Address, + CurrentBalance: balance, + RequiredBalance: required, + Denom: "KAS", + Network: string(consts.Kaspa), + }, + }, nil + } + return nil, nil +} + +func (k *Kaspa) GetStartDACmd() *exec.Cmd { + return nil +} + +func (k *Kaspa) GetDAAccData(cfg roller.RollappConfig) ([]keys.AccountData, error) { + balance, err := k.getBalance() + if err != nil { + return nil, err + } + + return []keys.AccountData{ + { + Address: k.Address, + Balance: cosmossdktypes.Coin{ + Denom: "KAS", + Amount: cosmossdkmath.NewIntFromBigInt(balance), + }, + }, + }, nil +} + +func (k *Kaspa) GetSequencerDAConfig(_ string) string { + return fmt.Sprintf( + `{"address": "%s", "api_url": "%s", "grpc_address": "%s", "network": "%s"}`, + k.Address, + k.ApiUrl, + k.GrpcAddress, + k.Network, + ) +} + +func (k *Kaspa) SetRPCEndpoint(rpc string) { + k.RpcEndpoint = rpc +} + +func (k *Kaspa) GetLightNodeEndpoint() string { + return "" +} + +func (k *Kaspa) GetNetworkName() string { + return "kaspa" +} + +func (k *Kaspa) GetStatus(c roller.RollappConfig) string { + return "Active" +} + +func (k *Kaspa) GetKeyName() string { + return "kaspa" +} + +func (k *Kaspa) GetNamespaceID() string { + return "" +} + +func (k *Kaspa) GetAppID() uint32 { + return 0 +} + +func (k *Kaspa) getBalance() (*big.Int, error) { + client, err := ethclient.Dial(k.RpcEndpoint) + if err != nil { + return nil, err + } + balance, err := client.BalanceAt(context.Background(), common.HexToAddress(k.Address), nil) + if err != nil { + return nil, err + } + + return balance, nil +} diff --git a/data_layer/kaspa/utils.go b/data_layer/kaspa/utils.go new file mode 100644 index 00000000..38d6d0af --- /dev/null +++ b/data_layer/kaspa/utils.go @@ -0,0 +1,47 @@ +package kaspa + +import ( + "os" + "path/filepath" + + "github.com/pelletier/go-toml" + + "github.com/dymensionxyz/roller/cmd/consts" +) + +func writeConfigToTOML(path string, c Kaspa) error { + tomlBytes, err := toml.Marshal(c) + if err != nil { + return err + } + dir := filepath.Dir(path) + // nolint:gofumpt + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + // nolint:gofumpt + err = os.WriteFile(path, tomlBytes, 0o644) + if err != nil { + return err + } + + return nil +} + +func loadConfigFromTOML(path string) (Kaspa, error) { + var config Kaspa + tomlBytes, err := os.ReadFile(path) + if err != nil { + return config, err + } + err = toml.Unmarshal(tomlBytes, &config) + if err != nil { + return config, err + } + + return config, nil +} + +func GetCfgFilePath(rollerHome string) string { + return filepath.Join(rollerHome, consts.ConfigDirName.DALightNode, ConfigFileName) +} diff --git a/utils/roller/types.go b/utils/roller/types.go index bd59d2db..cee85a33 100644 --- a/utils/roller/types.go +++ b/utils/roller/types.go @@ -2,7 +2,7 @@ package roller import "github.com/dymensionxyz/roller/cmd/consts" -var SupportedDas = []consts.DAType{consts.Celestia, consts.Avail, consts.LoadNetwork, consts.Bnb, consts.Aptos, consts.Sui, consts.Walrus, consts.Local} +var SupportedDas = []consts.DAType{consts.Celestia, consts.Avail, consts.LoadNetwork, consts.Bnb, consts.Aptos, consts.Sui, consts.Walrus, consts.Local, consts.Kaspa} type RollappConfig struct { // new roller.toml diff --git a/utils/roller/validation.go b/utils/roller/validation.go index a96f8a11..7c58e833 100644 --- a/utils/roller/validation.go +++ b/utils/roller/validation.go @@ -10,7 +10,7 @@ import ( func IsValidDAType(t string) bool { switch consts.DAType(t) { - case consts.Local, consts.Celestia, consts.Avail, consts.LoadNetwork, consts.Bnb, consts.Aptos, consts.Sui, consts.Walrus: + case consts.Local, consts.Celestia, consts.Avail, consts.LoadNetwork, consts.Bnb, consts.Aptos, consts.Sui, consts.Walrus, consts.Kaspa: return true } return false From 2f6af67f2bdeced96d558643d118e3320c661357 Mon Sep 17 00:00:00 2001 From: anhductn2001 Date: Tue, 17 Jun 2025 09:20:35 +0700 Subject: [PATCH 2/3] support Kaspa --- data_layer/kaspa/kaspa.go | 167 +++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 84 deletions(-) diff --git a/data_layer/kaspa/kaspa.go b/data_layer/kaspa/kaspa.go index 034b476a..cd162470 100644 --- a/data_layer/kaspa/kaspa.go +++ b/data_layer/kaspa/kaspa.go @@ -1,23 +1,18 @@ package kaspa import ( - "context" - "crypto/ecdsa" - "encoding/hex" + "bytes" + "encoding/json" "fmt" + "io" "math/big" - "os" + "net/http" "os/exec" - cosmossdkmath "cosmossdk.io/math" - cosmossdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/dymensionxyz/roller/cmd/consts" "github.com/dymensionxyz/roller/utils/errorhandling" "github.com/dymensionxyz/roller/utils/keys" "github.com/dymensionxyz/roller/utils/roller" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" "github.com/pterm/pterm" ) @@ -29,17 +24,15 @@ const ( type Kaspa struct { Root string - PrivateKey string Address string - RpcEndpoint string - ChainID uint32 GrpcAddress string Network string ApiUrl string + Mnemonic string } func (k *Kaspa) GetPrivateKey() (string, error) { - return k.PrivateKey, nil + return k.Address, nil } func (k *Kaspa) SetMetricsEndpoint(endpoint string) { @@ -56,11 +49,9 @@ func NewKaspa(root string) *Kaspa { if err != nil { if rollerData.HubData.Environment == "mainnet" { daNetwork = string(consts.KaspaMainnet) - kaspaConfig.ChainID = 1 kaspaConfig.Network = "mainnet" } else { daNetwork = string(consts.KaspaTestnet) - kaspaConfig.ChainID = 2 kaspaConfig.Network = "testnet" } @@ -70,55 +61,25 @@ func NewKaspa(root string) *Kaspa { return &kaspaConfig } - kaspaConfig.RpcEndpoint = daData.RpcUrl kaspaConfig.ApiUrl = daData.ApiUrl - kaspaConfig.GrpcAddress = "localhost:16210" kaspaConfig.Root = root - // Check for mnemonic in environment variable - mnemonic := os.Getenv("KASPA_MNEMONIC") - if mnemonic != "" { - // Use the provided mnemonic - kaspaConfig.PrivateKey = mnemonic - // For now, we'll use the provided address directly - kaspaConfig.Address = "kaspatest:qzwyrgapjnhtjqkxdrmp7fpm3yddw296v2ajv9nmgmw5k3z0r38guevxyk7j0" - } else { - useExistingKaspaWallet, _ := pterm.DefaultInteractiveConfirm.WithDefaultText( - "would you like to import an existing Kaspa wallet?", - ).Show() + useExistingGrpcAddress, _ := pterm.DefaultInteractiveConfirm.WithDefaultText( + "would you like to use your own gRPC endpoint??", + ).Show() - if useExistingKaspaWallet { - kaspaConfig.PrivateKey, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( - "> Enter your hex private key", - ).Show() - privateKey, err := crypto.HexToECDSA(kaspaConfig.PrivateKey) - if err != nil { - pterm.Error.Println("failed to parse private key from hex", err) - } - publicKey := privateKey.Public().(*ecdsa.PublicKey) - address := crypto.PubkeyToAddress(*publicKey).Hex() - - kaspaConfig.Address = address - } else { - privateKey, err := crypto.GenerateKey() - if err != nil { - pterm.Error.Println("failed to generate new private key", err) - } - - privateKeyBytes := crypto.FromECDSA(privateKey) - privateKeyHex := hex.EncodeToString(privateKeyBytes) - kaspaConfig.PrivateKey = privateKeyHex - - fmt.Printf("\t%s\n", kaspaConfig.PrivateKey) - fmt.Println() - fmt.Println(pterm.LightYellow("๐Ÿ’ก save this information and keep it safe")) - - publicKey := privateKey.Public().(*ecdsa.PublicKey) - address := crypto.PubkeyToAddress(*publicKey).Hex() - kaspaConfig.Address = address - } + if useExistingGrpcAddress { + kaspaConfig.GrpcAddress, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( + "> Enter your gRPC endpoint", + ).Show() + } else { + kaspaConfig.GrpcAddress = "localhost:16210" } + kaspaConfig.Address, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( + "> Enter your Kaspa Address", + ).Show() + pterm.DefaultSection.WithIndentCharacter("๐Ÿ””").Println("Please fund your Kaspa addresses below") pterm.DefaultBasicText.Println(pterm.LightGreen(kaspaConfig.Address)) @@ -139,7 +100,8 @@ func NewKaspa(root string) *Kaspa { continue } - if balance.Cmp(big.NewInt(0)) > 0 { + balanceBig := new(big.Int).SetUint64(balance) + if balanceBig.Cmp(big.NewInt(0)) > 0 { pterm.Println("Wallet funded with balance:", balance) break } @@ -176,12 +138,13 @@ func (k *Kaspa) CheckDABalance() ([]keys.NotFundedAddressData, error) { exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(8), nil) // Kaspa has 8 decimals required := new(big.Int).Mul(big.NewInt(requiredKAS), exp) - if required.Cmp(balance) > 0 { + balanceBig := new(big.Int).SetUint64(balance) + if required.Cmp(balanceBig) > 0 { return []keys.NotFundedAddressData{ { KeyName: k.GetKeyName(), Address: k.Address, - CurrentBalance: balance, + CurrentBalance: balanceBig, RequiredBalance: required, Denom: "KAS", Network: string(consts.Kaspa), @@ -196,34 +159,20 @@ func (k *Kaspa) GetStartDACmd() *exec.Cmd { } func (k *Kaspa) GetDAAccData(cfg roller.RollappConfig) ([]keys.AccountData, error) { - balance, err := k.getBalance() - if err != nil { - return nil, err - } - - return []keys.AccountData{ - { - Address: k.Address, - Balance: cosmossdktypes.Coin{ - Denom: "KAS", - Amount: cosmossdkmath.NewIntFromBigInt(balance), - }, - }, - }, nil + return nil, nil } func (k *Kaspa) GetSequencerDAConfig(_ string) string { return fmt.Sprintf( - `{"address": "%s", "api_url": "%s", "grpc_address": "%s", "network": "%s"}`, - k.Address, + `{"api_url":"%s","grpc_address":"%s","network":"%s","address":"%s","mnemonic_env":"KASPA_MNEMONIC"}`, k.ApiUrl, k.GrpcAddress, k.Network, + k.Address, ) } func (k *Kaspa) SetRPCEndpoint(rpc string) { - k.RpcEndpoint = rpc } func (k *Kaspa) GetLightNodeEndpoint() string { @@ -250,15 +199,65 @@ func (k *Kaspa) GetAppID() uint32 { return 0 } -func (k *Kaspa) getBalance() (*big.Int, error) { - client, err := ethclient.Dial(k.RpcEndpoint) +type GetUtxosParams struct { + Address string `json:"address"` +} + +type JsonRpcRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params GetUtxosParams `json:"params"` + ID int `json:"id"` +} + +type UTXO struct { + Amount uint64 `json:"amount"` +} + +type JsonRpcResponse struct { + Result struct { + Entries []UTXO `json:"entries"` + } `json:"result"` + Error interface{} `json:"error"` +} + +func (k *Kaspa) getBalance() (uint64, error) { + reqBody := JsonRpcRequest{ + Jsonrpc: "2.0", + Method: "getUtxosByAddress", + Params: GetUtxosParams{Address: k.Address}, + ID: 1, + } + + body, err := json.Marshal(reqBody) + if err != nil { + return 0, err + } + + resp, err := http.Post(k.ApiUrl, "application/json", bytes.NewBuffer(body)) if err != nil { - return nil, err + return 0, err } - balance, err := client.BalanceAt(context.Background(), common.HexToAddress(k.Address), nil) + defer resp.Body.Close() + + respData, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return 0, err + } + + var rpcResp JsonRpcResponse + if err := json.Unmarshal(respData, &rpcResp); err != nil { + return 0, err + } + + if rpcResp.Error != nil { + return 0, fmt.Errorf("RPC error: %v", rpcResp.Error) + } + + var total uint64 = 0 + for _, utxo := range rpcResp.Result.Entries { + total += utxo.Amount } - return balance, nil + return total, nil } From 70276ceabfaeddf3ade53f06a0a6b20e4dbdeb90 Mon Sep 17 00:00:00 2001 From: anhductn2001 Date: Fri, 11 Jul 2025 08:01:39 +0700 Subject: [PATCH 3/3] update get balance --- data_layer/kaspa/kaspa.go | 68 +++++++++++---------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/data_layer/kaspa/kaspa.go b/data_layer/kaspa/kaspa.go index cd162470..936e741f 100644 --- a/data_layer/kaspa/kaspa.go +++ b/data_layer/kaspa/kaspa.go @@ -1,7 +1,6 @@ package kaspa import ( - "bytes" "encoding/json" "fmt" "io" @@ -49,10 +48,10 @@ func NewKaspa(root string) *Kaspa { if err != nil { if rollerData.HubData.Environment == "mainnet" { daNetwork = string(consts.KaspaMainnet) - kaspaConfig.Network = "mainnet" + kaspaConfig.Network = "kaspa-mainnet" } else { daNetwork = string(consts.KaspaTestnet) - kaspaConfig.Network = "testnet" + kaspaConfig.Network = "kaspa-testnet-10" } daData, exists := consts.DaNetworks[daNetwork] @@ -73,7 +72,7 @@ func NewKaspa(root string) *Kaspa { "> Enter your gRPC endpoint", ).Show() } else { - kaspaConfig.GrpcAddress = "localhost:16210" + kaspaConfig.GrpcAddress = "185.69.54.99:16210" } kaspaConfig.Address, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( @@ -199,65 +198,34 @@ func (k *Kaspa) GetAppID() uint32 { return 0 } -type GetUtxosParams struct { +type KaspaBalanceResponse struct { Address string `json:"address"` -} - -type JsonRpcRequest struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - Params GetUtxosParams `json:"params"` - ID int `json:"id"` -} - -type UTXO struct { - Amount uint64 `json:"amount"` -} - -type JsonRpcResponse struct { - Result struct { - Entries []UTXO `json:"entries"` - } `json:"result"` - Error interface{} `json:"error"` + Balance uint64 `json:"balance"` // ฤฦกn vแป‹: sompi } func (k *Kaspa) getBalance() (uint64, error) { - reqBody := JsonRpcRequest{ - Jsonrpc: "2.0", - Method: "getUtxosByAddress", - Params: GetUtxosParams{Address: k.Address}, - ID: 1, - } + url := fmt.Sprintf("%s/addresses/%s/balance", k.ApiUrl, k.Address) - body, err := json.Marshal(reqBody) + resp, err := http.Get(url) if err != nil { - return 0, err - } - - resp, err := http.Post(k.ApiUrl, "application/json", bytes.NewBuffer(body)) - if err != nil { - return 0, err + return 0, fmt.Errorf("failed to call Kaspa API: %w", err) } defer resp.Body.Close() - respData, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err + if resp.StatusCode != 200 { + return 0, fmt.Errorf("Kaspa API returned status: %s", resp.Status) } - var rpcResp JsonRpcResponse - if err := json.Unmarshal(respData, &rpcResp); err != nil { - return 0, err - } - - if rpcResp.Error != nil { - return 0, fmt.Errorf("RPC error: %v", rpcResp.Error) + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, fmt.Errorf("failed to read response body: %w", err) } - var total uint64 = 0 - for _, utxo := range rpcResp.Result.Entries { - total += utxo.Amount + var data KaspaBalanceResponse + err = json.Unmarshal(body, &data) + if err != nil { + return 0, fmt.Errorf("failed to parse JSON: %w", err) } - return total, nil + return data.Balance, nil }