mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-06 03:11:38 +00:00
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user