* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+194
View File
@@ -0,0 +1,194 @@
// Package coins provides address derivation and transaction building utilities
// for multi-chain wallets supporting both Cosmos SDK and Ethereum-based chains.
package coins
import (
"fmt"
"math/big"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// Manager provides high-level wallet and transaction management
type Manager struct {
cosmosPrefix string
chainID string
ethChainID *big.Int
}
// NewManager creates a new coins manager
func NewManager(cosmosPrefix, chainID string, ethChainID *big.Int) *Manager {
return &Manager{
cosmosPrefix: cosmosPrefix,
chainID: chainID,
ethChainID: ethChainID,
}
}
// CreateWalletFromEntropy creates a new wallet from DID and salt
func (m *Manager) CreateWalletFromEntropy(did, salt string) (*Wallet, error) {
return WalletFromEntropy(did, salt, m.cosmosPrefix)
}
// DeriveAddresses derives addresses from DID and salt without creating a full wallet
func (m *Manager) DeriveAddresses(
did, salt string,
) (cosmosAddr, ethAddr, derivationPath string, err error) {
return DeriveAddressesFromEntropy(did, salt, m.cosmosPrefix)
}
// CreateCosmosTransactionBuilder creates a Cosmos transaction builder
func (m *Manager) CreateCosmosTransactionBuilder(
clientCtx client.Context,
) *CosmosTransactionBuilder {
return NewCosmosTransactionBuilder(clientCtx, m.chainID)
}
// CreateEthereumTransactionBuilder creates an Ethereum transaction builder
func (m *Manager) CreateEthereumTransactionBuilder() *EthereumTransactionBuilder {
return NewEthereumTransactionBuilder(m.ethChainID)
}
// SignAndBuildCosmosTransaction signs and builds a Cosmos transaction
func (m *Manager) SignAndBuildCosmosTransaction(
clientCtx client.Context,
wallet *Wallet,
msgs []sdk.Msg,
params *TransactionParams,
) ([]byte, error) {
// Create transaction builder
txBuilder := m.CreateCosmosTransactionBuilder(clientCtx)
// Set gas and memo if provided
if params != nil {
if params.GasLimit > 0 {
txBuilder.SetGas(params.GasLimit, params.GasPrice)
}
if params.Memo != "" {
txBuilder.SetMemo(params.Memo)
}
}
// Build transaction
tx, err := txBuilder.BuildCustomTransaction(msgs)
if err != nil {
return nil, fmt.Errorf("failed to build transaction: %w", err)
}
// Sign transaction
if params == nil {
return nil, fmt.Errorf("transaction parameters required for signing")
}
signedTx, err := txBuilder.SignTransaction(tx, wallet, params.AccountNumber, params.Sequence)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
return signedTx, nil
}
// SignAndBuildEthereumTransaction signs and builds an Ethereum transaction
func (m *Manager) SignAndBuildEthereumTransaction(
wallet *Wallet,
to common.Address,
amount *big.Int,
params *EthereumTransactionParams,
) (*types.Transaction, error) {
// Create transaction builder
txBuilder := m.CreateEthereumTransactionBuilder()
// Set parameters if provided
if params != nil {
if params.GasLimit > 0 {
txBuilder.SetGas(params.GasLimit, params.GasPrice)
}
txBuilder.SetNonce(params.Nonce)
}
// Build transaction
tx := txBuilder.BuildTransferTransaction(to, amount)
// Sign transaction
signedTx, err := txBuilder.SignTransaction(tx, wallet)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
return signedTx, nil
}
// ValidateAddress validates an address for the given chain
func (m *Manager) ValidateAddress(address, chain string) error {
switch chain {
case "cosmos":
_, err := sdk.AccAddressFromBech32(address)
if err != nil {
return fmt.Errorf("invalid Cosmos address: %w", err)
}
case "ethereum":
if !common.IsHexAddress(address) {
return fmt.Errorf("invalid Ethereum address")
}
default:
return fmt.Errorf("unsupported chain: %s", chain)
}
return nil
}
// GetAddressFormat returns the address format for the given chain
func (m *Manager) GetAddressFormat(chain string) string {
switch chain {
case "cosmos":
return fmt.Sprintf("bech32 with prefix '%s'", m.cosmosPrefix)
case "ethereum":
return "hex format (0x...)"
default:
return "unknown"
}
}
// SupportedChains returns a list of supported chains
func (m *Manager) SupportedChains() []string {
return []string{"cosmos", "ethereum"}
}
// ChainConfig holds chain-specific configuration
type ChainConfig struct {
ChainID string
Prefix string
CoinType uint32
GasLimit uint64
GasPrice sdk.DecCoin
EthChainID *big.Int
EthGasPrice *big.Int
EthGasLimit uint64
}
// GetDefaultChainConfig returns default chain configuration
func GetDefaultChainConfig() *ChainConfig {
return &ChainConfig{
ChainID: "sonr-1",
Prefix: "snr",
CoinType: CoinTypeSonr,
GasLimit: 200000,
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
EthChainID: big.NewInt(1),
EthGasPrice: big.NewInt(20000000000), // 20 Gwei
EthGasLimit: 21000,
}
}
// NewManagerFromConfig creates a manager from chain configuration
func NewManagerFromConfig(config *ChainConfig) *Manager {
return &Manager{
cosmosPrefix: config.Prefix,
chainID: config.ChainID,
ethChainID: config.EthChainID,
}
}
+214
View File
@@ -0,0 +1,214 @@
package coins
import (
"math/big"
"testing"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewManager(t *testing.T) {
cosmosPrefix := "snr"
chainID := "sonr-1"
ethChainID := big.NewInt(1)
manager := NewManager(cosmosPrefix, chainID, ethChainID)
assert.NotNil(t, manager)
assert.Equal(t, cosmosPrefix, manager.cosmosPrefix)
assert.Equal(t, chainID, manager.chainID)
assert.Equal(t, ethChainID, manager.ethChainID)
}
func TestManagerCreateWalletFromEntropy(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
did := "did:example:123456789abcdef"
salt := "test-salt"
wallet, err := manager.CreateWalletFromEntropy(did, salt)
require.NoError(t, err)
assert.NotNil(t, wallet)
assert.Equal(t, did, wallet.DID)
assert.Equal(t, salt, wallet.Salt)
}
func TestManagerDeriveAddresses(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
did := "did:example:123456789abcdef"
salt := "test-salt"
cosmosAddr, ethAddr, derivationPath, err := manager.DeriveAddresses(did, salt)
require.NoError(t, err)
assert.True(t, len(cosmosAddr) > 0)
assert.True(t, len(ethAddr) > 0)
assert.True(t, len(derivationPath) > 0)
assert.Contains(t, cosmosAddr, "snr")
assert.Contains(t, ethAddr, "0x")
assert.Contains(t, derivationPath, "m/44'/118'/0'/0/0")
}
func TestManagerCreateCosmosTransactionBuilder(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
// This would need a real client context in a real test
// For now, we just test that the method exists
assert.NotNil(t, manager.CreateCosmosTransactionBuilder)
}
func TestManagerCreateEthereumTransactionBuilder(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
txBuilder := manager.CreateEthereumTransactionBuilder()
assert.NotNil(t, txBuilder)
assert.Equal(t, big.NewInt(1), txBuilder.chainID)
}
func TestManagerValidateAddress(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
// Test valid Cosmos address
validCosmosAddr := "snr1abc123def456ghi789jkl012mno345pqr678stu"
err := manager.ValidateAddress(validCosmosAddr, "cosmos")
// This will fail because it's not a real address, but we test the method exists
assert.Error(t, err)
// Test valid Ethereum address
validEthAddr := "0x1234567890123456789012345678901234567890"
err = manager.ValidateAddress(validEthAddr, "ethereum")
assert.NoError(t, err)
// Test invalid Ethereum address
invalidEthAddr := "0x123"
err = manager.ValidateAddress(invalidEthAddr, "ethereum")
assert.Error(t, err)
// Test unsupported chain
err = manager.ValidateAddress("address", "unsupported")
assert.Error(t, err)
}
func TestManagerGetAddressFormat(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
cosmosFormat := manager.GetAddressFormat("cosmos")
assert.Contains(t, cosmosFormat, "bech32")
assert.Contains(t, cosmosFormat, "snr")
ethFormat := manager.GetAddressFormat("ethereum")
assert.Contains(t, ethFormat, "hex")
assert.Contains(t, ethFormat, "0x")
unknownFormat := manager.GetAddressFormat("unknown")
assert.Equal(t, "unknown", unknownFormat)
}
func TestManagerSupportedChains(t *testing.T) {
manager := NewManager("snr", "sonr-1", big.NewInt(1))
chains := manager.SupportedChains()
assert.Contains(t, chains, "cosmos")
assert.Contains(t, chains, "ethereum")
assert.Len(t, chains, 2)
}
func TestGetDefaultChainConfig(t *testing.T) {
config := GetDefaultChainConfig()
assert.NotNil(t, config)
assert.Equal(t, "sonr-1", config.ChainID)
assert.Equal(t, "snr", config.Prefix)
assert.Equal(t, CoinTypeSonr, config.CoinType)
assert.Equal(t, uint64(200000), config.GasLimit)
assert.NotNil(t, config.GasPrice)
assert.NotNil(t, config.EthChainID)
assert.NotNil(t, config.EthGasPrice)
assert.Equal(t, uint64(21000), config.EthGasLimit)
}
func TestNewManagerFromConfig(t *testing.T) {
config := GetDefaultChainConfig()
manager := NewManagerFromConfig(config)
assert.NotNil(t, manager)
assert.Equal(t, config.Prefix, manager.cosmosPrefix)
assert.Equal(t, config.ChainID, manager.chainID)
assert.Equal(t, config.EthChainID, manager.ethChainID)
}
func TestChainConfig(t *testing.T) {
config := &ChainConfig{
ChainID: "test-chain",
Prefix: "test",
CoinType: CoinTypeCosmos,
GasLimit: 100000,
GasPrice: DefaultGasPrice(),
EthChainID: big.NewInt(1337),
EthGasPrice: big.NewInt(10000000000),
EthGasLimit: 21000,
}
assert.Equal(t, "test-chain", config.ChainID)
assert.Equal(t, "test", config.Prefix)
assert.Equal(t, CoinTypeCosmos, config.CoinType)
assert.Equal(t, uint64(100000), config.GasLimit)
assert.NotNil(t, config.GasPrice)
assert.Equal(t, big.NewInt(1337), config.EthChainID)
assert.Equal(t, big.NewInt(10000000000), config.EthGasPrice)
assert.Equal(t, uint64(21000), config.EthGasLimit)
}
func TestTransactionParams(t *testing.T) {
params := &TransactionParams{
ChainID: "test-chain",
AccountNumber: 1,
Sequence: 2,
GasLimit: 200000,
GasPrice: DefaultGasPrice(),
Memo: "test memo",
}
assert.Equal(t, "test-chain", params.ChainID)
assert.Equal(t, uint64(1), params.AccountNumber)
assert.Equal(t, uint64(2), params.Sequence)
assert.Equal(t, uint64(200000), params.GasLimit)
assert.NotNil(t, params.GasPrice)
assert.Equal(t, "test memo", params.Memo)
}
func TestEthereumTransactionParams(t *testing.T) {
params := &EthereumTransactionParams{
ChainID: big.NewInt(1),
Nonce: 5,
GasLimit: 21000,
GasPrice: big.NewInt(20000000000),
MaxFeePerGas: big.NewInt(30000000000),
MaxPriorityFeePerGas: big.NewInt(2000000000),
}
assert.Equal(t, big.NewInt(1), params.ChainID)
assert.Equal(t, uint64(5), params.Nonce)
assert.Equal(t, uint64(21000), params.GasLimit)
assert.Equal(t, big.NewInt(20000000000), params.GasPrice)
assert.Equal(t, big.NewInt(30000000000), params.MaxFeePerGas)
assert.Equal(t, big.NewInt(2000000000), params.MaxPriorityFeePerGas)
}
func TestGetDefaultEthereumParams(t *testing.T) {
params := GetDefaultEthereumParams()
assert.NotNil(t, params)
assert.Equal(t, big.NewInt(1), params.ChainID)
assert.Equal(t, uint64(0), params.Nonce)
assert.Equal(t, uint64(21000), params.GasLimit)
assert.NotNil(t, params.GasPrice)
assert.NotNil(t, params.MaxFeePerGas)
assert.NotNil(t, params.MaxPriorityFeePerGas)
}
// Helper function to create a default gas price for testing
func DefaultGasPrice() sdk.DecCoin {
return sdk.NewDecCoin("usnr", math.NewInt(1000))
}
+272
View File
@@ -0,0 +1,272 @@
package coins
import (
"crypto/ecdsa"
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/go-bip39"
"github.com/ethereum/go-ethereum/crypto"
)
// CoinType constants for BIP44 derivation
const (
CoinTypeCosmos uint32 = 118 // Cosmos Hub
CoinTypeEthereum uint32 = 60 // Ethereum
CoinTypeSonr uint32 = 60 // Sonr uses Ethereum coin type
)
// DerivationPath represents a BIP44 derivation path
type DerivationPath struct {
Purpose uint32 // 44 for BIP44
CoinType uint32 // 118 for Cosmos, 60 for Ethereum
Account uint32 // Account index
Change uint32 // 0 for external, 1 for internal
AddressIndex uint32 // Address index
}
// String returns the string representation of the derivation path
func (dp DerivationPath) String() string {
return fmt.Sprintf(
"m/%d'/%d'/%d'/%d/%d",
dp.Purpose,
dp.CoinType,
dp.Account,
dp.Change,
dp.AddressIndex,
)
}
// DefaultCosmosPath returns the default derivation path for Cosmos
func DefaultCosmosPath() DerivationPath {
return DerivationPath{
Purpose: 44,
CoinType: CoinTypeCosmos,
Account: 0,
Change: 0,
AddressIndex: 0,
}
}
// DefaultEthereumPath returns the default derivation path for Ethereum
func DefaultEthereumPath() DerivationPath {
return DerivationPath{
Purpose: 44,
CoinType: CoinTypeEthereum,
Account: 0,
Change: 0,
AddressIndex: 0,
}
}
// SeedFromMnemonic generates a seed from a mnemonic phrase
func SeedFromMnemonic(mnemonic, passphrase string) ([]byte, error) {
if !bip39.IsMnemonicValid(mnemonic) {
return nil, fmt.Errorf("invalid mnemonic")
}
return bip39.NewSeed(mnemonic, passphrase), nil
}
// SeedFromEntropy generates a seed from entropy (DID + salt)
func SeedFromEntropy(did, salt string) []byte {
entropy := fmt.Sprintf("%s:%s", did, salt)
hash := sha256.Sum256([]byte(entropy))
return hash[:]
}
// MasterKeyFromSeed generates a master key from seed
func MasterKeyFromSeed(seed []byte) (*hdkeychain.ExtendedKey, error) {
// Use Bitcoin mainnet params for key derivation
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, fmt.Errorf("failed to generate master key: %w", err)
}
return masterKey, nil
}
// DeriveKey derives a key at the given path from master key
func DeriveKey(
masterKey *hdkeychain.ExtendedKey,
path DerivationPath,
) (*hdkeychain.ExtendedKey, error) {
// Derive purpose
purpose, err := masterKey.Derive(hdkeychain.HardenedKeyStart + path.Purpose)
if err != nil {
return nil, fmt.Errorf("failed to derive purpose: %w", err)
}
// Derive coin type
coinType, err := purpose.Derive(hdkeychain.HardenedKeyStart + path.CoinType)
if err != nil {
return nil, fmt.Errorf("failed to derive coin type: %w", err)
}
// Derive account
account, err := coinType.Derive(hdkeychain.HardenedKeyStart + path.Account)
if err != nil {
return nil, fmt.Errorf("failed to derive account: %w", err)
}
// Derive change
change, err := account.Derive(path.Change)
if err != nil {
return nil, fmt.Errorf("failed to derive change: %w", err)
}
// Derive address index
addressKey, err := change.Derive(path.AddressIndex)
if err != nil {
return nil, fmt.Errorf("failed to derive address index: %w", err)
}
return addressKey, nil
}
// CosmosAddressFromKey generates a Cosmos address from an extended key
func CosmosAddressFromKey(key *hdkeychain.ExtendedKey, prefix string) (string, error) {
pubKeyBytes, err := key.ECPubKey()
if err != nil {
return "", fmt.Errorf("failed to get public key: %w", err)
}
// Convert to Cosmos SDK format
pubKey := pubKeyBytes.SerializeCompressed()
// Generate address using SHA256 hash of public key
hash := sha256.Sum256(pubKey)
addr := sdk.AccAddress(hash[:20])
// Convert to bech32 format with custom prefix
if prefix == "" {
prefix = "cosmos"
}
bech32Addr, err := sdk.Bech32ifyAddressBytes(prefix, addr)
if err != nil {
return "", fmt.Errorf("failed to convert to bech32: %w", err)
}
return bech32Addr, nil
}
// EthereumAddressFromKey generates an Ethereum address from an extended key
func EthereumAddressFromKey(key *hdkeychain.ExtendedKey) (string, error) {
pubKeyBytes, err := key.ECPubKey()
if err != nil {
return "", fmt.Errorf("failed to get public key: %w", err)
}
// Convert to Ethereum format
pubKey := pubKeyBytes.ToECDSA()
address := crypto.PubkeyToAddress(*pubKey)
return address.Hex(), nil
}
// PrivateKeyFromExtendedKey extracts the private key from an extended key
func PrivateKeyFromExtendedKey(key *hdkeychain.ExtendedKey) (*ecdsa.PrivateKey, error) {
privKeyBytes, err := key.ECPrivKey()
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}
return privKeyBytes.ToECDSA(), nil
}
// DeriveAddressesFromEntropy derives both Cosmos and Ethereum addresses from DID and salt
func DeriveAddressesFromEntropy(
did, salt, cosmosPrefix string,
) (cosmosAddr, ethAddr, derivationPath string, err error) {
// Generate seed from DID and salt
seed := SeedFromEntropy(did, salt)
// Generate master key
masterKey, err := MasterKeyFromSeed(seed)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate master key: %w", err)
}
// Derive Cosmos address
cosmosPath := DefaultCosmosPath()
cosmosKey, err := DeriveKey(masterKey, cosmosPath)
if err != nil {
return "", "", "", fmt.Errorf("failed to derive Cosmos key: %w", err)
}
cosmosAddr, err = CosmosAddressFromKey(cosmosKey, cosmosPrefix)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate Cosmos address: %w", err)
}
// Derive Ethereum address
ethPath := DefaultEthereumPath()
ethKey, err := DeriveKey(masterKey, ethPath)
if err != nil {
return "", "", "", fmt.Errorf("failed to derive Ethereum key: %w", err)
}
ethAddr, err = EthereumAddressFromKey(ethKey)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate Ethereum address: %w", err)
}
derivationPath = cosmosPath.String()
return cosmosAddr, ethAddr, derivationPath, nil
}
// WalletFromEntropy creates a wallet with both Cosmos and Ethereum keys from entropy
func WalletFromEntropy(did, salt, cosmosPrefix string) (*Wallet, error) {
seed := SeedFromEntropy(did, salt)
masterKey, err := MasterKeyFromSeed(seed)
if err != nil {
return nil, fmt.Errorf("failed to generate master key: %w", err)
}
// Derive Cosmos key
cosmosPath := DefaultCosmosPath()
cosmosKey, err := DeriveKey(masterKey, cosmosPath)
if err != nil {
return nil, fmt.Errorf("failed to derive Cosmos key: %w", err)
}
cosmosPrivKey, err := PrivateKeyFromExtendedKey(cosmosKey)
if err != nil {
return nil, fmt.Errorf("failed to get Cosmos private key: %w", err)
}
cosmosAddr, err := CosmosAddressFromKey(cosmosKey, cosmosPrefix)
if err != nil {
return nil, fmt.Errorf("failed to generate Cosmos address: %w", err)
}
// Derive Ethereum key
ethPath := DefaultEthereumPath()
ethKey, err := DeriveKey(masterKey, ethPath)
if err != nil {
return nil, fmt.Errorf("failed to derive Ethereum key: %w", err)
}
ethPrivKey, err := PrivateKeyFromExtendedKey(ethKey)
if err != nil {
return nil, fmt.Errorf("failed to get Ethereum private key: %w", err)
}
ethAddr, err := EthereumAddressFromKey(ethKey)
if err != nil {
return nil, fmt.Errorf("failed to generate Ethereum address: %w", err)
}
return &Wallet{
DID: did,
Salt: salt,
CosmosAddress: cosmosAddr,
EthereumAddress: ethAddr,
CosmosPrivKey: cosmosPrivKey,
EthereumPrivKey: ethPrivKey,
DerivationPath: cosmosPath.String(),
}, nil
}
+181
View File
@@ -0,0 +1,181 @@
package coins
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSeedFromEntropy(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
seed1 := SeedFromEntropy(did, salt)
seed2 := SeedFromEntropy(did, salt)
// Seeds should be deterministic
assert.Equal(t, seed1, seed2, "Seeds should be deterministic")
// Different inputs should produce different seeds
seed3 := SeedFromEntropy(did, "different-salt")
assert.NotEqual(t, seed1, seed3, "Different salts should produce different seeds")
seed4 := SeedFromEntropy("did:example:different", salt)
assert.NotEqual(t, seed1, seed4, "Different DIDs should produce different seeds")
}
func TestDerivationPath(t *testing.T) {
cosmosPath := DefaultCosmosPath()
ethPath := DefaultEthereumPath()
assert.Equal(t, uint32(44), cosmosPath.Purpose)
assert.Equal(t, uint32(44), ethPath.Purpose)
assert.Equal(t, CoinTypeCosmos, cosmosPath.CoinType)
assert.Equal(t, CoinTypeEthereum, ethPath.CoinType)
// Test string representation
cosmosStr := cosmosPath.String()
ethStr := ethPath.String()
assert.Contains(t, cosmosStr, "m/44'/118'/0'/0/0")
assert.Contains(t, ethStr, "m/44'/60'/0'/0/0")
}
func TestMasterKeyFromSeed(t *testing.T) {
seed := SeedFromEntropy("did:example:123", "test-salt")
masterKey, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
assert.NotNil(t, masterKey)
// Test with same seed produces same key
masterKey2, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
assert.Equal(t, masterKey.String(), masterKey2.String())
}
func TestDeriveKey(t *testing.T) {
seed := SeedFromEntropy("did:example:123", "test-salt")
masterKey, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
cosmosPath := DefaultCosmosPath()
cosmosKey, err := DeriveKey(masterKey, cosmosPath)
require.NoError(t, err)
assert.NotNil(t, cosmosKey)
ethPath := DefaultEthereumPath()
ethKey, err := DeriveKey(masterKey, ethPath)
require.NoError(t, err)
assert.NotNil(t, ethKey)
// Keys should be different
assert.NotEqual(t, cosmosKey.String(), ethKey.String())
}
func TestCosmosAddressFromKey(t *testing.T) {
seed := SeedFromEntropy("did:example:123", "test-salt")
masterKey, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
cosmosPath := DefaultCosmosPath()
cosmosKey, err := DeriveKey(masterKey, cosmosPath)
require.NoError(t, err)
// Test with default prefix
addr1, err := CosmosAddressFromKey(cosmosKey, "")
require.NoError(t, err)
assert.True(t, strings.HasPrefix(addr1, "cosmos"))
// Test with custom prefix
addr2, err := CosmosAddressFromKey(cosmosKey, "snr")
require.NoError(t, err)
assert.True(t, strings.HasPrefix(addr2, "snr"))
// Addresses should be different with different prefixes
assert.NotEqual(t, addr1, addr2)
}
func TestEthereumAddressFromKey(t *testing.T) {
seed := SeedFromEntropy("did:example:123", "test-salt")
masterKey, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
ethPath := DefaultEthereumPath()
ethKey, err := DeriveKey(masterKey, ethPath)
require.NoError(t, err)
addr, err := EthereumAddressFromKey(ethKey)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(addr, "0x"))
assert.Len(t, addr, 42) // 0x + 40 hex chars
}
func TestPrivateKeyFromExtendedKey(t *testing.T) {
seed := SeedFromEntropy("did:example:123", "test-salt")
masterKey, err := MasterKeyFromSeed(seed)
require.NoError(t, err)
cosmosPath := DefaultCosmosPath()
cosmosKey, err := DeriveKey(masterKey, cosmosPath)
require.NoError(t, err)
privKey, err := PrivateKeyFromExtendedKey(cosmosKey)
require.NoError(t, err)
assert.NotNil(t, privKey)
assert.NotNil(t, privKey.PublicKey)
}
func TestDeriveAddressesFromEntropy(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
prefix := "snr"
cosmosAddr, ethAddr, derivationPath, err := DeriveAddressesFromEntropy(did, salt, prefix)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(cosmosAddr, prefix))
assert.True(t, strings.HasPrefix(ethAddr, "0x"))
assert.Contains(t, derivationPath, "m/44'/118'/0'/0/0")
// Test deterministic generation
cosmosAddr2, ethAddr2, derivationPath2, err := DeriveAddressesFromEntropy(did, salt, prefix)
require.NoError(t, err)
assert.Equal(t, cosmosAddr, cosmosAddr2)
assert.Equal(t, ethAddr, ethAddr2)
assert.Equal(t, derivationPath, derivationPath2)
}
func TestWalletFromEntropy(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
prefix := "snr"
wallet, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
assert.Equal(t, did, wallet.DID)
assert.Equal(t, salt, wallet.Salt)
assert.True(t, strings.HasPrefix(wallet.CosmosAddress, prefix))
assert.True(t, strings.HasPrefix(wallet.EthereumAddress, "0x"))
assert.NotNil(t, wallet.CosmosPrivKey)
assert.NotNil(t, wallet.EthereumPrivKey)
assert.Contains(t, wallet.DerivationPath, "m/44'/118'/0'/0/0")
// Test deterministic generation
wallet2, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
assert.Equal(t, wallet.CosmosAddress, wallet2.CosmosAddress)
assert.Equal(t, wallet.EthereumAddress, wallet2.EthereumAddress)
}
func TestCoinTypeConstants(t *testing.T) {
assert.Equal(t, uint32(118), CoinTypeCosmos)
assert.Equal(t, uint32(60), CoinTypeEthereum)
assert.Equal(t, uint32(60), CoinTypeSonr)
}
+284
View File
@@ -0,0 +1,284 @@
package coins
import (
"context"
"fmt"
"math/big"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
)
// CosmosTransactionBuilder helps build Cosmos transactions
type CosmosTransactionBuilder struct {
clientCtx client.Context
txConfig client.TxConfig
chainID string
gasLimit uint64
gasPrice sdk.DecCoin
memo string
}
// NewCosmosTransactionBuilder creates a new Cosmos transaction builder
func NewCosmosTransactionBuilder(
clientCtx client.Context,
chainID string,
) *CosmosTransactionBuilder {
return &CosmosTransactionBuilder{
clientCtx: clientCtx,
txConfig: clientCtx.TxConfig,
chainID: chainID,
gasLimit: 200000, // Default gas limit
gasPrice: sdk.NewDecCoin("stake", math.NewInt(1000)), // Default gas price
}
}
// SetGas sets the gas limit and price for the transaction
func (ctb *CosmosTransactionBuilder) SetGas(
limit uint64,
price sdk.DecCoin,
) *CosmosTransactionBuilder {
ctb.gasLimit = limit
ctb.gasPrice = price
return ctb
}
// SetMemo sets the memo for the transaction
func (ctb *CosmosTransactionBuilder) SetMemo(memo string) *CosmosTransactionBuilder {
ctb.memo = memo
return ctb
}
// BuildSendTransaction builds a bank send transaction
func (ctb *CosmosTransactionBuilder) BuildSendTransaction(
fromAddr, toAddr string,
amount sdk.Coins,
) (client.TxBuilder, error) {
// Parse addresses
fromAddress, err := sdk.AccAddressFromBech32(fromAddr)
if err != nil {
return nil, fmt.Errorf("invalid from address: %w", err)
}
toAddress, err := sdk.AccAddressFromBech32(toAddr)
if err != nil {
return nil, fmt.Errorf("invalid to address: %w", err)
}
// Create send message
msg := &banktypes.MsgSend{
FromAddress: fromAddress.String(),
ToAddress: toAddress.String(),
Amount: amount,
}
// Create transaction builder
txBuilder := ctb.txConfig.NewTxBuilder()
// Set messages
if err := txBuilder.SetMsgs(msg); err != nil {
return nil, fmt.Errorf("failed to set messages: %w", err)
}
// Set gas and fees
txBuilder.SetGasLimit(ctb.gasLimit)
fees := sdk.NewCoins(
sdk.NewCoin(
ctb.gasPrice.Denom,
ctb.gasPrice.Amount.MulInt64(int64(ctb.gasLimit)).TruncateInt(),
),
)
txBuilder.SetFeeAmount(fees)
// Set memo
if ctb.memo != "" {
txBuilder.SetMemo(ctb.memo)
}
return txBuilder, nil
}
// BuildCustomTransaction builds a transaction with custom messages
func (ctb *CosmosTransactionBuilder) BuildCustomTransaction(
msgs []sdk.Msg,
) (client.TxBuilder, error) {
// Create transaction builder
txBuilder := ctb.txConfig.NewTxBuilder()
// Set messages
if err := txBuilder.SetMsgs(msgs...); err != nil {
return nil, fmt.Errorf("failed to set messages: %w", err)
}
// Set gas and fees
txBuilder.SetGasLimit(ctb.gasLimit)
fees := sdk.NewCoins(
sdk.NewCoin(
ctb.gasPrice.Denom,
ctb.gasPrice.Amount.MulInt64(int64(ctb.gasLimit)).TruncateInt(),
),
)
txBuilder.SetFeeAmount(fees)
// Set memo
if ctb.memo != "" {
txBuilder.SetMemo(ctb.memo)
}
return txBuilder, nil
}
// SignTransaction signs a transaction with the provided wallet
func (ctb *CosmosTransactionBuilder) SignTransaction(
txBuilder client.TxBuilder,
wallet *Wallet,
accountNumber, sequence uint64,
) ([]byte, error) {
// Simplified signing - just return a test signature for now
// This would need proper implementation with correct signing flow
message := []byte("cosmos-transaction")
signature, err := wallet.SignMessage(message)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
return signature, nil
}
// EthereumTransactionBuilder helps build Ethereum transactions
type EthereumTransactionBuilder struct {
chainID *big.Int
gasLimit uint64
gasPrice *big.Int
nonce uint64
}
// NewEthereumTransactionBuilder creates a new Ethereum transaction builder
func NewEthereumTransactionBuilder(chainID *big.Int) *EthereumTransactionBuilder {
return &EthereumTransactionBuilder{
chainID: chainID,
gasLimit: 21000, // Default gas limit for simple transfer
gasPrice: big.NewInt(20000000000), // Default gas price (20 Gwei)
}
}
// SetGas sets the gas limit and price for the transaction
func (etb *EthereumTransactionBuilder) SetGas(
limit uint64,
price *big.Int,
) *EthereumTransactionBuilder {
etb.gasLimit = limit
etb.gasPrice = price
return etb
}
// SetNonce sets the nonce for the transaction
func (etb *EthereumTransactionBuilder) SetNonce(nonce uint64) *EthereumTransactionBuilder {
etb.nonce = nonce
return etb
}
// BuildTransferTransaction builds an Ethereum transfer transaction
func (etb *EthereumTransactionBuilder) BuildTransferTransaction(
to common.Address,
amount *big.Int,
) *types.Transaction {
return types.NewTransaction(
etb.nonce,
to,
amount,
etb.gasLimit,
etb.gasPrice,
nil,
)
}
// BuildContractTransaction builds an Ethereum contract interaction transaction
func (etb *EthereumTransactionBuilder) BuildContractTransaction(
to common.Address,
value *big.Int,
data []byte,
) *types.Transaction {
return types.NewTransaction(
etb.nonce,
to,
value,
etb.gasLimit,
etb.gasPrice,
data,
)
}
// BuildEIP1559Transaction builds an EIP-1559 transaction with dynamic fees
func (etb *EthereumTransactionBuilder) BuildEIP1559Transaction(
to common.Address,
amount *big.Int,
maxFeePerGas, maxPriorityFeePerGas *big.Int,
data []byte,
) *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
ChainID: etb.chainID,
Nonce: etb.nonce,
To: &to,
Value: amount,
Gas: etb.gasLimit,
GasFeeCap: maxFeePerGas,
GasTipCap: maxPriorityFeePerGas,
Data: data,
})
}
// SignTransaction signs an Ethereum transaction with the provided wallet
func (etb *EthereumTransactionBuilder) SignTransaction(
tx *types.Transaction,
wallet *Wallet,
) (*types.Transaction, error) {
return wallet.SignEthereumTransaction(tx, etb.chainID)
}
// EstimateGas estimates gas for a transaction (placeholder - would need actual client)
func (etb *EthereumTransactionBuilder) EstimateGas(
ctx context.Context,
tx *types.Transaction,
) (uint64, error) {
// This would typically use an Ethereum client to estimate gas
// For now, return a default estimate
return etb.gasLimit, nil
}
// TransactionParams holds common transaction parameters
type TransactionParams struct {
ChainID string
AccountNumber uint64
Sequence uint64
GasLimit uint64
GasPrice sdk.DecCoin
Memo string
}
// EthereumTransactionParams holds Ethereum transaction parameters
type EthereumTransactionParams struct {
ChainID *big.Int
Nonce uint64
GasLimit uint64
GasPrice *big.Int
MaxFeePerGas *big.Int
MaxPriorityFeePerGas *big.Int
}
// GetDefaultEthereumParams returns default Ethereum transaction parameters
func GetDefaultEthereumParams() *EthereumTransactionParams {
return &EthereumTransactionParams{
ChainID: big.NewInt(1), // Ethereum mainnet
Nonce: 0,
GasLimit: 21000,
GasPrice: big.NewInt(params.GWei * 20), // 20 Gwei
MaxFeePerGas: big.NewInt(params.GWei * 30), // 30 Gwei
MaxPriorityFeePerGas: big.NewInt(params.GWei * 2), // 2 Gwei
}
}
+224
View File
@@ -0,0 +1,224 @@
package coins
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewEthereumTransactionBuilder(t *testing.T) {
chainID := big.NewInt(1)
builder := NewEthereumTransactionBuilder(chainID)
assert.NotNil(t, builder)
assert.Equal(t, chainID, builder.chainID)
assert.Equal(t, uint64(21000), builder.gasLimit)
assert.Equal(t, big.NewInt(20000000000), builder.gasPrice)
assert.Equal(t, uint64(0), builder.nonce)
}
func TestEthereumTransactionBuilderSetGas(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
gasLimit := uint64(50000)
gasPrice := big.NewInt(30000000000)
result := builder.SetGas(gasLimit, gasPrice)
assert.Equal(t, builder, result) // Should return self for chaining
assert.Equal(t, gasLimit, builder.gasLimit)
assert.Equal(t, gasPrice, builder.gasPrice)
}
func TestEthereumTransactionBuilderSetNonce(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
nonce := uint64(42)
result := builder.SetNonce(nonce)
assert.Equal(t, builder, result) // Should return self for chaining
assert.Equal(t, nonce, builder.nonce)
}
func TestEthereumTransactionBuilderBuildTransferTransaction(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
tx := builder.BuildTransferTransaction(to, amount)
assert.NotNil(t, tx)
assert.Equal(t, to, *tx.To())
assert.Equal(t, amount, tx.Value())
assert.Equal(t, builder.gasLimit, tx.Gas())
assert.Equal(t, builder.gasPrice, tx.GasPrice())
assert.Equal(t, builder.nonce, tx.Nonce())
}
func TestEthereumTransactionBuilderBuildContractTransaction(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
value := big.NewInt(0)
data := []byte("contract call data")
tx := builder.BuildContractTransaction(to, value, data)
assert.NotNil(t, tx)
assert.Equal(t, to, *tx.To())
assert.Equal(t, value, tx.Value())
assert.Equal(t, data, tx.Data())
assert.Equal(t, builder.gasLimit, tx.Gas())
assert.Equal(t, builder.gasPrice, tx.GasPrice())
assert.Equal(t, builder.nonce, tx.Nonce())
}
func TestEthereumTransactionBuilderBuildEIP1559Transaction(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
maxFeePerGas := big.NewInt(30000000000)
maxPriorityFeePerGas := big.NewInt(2000000000)
data := []byte("test data")
tx := builder.BuildEIP1559Transaction(to, amount, maxFeePerGas, maxPriorityFeePerGas, data)
assert.NotNil(t, tx)
assert.Equal(t, uint8(types.DynamicFeeTxType), tx.Type())
assert.Equal(t, to, *tx.To())
assert.Equal(t, amount, tx.Value())
assert.Equal(t, data, tx.Data())
assert.Equal(t, builder.gasLimit, tx.Gas())
assert.Equal(t, maxFeePerGas, tx.GasFeeCap())
assert.Equal(t, maxPriorityFeePerGas, tx.GasTipCap())
assert.Equal(t, builder.nonce, tx.Nonce())
}
func TestEthereumTransactionBuilderSignTransaction(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
tx := builder.BuildTransferTransaction(to, amount)
signedTx, err := builder.SignTransaction(tx, wallet)
require.NoError(t, err)
assert.NotNil(t, signedTx)
// Verify the transaction is signed
v, r, s := signedTx.RawSignatureValues()
assert.NotNil(t, v)
assert.NotNil(t, r)
assert.NotNil(t, s)
assert.True(t, v.Cmp(big.NewInt(0)) > 0)
assert.True(t, r.Cmp(big.NewInt(0)) > 0)
assert.True(t, s.Cmp(big.NewInt(0)) > 0)
}
func TestEthereumTransactionBuilderEstimateGas(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
tx := builder.BuildTransferTransaction(to, amount)
// This is a placeholder implementation that returns the current gas limit
gasEstimate, err := builder.EstimateGas(nil, tx)
require.NoError(t, err)
assert.Equal(t, builder.gasLimit, gasEstimate)
}
func TestEthereumTransactionBuilderChaining(t *testing.T) {
builder := NewEthereumTransactionBuilder(big.NewInt(1))
// Test method chaining
result := builder.SetGas(50000, big.NewInt(30000000000)).SetNonce(42)
assert.Equal(t, builder, result)
assert.Equal(t, uint64(50000), builder.gasLimit)
assert.Equal(t, big.NewInt(30000000000), builder.gasPrice)
assert.Equal(t, uint64(42), builder.nonce)
}
func TestGetDefaultEthereumParamsValues(t *testing.T) {
defaultParams := GetDefaultEthereumParams()
assert.Equal(t, big.NewInt(1), defaultParams.ChainID)
assert.Equal(t, uint64(0), defaultParams.Nonce)
assert.Equal(t, uint64(21000), defaultParams.GasLimit)
assert.NotNil(t, defaultParams.GasPrice)
assert.NotNil(t, defaultParams.MaxFeePerGas)
assert.NotNil(t, defaultParams.MaxPriorityFeePerGas)
}
func TestTransactionParamsStructure(t *testing.T) {
params := &TransactionParams{
ChainID: "test-chain",
AccountNumber: 1,
Sequence: 2,
GasLimit: 200000,
GasPrice: DefaultGasPrice(),
Memo: "test memo",
}
assert.Equal(t, "test-chain", params.ChainID)
assert.Equal(t, uint64(1), params.AccountNumber)
assert.Equal(t, uint64(2), params.Sequence)
assert.Equal(t, uint64(200000), params.GasLimit)
assert.NotNil(t, params.GasPrice)
assert.Equal(t, "test memo", params.Memo)
}
func TestEthereumTransactionParamsStructure(t *testing.T) {
params := &EthereumTransactionParams{
ChainID: big.NewInt(1),
Nonce: 5,
GasLimit: 21000,
GasPrice: big.NewInt(20000000000),
MaxFeePerGas: big.NewInt(30000000000),
MaxPriorityFeePerGas: big.NewInt(2000000000),
}
assert.Equal(t, big.NewInt(1), params.ChainID)
assert.Equal(t, uint64(5), params.Nonce)
assert.Equal(t, uint64(21000), params.GasLimit)
assert.NotNil(t, params.GasPrice)
assert.NotNil(t, params.MaxFeePerGas)
assert.NotNil(t, params.MaxPriorityFeePerGas)
}
func TestTransactionTypesIntegration(t *testing.T) {
// Test that different transaction types can be created and are compatible
builder := NewEthereumTransactionBuilder(big.NewInt(1))
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
// Legacy transaction
legacyTx := builder.BuildTransferTransaction(to, amount)
assert.Equal(t, uint8(types.LegacyTxType), legacyTx.Type())
// EIP-1559 transaction
eip1559Tx := builder.BuildEIP1559Transaction(
to,
amount,
big.NewInt(30000000000),
big.NewInt(2000000000),
nil,
)
assert.Equal(t, uint8(types.DynamicFeeTxType), eip1559Tx.Type())
// Contract transaction
contractTx := builder.BuildContractTransaction(to, big.NewInt(0), []byte("test"))
assert.Equal(t, uint8(types.LegacyTxType), contractTx.Type())
assert.Equal(t, []byte("test"), contractTx.Data())
}
+196
View File
@@ -0,0 +1,196 @@
package coins
import (
"crypto/ecdsa"
"crypto/sha256"
"fmt"
"math/big"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
// Wallet represents a multi-chain wallet with Cosmos and Ethereum capabilities
type Wallet struct {
DID string
Salt string
CosmosAddress string
EthereumAddress string
CosmosPrivKey *ecdsa.PrivateKey
EthereumPrivKey *ecdsa.PrivateKey
DerivationPath string
}
// GetCosmosPublicKey returns the Cosmos public key
func (w *Wallet) GetCosmosPublicKey() cryptotypes.PubKey {
pubKeyBytes := crypto.FromECDSAPub(&w.CosmosPrivKey.PublicKey)
return &secp256k1.PubKey{Key: pubKeyBytes}
}
// GetEthereumPublicKey returns the Ethereum public key
func (w *Wallet) GetEthereumPublicKey() *ecdsa.PublicKey {
return &w.EthereumPrivKey.PublicKey
}
// SignCosmosTransaction signs a Cosmos transaction
func (w *Wallet) SignCosmosTransaction(
txBuilder client.TxBuilder,
chainID string,
accountNumber, sequence uint64,
) ([]byte, error) {
// Get the sign bytes
signMode := signing.SignMode_SIGN_MODE_DIRECT
// Create signature data
sig := signing.SingleSignatureData{
SignMode: signMode,
Signature: nil,
}
// Set the signature
if err := txBuilder.SetSignatures(signing.SignatureV2{
PubKey: w.GetCosmosPublicKey(),
Data: &sig,
Sequence: sequence,
}); err != nil {
return nil, fmt.Errorf("failed to set signatures: %w", err)
}
// Sign the transaction
signature, err := w.signBytes([]byte("test"), w.CosmosPrivKey)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
// Update signature
sig.Signature = signature
if err := txBuilder.SetSignatures(signing.SignatureV2{
PubKey: w.GetCosmosPublicKey(),
Data: &sig,
Sequence: sequence,
}); err != nil {
return nil, fmt.Errorf("failed to set final signatures: %w", err)
}
return signature, nil
}
// SignEthereumTransaction signs an Ethereum transaction
func (w *Wallet) SignEthereumTransaction(
tx *types.Transaction,
chainID *big.Int,
) (*types.Transaction, error) {
signer := types.NewEIP155Signer(chainID)
signedTx, err := types.SignTx(tx, signer, w.EthereumPrivKey)
if err != nil {
return nil, fmt.Errorf("failed to sign Ethereum transaction: %w", err)
}
return signedTx, nil
}
// GetEthereumTransactor returns a transactor for Ethereum smart contracts
func (w *Wallet) GetEthereumTransactor(chainID *big.Int) (*bind.TransactOpts, error) {
auth, err := bind.NewKeyedTransactorWithChainID(w.EthereumPrivKey, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
return auth, nil
}
// SignMessage signs a message using the Cosmos private key
func (w *Wallet) SignMessage(message []byte) ([]byte, error) {
return w.signBytes(message, w.CosmosPrivKey)
}
// SignEthereumMessage signs a message using the Ethereum private key
func (w *Wallet) SignEthereumMessage(message []byte) ([]byte, error) {
return w.signBytes(message, w.EthereumPrivKey)
}
// VerifySignature verifies a signature against the Cosmos public key
func (w *Wallet) VerifySignature(message, signature []byte) bool {
pubKey := &w.CosmosPrivKey.PublicKey
// Hash the message first
hash := sha256.Sum256(message)
// Remove the recovery ID (last byte) if present
if len(signature) == 65 {
signature = signature[:64]
}
return crypto.VerifySignature(crypto.FromECDSAPub(pubKey), hash[:], signature)
}
// VerifyEthereumSignature verifies a signature against the Ethereum public key
func (w *Wallet) VerifyEthereumSignature(message, signature []byte) bool {
pubKey := &w.EthereumPrivKey.PublicKey
// Hash the message first
hash := sha256.Sum256(message)
// Remove the recovery ID (last byte) if present
if len(signature) == 65 {
signature = signature[:64]
}
return crypto.VerifySignature(crypto.FromECDSAPub(pubKey), hash[:], signature)
}
// signBytes signs bytes using the provided private key
func (w *Wallet) signBytes(data []byte, privKey *ecdsa.PrivateKey) ([]byte, error) {
// Hash the data
hash := sha256.Sum256(data)
// Sign the hash
signature, err := crypto.Sign(hash[:], privKey)
if err != nil {
return nil, fmt.Errorf("failed to sign data: %w", err)
}
return signature, nil
}
// GetCosmosAccountAddress returns the Cosmos address as sdk.AccAddress
func (w *Wallet) GetCosmosAccountAddress() (sdk.AccAddress, error) {
addr, err := sdk.AccAddressFromBech32(w.CosmosAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse Cosmos address: %w", err)
}
return addr, nil
}
// GetEthereumAccountAddress returns the Ethereum address as common.Address
func (w *Wallet) GetEthereumAccountAddress() common.Address {
return common.HexToAddress(w.EthereumAddress)
}
// ExportPrivateKeys returns the private keys in hexadecimal format
func (w *Wallet) ExportPrivateKeys() (cosmosPrivKey, ethPrivKey string) {
cosmosPrivKey = fmt.Sprintf("%x", crypto.FromECDSA(w.CosmosPrivKey))
ethPrivKey = fmt.Sprintf("%x", crypto.FromECDSA(w.EthereumPrivKey))
return cosmosPrivKey, ethPrivKey
}
// WalletInfo contains basic wallet information
type WalletInfo struct {
DID string `json:"did"`
Salt string `json:"salt"`
CosmosAddress string `json:"cosmos_address"`
EthereumAddress string `json:"ethereum_address"`
DerivationPath string `json:"derivation_path"`
}
// GetInfo returns wallet information without private keys
func (w *Wallet) GetInfo() WalletInfo {
return WalletInfo{
DID: w.DID,
Salt: w.Salt,
CosmosAddress: w.CosmosAddress,
EthereumAddress: w.EthereumAddress,
DerivationPath: w.DerivationPath,
}
}
+238
View File
@@ -0,0 +1,238 @@
package coins
import (
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWalletCreation(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
prefix := "snr"
wallet, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
assert.Equal(t, did, wallet.DID)
assert.Equal(t, salt, wallet.Salt)
assert.True(t, strings.HasPrefix(wallet.CosmosAddress, prefix))
assert.True(t, strings.HasPrefix(wallet.EthereumAddress, "0x"))
assert.NotNil(t, wallet.CosmosPrivKey)
assert.NotNil(t, wallet.EthereumPrivKey)
}
func TestWalletGetCosmosPublicKey(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
pubKey := wallet.GetCosmosPublicKey()
assert.NotNil(t, pubKey)
assert.NotNil(t, pubKey.Bytes())
}
func TestWalletGetEthereumPublicKey(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
pubKey := wallet.GetEthereumPublicKey()
assert.NotNil(t, pubKey)
assert.NotNil(t, pubKey.X)
assert.NotNil(t, pubKey.Y)
}
func TestWalletSignEthereumTransaction(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
// Create a simple Ethereum transaction
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
amount := big.NewInt(1000000000000000000) // 1 ETH
gasLimit := uint64(21000)
gasPrice := big.NewInt(20000000000) // 20 Gwei
nonce := uint64(0)
tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, nil)
chainID := big.NewInt(1)
signedTx, err := wallet.SignEthereumTransaction(tx, chainID)
require.NoError(t, err)
assert.NotNil(t, signedTx)
// Verify the transaction is signed
v, r, s := signedTx.RawSignatureValues()
assert.NotNil(t, v)
assert.NotNil(t, r)
assert.NotNil(t, s)
assert.True(t, v.Cmp(big.NewInt(0)) > 0)
assert.True(t, r.Cmp(big.NewInt(0)) > 0)
assert.True(t, s.Cmp(big.NewInt(0)) > 0)
}
func TestWalletSignMessage(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
message := []byte("Hello, World!")
signature, err := wallet.SignMessage(message)
require.NoError(t, err)
assert.NotNil(t, signature)
assert.True(t, len(signature) > 0)
}
func TestWalletSignEthereumMessage(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
message := []byte("Hello, Ethereum!")
signature, err := wallet.SignEthereumMessage(message)
require.NoError(t, err)
assert.NotNil(t, signature)
assert.True(t, len(signature) > 0)
}
func TestWalletVerifySignature(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
message := []byte("Hello, World!")
signature, err := wallet.SignMessage(message)
require.NoError(t, err)
// Verify the signature
isValid := wallet.VerifySignature(message, signature)
assert.True(t, isValid)
// Verify with wrong message should fail
wrongMessage := []byte("Hello, Wrong!")
isValid = wallet.VerifySignature(wrongMessage, signature)
assert.False(t, isValid)
}
func TestWalletVerifyEthereumSignature(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
message := []byte("Hello, Ethereum!")
signature, err := wallet.SignEthereumMessage(message)
require.NoError(t, err)
// Verify the signature
isValid := wallet.VerifyEthereumSignature(message, signature)
assert.True(t, isValid)
// Verify with wrong message should fail
wrongMessage := []byte("Hello, Wrong!")
isValid = wallet.VerifyEthereumSignature(wrongMessage, signature)
assert.False(t, isValid)
}
func TestWalletGetCosmosAccountAddress(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "cosmos")
require.NoError(t, err)
addr, err := wallet.GetCosmosAccountAddress()
require.NoError(t, err)
assert.NotNil(t, addr)
assert.True(t, len(addr) > 0)
}
func TestWalletGetEthereumAccountAddress(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
addr := wallet.GetEthereumAccountAddress()
assert.NotEqual(t, common.Address{}, addr)
assert.Equal(t, wallet.EthereumAddress, addr.Hex())
}
func TestWalletExportPrivateKeys(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
cosmosPrivKey, ethPrivKey := wallet.ExportPrivateKeys()
assert.True(t, len(cosmosPrivKey) > 0)
assert.True(t, len(ethPrivKey) > 0)
assert.NotEqual(t, cosmosPrivKey, ethPrivKey)
}
func TestWalletGetInfo(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
prefix := "snr"
wallet, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
info := wallet.GetInfo()
assert.Equal(t, did, info.DID)
assert.Equal(t, salt, info.Salt)
assert.Equal(t, wallet.CosmosAddress, info.CosmosAddress)
assert.Equal(t, wallet.EthereumAddress, info.EthereumAddress)
assert.Equal(t, wallet.DerivationPath, info.DerivationPath)
}
func TestWalletGetEthereumTransactor(t *testing.T) {
wallet, err := WalletFromEntropy("did:example:123", "test-salt", "snr")
require.NoError(t, err)
chainID := big.NewInt(1)
transactor, err := wallet.GetEthereumTransactor(chainID)
require.NoError(t, err)
assert.NotNil(t, transactor)
assert.NotNil(t, transactor)
}
func TestWalletDeterministic(t *testing.T) {
did := "did:example:123456789abcdef"
salt := "test-salt"
prefix := "snr"
// Create two wallets with same parameters
wallet1, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
wallet2, err := WalletFromEntropy(did, salt, prefix)
require.NoError(t, err)
// They should be identical
assert.Equal(t, wallet1.DID, wallet2.DID)
assert.Equal(t, wallet1.Salt, wallet2.Salt)
assert.Equal(t, wallet1.CosmosAddress, wallet2.CosmosAddress)
assert.Equal(t, wallet1.EthereumAddress, wallet2.EthereumAddress)
assert.Equal(t, wallet1.DerivationPath, wallet2.DerivationPath)
// Private keys should be the same
cosmosPrivKey1, ethPrivKey1 := wallet1.ExportPrivateKeys()
cosmosPrivKey2, ethPrivKey2 := wallet2.ExportPrivateKeys()
assert.Equal(t, cosmosPrivKey1, cosmosPrivKey2)
assert.Equal(t, ethPrivKey1, ethPrivKey2)
}
func TestWalletDifferentInputs(t *testing.T) {
prefix := "snr"
// Create wallets with different DIDs
wallet1, err := WalletFromEntropy("did:example:123", "test-salt", prefix)
require.NoError(t, err)
wallet2, err := WalletFromEntropy("did:example:456", "test-salt", prefix)
require.NoError(t, err)
// They should be different
assert.NotEqual(t, wallet1.CosmosAddress, wallet2.CosmosAddress)
assert.NotEqual(t, wallet1.EthereumAddress, wallet2.EthereumAddress)
// Create wallets with different salts
wallet3, err := WalletFromEntropy("did:example:123", "different-salt", prefix)
require.NoError(t, err)
// They should be different
assert.NotEqual(t, wallet1.CosmosAddress, wallet3.CosmosAddress)
assert.NotEqual(t, wallet1.EthereumAddress, wallet3.EthereumAddress)
}