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..936e741f --- /dev/null +++ b/data_layer/kaspa/kaspa.go @@ -0,0 +1,231 @@ +package kaspa + +import ( + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "os/exec" + + "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/pterm/pterm" +) + +const ( + ConfigFileName = "kaspa.toml" + MnemonicEntropySize = 256 + requiredKAS = 1 +) + +type Kaspa struct { + Root string + Address string + GrpcAddress string + Network string + ApiUrl string + Mnemonic string +} + +func (k *Kaspa) GetPrivateKey() (string, error) { + return k.Address, 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.Network = "kaspa-mainnet" + } else { + daNetwork = string(consts.KaspaTestnet) + kaspaConfig.Network = "kaspa-testnet-10" + } + + daData, exists := consts.DaNetworks[daNetwork] + if !exists { + pterm.Error.Printf("DA network configuration not found for: %s", daNetwork) + return &kaspaConfig + } + + kaspaConfig.ApiUrl = daData.ApiUrl + kaspaConfig.Root = root + + useExistingGrpcAddress, _ := pterm.DefaultInteractiveConfirm.WithDefaultText( + "would you like to use your own gRPC endpoint??", + ).Show() + + if useExistingGrpcAddress { + kaspaConfig.GrpcAddress, _ = pterm.DefaultInteractiveTextInput.WithDefaultText( + "> Enter your gRPC endpoint", + ).Show() + } else { + kaspaConfig.GrpcAddress = "185.69.54.99: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)) + + 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 + } + + balanceBig := new(big.Int).SetUint64(balance) + if balanceBig.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) + balanceBig := new(big.Int).SetUint64(balance) + if required.Cmp(balanceBig) > 0 { + return []keys.NotFundedAddressData{ + { + KeyName: k.GetKeyName(), + Address: k.Address, + CurrentBalance: balanceBig, + 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) { + return nil, nil +} + +func (k *Kaspa) GetSequencerDAConfig(_ string) string { + return fmt.Sprintf( + `{"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) { +} + +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 +} + +type KaspaBalanceResponse struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` // Đơn vị: sompi +} + +func (k *Kaspa) getBalance() (uint64, error) { + url := fmt.Sprintf("%s/addresses/%s/balance", k.ApiUrl, k.Address) + + resp, err := http.Get(url) + if err != nil { + return 0, fmt.Errorf("failed to call Kaspa API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return 0, fmt.Errorf("Kaspa API returned status: %s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, fmt.Errorf("failed to read response body: %w", err) + } + + var data KaspaBalanceResponse + err = json.Unmarshal(body, &data) + if err != nil { + return 0, fmt.Errorf("failed to parse JSON: %w", err) + } + + return data.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