Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ var Denoms = struct {
LoadNetwork string
Bnb string
Walrus string
Kaspa string
}{
Hub: "adym",
HubIbcOnRollapp: "ibc/FECACB927EB3102CCCB240FFB3B6FCCEEB8D944C6FEA8DFF079650FEFF59781D",
Expand All @@ -145,6 +146,7 @@ var Denoms = struct {
LoadNetwork: "tLOAD",
Bnb: "ubnb",
Walrus: "wal",
Kaspa: "kas",
}

const (
Expand Down
21 changes: 21 additions & 0 deletions cmd/consts/da.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
Sui DAType = "sui"
Walrus DAType = "walrus"
Mock DAType = "mock"
Kaspa DAType = "kaspa"
)

type DaNetwork string
Expand All @@ -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{
Expand Down Expand Up @@ -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: "",
},
}
18 changes: 18 additions & 0 deletions cmd/rollapp/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
9 changes: 9 additions & 0 deletions data_layer/da_layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
231 changes: 231 additions & 0 deletions data_layer/kaspa/kaspa.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading