add multi-account support (#1500)
* add multiaccount support add multi account ImportPrivateKey and StoreAccount test derivation from normal keys * add multiaccount to mobile pkg * use multiaccount params structs from the mobile pkg * move multiaccount tests together with the other lib tests * fix codeclimate warning and temporarily increase methods threshold * split library_test_utils.go to avoid linter warnings
This commit is contained in:
parent
e93d994460
commit
dcb0fa5262
|
@ -18,7 +18,7 @@ checks:
|
|||
# Classes defined with a high number of functions or methods.
|
||||
method-count:
|
||||
config:
|
||||
threshold: 20
|
||||
threshold: 21
|
||||
# Excessive lines of code within a single function or method
|
||||
method-lines:
|
||||
config:
|
||||
|
|
|
@ -12,11 +12,13 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
|
@ -42,7 +44,8 @@ type Manager struct {
|
|||
|
||||
mu sync.RWMutex
|
||||
|
||||
onboarding *Onboarding
|
||||
accountsGenerator *generator.Generator
|
||||
onboarding *Onboarding
|
||||
|
||||
selectedWalletAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
||||
selectedChatAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
||||
|
@ -50,9 +53,19 @@ type Manager struct {
|
|||
|
||||
// NewManager returns new node account manager.
|
||||
func NewManager(geth GethServiceProvider) *Manager {
|
||||
return &Manager{
|
||||
manager := &Manager{
|
||||
geth: geth,
|
||||
}
|
||||
|
||||
accountsGenerator := generator.New(manager)
|
||||
manager.accountsGenerator = accountsGenerator
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// AccountsGenerator returns accountsGenerator.
|
||||
func (m *Manager) AccountsGenerator() *generator.Generator {
|
||||
return m.accountsGenerator
|
||||
}
|
||||
|
||||
// CreateAccount creates an internal geth account
|
||||
|
@ -237,6 +250,8 @@ func (m *Manager) SelectAccount(walletAddress, chatAddress, password string) err
|
|||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.accountsGenerator.Reset()
|
||||
|
||||
selectedWalletAccount, err := m.unlockExtendedKey(walletAddress, password)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -299,10 +314,48 @@ func (m *Manager) Logout() {
|
|||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.accountsGenerator.Reset()
|
||||
m.selectedWalletAccount = nil
|
||||
m.selectedChatAccount = nil
|
||||
}
|
||||
|
||||
// ImportAccount imports the account specified with privateKey.
|
||||
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
account, err := keyStore.ImportECDSA(privateKey, password)
|
||||
|
||||
return account.Address, err
|
||||
}
|
||||
|
||||
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// imports extended key, create key file (if necessary)
|
||||
account, err := keyStore.ImportSingleExtendedKey(extKey, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
address = account.Address.Hex()
|
||||
|
||||
// obtain public key to return
|
||||
account, key, err := keyStore.AccountDecryptedKey(account, password)
|
||||
if err != nil {
|
||||
return address, "", err
|
||||
}
|
||||
|
||||
pubKey = hexutil.Encode(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
|
||||
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
|
||||
func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||
|
@ -491,7 +544,6 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
|
|||
}
|
||||
|
||||
var key *keystore.Key
|
||||
|
||||
account, key, err = keyStore.AccountDecryptedKey(account, password)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
type account struct {
|
||||
privateKey *ecdsa.PrivateKey
|
||||
extendedKey *extkeys.ExtendedKey
|
||||
}
|
||||
|
||||
func (a *account) toAccountInfo() AccountInfo {
|
||||
publicKeyHex := hexutil.Encode(crypto.FromECDSAPub(&a.privateKey.PublicKey))
|
||||
addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex()
|
||||
|
||||
return AccountInfo{
|
||||
PublicKey: publicKeyHex,
|
||||
Address: addressHex,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *account) toIdentifiedAccountInfo(id string) IdentifiedAccountInfo {
|
||||
info := a.toAccountInfo()
|
||||
return IdentifiedAccountInfo{
|
||||
AccountInfo: info,
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *account) toGeneratedAccountInfo(id string, mnemonic string) GeneratedAccountInfo {
|
||||
idInfo := a.toIdentifiedAccountInfo(id)
|
||||
return GeneratedAccountInfo{
|
||||
IdentifiedAccountInfo: idInfo,
|
||||
Mnemonic: mnemonic,
|
||||
}
|
||||
}
|
||||
|
||||
// AccountInfo contains a PublicKey and an Address of an account.
|
||||
type AccountInfo struct {
|
||||
PublicKey string `json:"publicKey"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// IdentifiedAccountInfo contains AccountInfo and the ID of an account.
|
||||
type IdentifiedAccountInfo struct {
|
||||
AccountInfo
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account.
|
||||
type GeneratedAccountInfo struct {
|
||||
IdentifiedAccountInfo
|
||||
Mnemonic string `json:"mnemonic"`
|
||||
}
|
||||
|
||||
func (a GeneratedAccountInfo) toGeneratedAndDerived(derived map[string]AccountInfo) GeneratedAndDerivedAccountInfo {
|
||||
return GeneratedAndDerivedAccountInfo{
|
||||
GeneratedAccountInfo: a,
|
||||
Derived: derived,
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratedAndDerivedAccountInfo contains GeneratedAccountInfo and derived AccountInfo mapped by derivation path.
|
||||
type GeneratedAndDerivedAccountInfo struct {
|
||||
GeneratedAccountInfo
|
||||
Derived map[string]AccountInfo `json:"derived"`
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAccountNotFoundByID is returned when the selected account doesn't exist in memory.
|
||||
ErrAccountNotFoundByID = errors.New("account not found")
|
||||
// ErrAccountCannotDeriveChildKeys is returned when trying to derive child accounts from a normal key.
|
||||
ErrAccountCannotDeriveChildKeys = errors.New("selected account cannot derive child keys")
|
||||
// ErrAccountManagerNotSet is returned when the account mananger instance is not set.
|
||||
ErrAccountManagerNotSet = errors.New("account manager not set")
|
||||
)
|
||||
|
||||
type AccountManager interface {
|
||||
AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error)
|
||||
ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error)
|
||||
ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error)
|
||||
}
|
||||
|
||||
type Generator struct {
|
||||
am AccountManager
|
||||
accounts map[string]*account
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func New(am AccountManager) *Generator {
|
||||
return &Generator{
|
||||
am: am,
|
||||
accounts: make(map[string]*account),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) Generate(mnemonicPhraseLength int, n int, bip39Passphrase string) ([]GeneratedAccountInfo, error) {
|
||||
entropyStrength, err := MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infos := make([]GeneratedAccountInfo, 0)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
mnemonic := extkeys.NewMnemonic()
|
||||
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
|
||||
}
|
||||
|
||||
info, err := g.ImportMnemonic(mnemonicPhrase, bip39Passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
return infos, err
|
||||
}
|
||||
|
||||
func (g *Generator) ImportPrivateKey(privateKeyHex string) (IdentifiedAccountInfo, error) {
|
||||
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
|
||||
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||
if err != nil {
|
||||
return IdentifiedAccountInfo{}, err
|
||||
}
|
||||
|
||||
acc := &account{
|
||||
privateKey: privateKey,
|
||||
}
|
||||
|
||||
id := g.addAccount(acc)
|
||||
|
||||
return acc.toIdentifiedAccountInfo(id), nil
|
||||
}
|
||||
|
||||
func (g *Generator) ImportJSONKey(json string, password string) (IdentifiedAccountInfo, error) {
|
||||
key, err := keystore.DecryptKey([]byte(json), password)
|
||||
if err != nil {
|
||||
return IdentifiedAccountInfo{}, err
|
||||
}
|
||||
|
||||
acc := &account{
|
||||
privateKey: key.PrivateKey,
|
||||
}
|
||||
|
||||
id := g.addAccount(acc)
|
||||
|
||||
return acc.toIdentifiedAccountInfo(id), nil
|
||||
}
|
||||
|
||||
func (g *Generator) ImportMnemonic(mnemonicPhrase string, bip39Passphrase string) (GeneratedAccountInfo, error) {
|
||||
mnemonic := extkeys.NewMnemonic()
|
||||
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, bip39Passphrase))
|
||||
if err != nil {
|
||||
return GeneratedAccountInfo{}, fmt.Errorf("can not create master extended key: %v", err)
|
||||
}
|
||||
|
||||
acc := &account{
|
||||
privateKey: masterExtendedKey.ToECDSA(),
|
||||
extendedKey: masterExtendedKey,
|
||||
}
|
||||
|
||||
id := g.addAccount(acc)
|
||||
|
||||
return acc.toGeneratedAccountInfo(id, mnemonicPhrase), nil
|
||||
}
|
||||
|
||||
func (g *Generator) GenerateAndDeriveAddresses(mnemonicPhraseLength int, n int, bip39Passphrase string, pathStrings []string) ([]GeneratedAndDerivedAccountInfo, error) {
|
||||
masterAccounts, err := g.Generate(mnemonicPhraseLength, n, bip39Passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accs := make([]GeneratedAndDerivedAccountInfo, n)
|
||||
|
||||
for i := 0; i < len(masterAccounts); i++ {
|
||||
acc := masterAccounts[i]
|
||||
derived, err := g.DeriveAddresses(acc.ID, pathStrings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accs[i] = acc.toGeneratedAndDerived(derived)
|
||||
}
|
||||
|
||||
return accs, nil
|
||||
}
|
||||
|
||||
func (g *Generator) DeriveAddresses(accountID string, pathStrings []string) (map[string]AccountInfo, error) {
|
||||
acc, err := g.findAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathAccountsInfo := make(map[string]AccountInfo)
|
||||
|
||||
for pathString, childAccount := range pathAccounts {
|
||||
pathAccountsInfo[pathString] = childAccount.toAccountInfo()
|
||||
}
|
||||
|
||||
return pathAccountsInfo, nil
|
||||
}
|
||||
|
||||
func (g *Generator) StoreAccount(accountID string, password string) (AccountInfo, error) {
|
||||
if g.am == nil {
|
||||
return AccountInfo{}, ErrAccountManagerNotSet
|
||||
}
|
||||
|
||||
acc, err := g.findAccount(accountID)
|
||||
if err != nil {
|
||||
return AccountInfo{}, err
|
||||
}
|
||||
|
||||
return g.store(acc, password)
|
||||
}
|
||||
|
||||
func (g *Generator) StoreDerivedAccounts(accountID string, password string, pathStrings []string) (map[string]AccountInfo, error) {
|
||||
if g.am == nil {
|
||||
return nil, ErrAccountManagerNotSet
|
||||
}
|
||||
|
||||
acc, err := g.findAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathAccountsInfo := make(map[string]AccountInfo)
|
||||
|
||||
for pathString, childAccount := range pathAccounts {
|
||||
info, err := g.store(childAccount, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathAccountsInfo[pathString] = info
|
||||
}
|
||||
|
||||
return pathAccountsInfo, nil
|
||||
}
|
||||
|
||||
func (g *Generator) LoadAccount(address string, password string) (IdentifiedAccountInfo, error) {
|
||||
if g.am == nil {
|
||||
return IdentifiedAccountInfo{}, ErrAccountManagerNotSet
|
||||
}
|
||||
|
||||
_, key, err := g.am.AddressToDecryptedAccount(address, password)
|
||||
if err != nil {
|
||||
return IdentifiedAccountInfo{}, err
|
||||
}
|
||||
|
||||
if err := ValidateKeystoreExtendedKey(key); err != nil {
|
||||
return IdentifiedAccountInfo{}, err
|
||||
}
|
||||
|
||||
acc := &account{
|
||||
privateKey: key.PrivateKey,
|
||||
extendedKey: key.ExtendedKey,
|
||||
}
|
||||
|
||||
id := g.addAccount(acc)
|
||||
|
||||
return acc.toIdentifiedAccountInfo(id), nil
|
||||
}
|
||||
|
||||
func (g *Generator) deriveChildAccounts(acc *account, pathStrings []string) (map[string]*account, error) {
|
||||
pathAccounts := make(map[string]*account)
|
||||
|
||||
for _, pathString := range pathStrings {
|
||||
childAccount, err := g.deriveChildAccount(acc, pathString)
|
||||
if err != nil {
|
||||
return pathAccounts, err
|
||||
}
|
||||
|
||||
pathAccounts[pathString] = childAccount
|
||||
}
|
||||
|
||||
return pathAccounts, nil
|
||||
}
|
||||
|
||||
func (g *Generator) deriveChildAccount(acc *account, pathString string) (*account, error) {
|
||||
_, path, err := decodePath(pathString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if acc.extendedKey.IsZeroed() && len(path) == 0 {
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
if acc.extendedKey.IsZeroed() {
|
||||
return nil, ErrAccountCannotDeriveChildKeys
|
||||
}
|
||||
|
||||
childExtendedKey, err := acc.extendedKey.Derive(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &account{
|
||||
privateKey: childExtendedKey.ToECDSA(),
|
||||
extendedKey: childExtendedKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *Generator) store(acc *account, password string) (AccountInfo, error) {
|
||||
if acc.extendedKey != nil {
|
||||
if _, _, err := g.am.ImportSingleExtendedKey(acc.extendedKey, password); err != nil {
|
||||
return AccountInfo{}, err
|
||||
}
|
||||
} else {
|
||||
if _, err := g.am.ImportAccount(acc.privateKey, password); err != nil {
|
||||
return AccountInfo{}, err
|
||||
}
|
||||
}
|
||||
|
||||
g.Reset()
|
||||
|
||||
return acc.toAccountInfo(), nil
|
||||
}
|
||||
|
||||
func (g *Generator) addAccount(acc *account) string {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
|
||||
id := uuid.NewRandom().String()
|
||||
g.accounts[id] = acc
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// Reset resets the accounts map removing all the accounts from memory.
|
||||
func (g *Generator) Reset() {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
|
||||
g.accounts = make(map[string]*account)
|
||||
}
|
||||
|
||||
func (g *Generator) findAccount(accountID string) (*account, error) {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
|
||||
acc, ok := g.accounts[accountID]
|
||||
if !ok {
|
||||
return nil, ErrAccountNotFoundByID
|
||||
}
|
||||
|
||||
return acc, nil
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testAccount = struct {
|
||||
mnemonic string
|
||||
bip39Passphrase string
|
||||
encriptionPassword string
|
||||
extendedMasterKey string
|
||||
bip44Key0 string
|
||||
bip44PubKey0 string
|
||||
bip44Address0 string
|
||||
bip44Address1 string
|
||||
}{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
bip39Passphrase: "TREZOR",
|
||||
encriptionPassword: "TEST_PASSWORD",
|
||||
extendedMasterKey: "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF",
|
||||
bip44Key0: "0x62f1d86b246c81bdd8f6c166d56896a4a5e1eddbcaebe06480e5c0bc74c28224",
|
||||
bip44PubKey0: "0x04986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f47784152cd2c7d9edd0ab20392a837464b5a750b2a7f3f06e6a5756b5211b6a6ed05",
|
||||
bip44Address0: "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6",
|
||||
bip44Address1: "0x7AF7283bd1462C3b957e8FAc28Dc19cBbF2FAdfe",
|
||||
}
|
||||
|
||||
const testAccountJSONFile = `{
|
||||
"address":"9c32f71d4db8fb9e1a58b0a80df79935e7256fa6",
|
||||
"crypto":
|
||||
{
|
||||
"cipher":"aes-128-ctr","ciphertext":"8055b65d5e41ef467c0cfe52ce6beda7f8dbe689221c6c43be9e9401bf173004",
|
||||
"cipherparams":{"iv":"738f002e5e5343e0bb0e1050e098f721"},
|
||||
"kdf":"scrypt",
|
||||
"kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"9a54fbe1439ac567bd05039f76907b2c2846364a38b2f6813bcdac5ab0ec9d18"},
|
||||
"mac":"79d817cd21afd4944e70d804d7871d10cbd15f25c6755416f780f81c1588677e"
|
||||
},
|
||||
"id":"6202ced9-f0cd-42e4-bf21-6029cca0ea91",
|
||||
"version":3
|
||||
}`
|
||||
|
||||
func TestGenerator_Generate(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
accountsInfo, err := g.Generate(12, 5, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(g.accounts))
|
||||
|
||||
for _, info := range accountsInfo {
|
||||
words := strings.Split(info.Mnemonic, " ")
|
||||
assert.Equal(t, 12, len(words))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_ImportPrivateKey(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
info, err := g.ImportPrivateKey(testAccount.bip44Key0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(g.accounts))
|
||||
|
||||
assert.Equal(t, testAccount.bip44PubKey0, info.PublicKey)
|
||||
assert.Equal(t, testAccount.bip44Address0, info.Address)
|
||||
}
|
||||
|
||||
func TestGenerator_ImportMnemonic(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(g.accounts))
|
||||
|
||||
key := g.accounts[info.ID]
|
||||
assert.Equal(t, testAccount.extendedMasterKey, key.extendedKey.String())
|
||||
}
|
||||
|
||||
func TestGenerator_ImportJSONKey(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
// wrong password
|
||||
_, err := g.ImportJSONKey(testAccountJSONFile, "wrong-password")
|
||||
assert.Error(t, err)
|
||||
|
||||
// right password
|
||||
info, err := g.ImportJSONKey(testAccountJSONFile, testAccount.encriptionPassword)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(g.accounts))
|
||||
assert.Equal(t, testAccount.bip44Address0, info.Address)
|
||||
|
||||
key := g.accounts[info.ID]
|
||||
keyHex := fmt.Sprintf("0x%x", crypto.FromECDSA(key.privateKey))
|
||||
assert.Equal(t, testAccount.bip44Key0, keyHex)
|
||||
}
|
||||
|
||||
func TestGenerator_DeriveAddresses(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(g.accounts))
|
||||
|
||||
path0 := "m/44'/60'/0'/0/0"
|
||||
path1 := "m/44'/60'/0'/0/1"
|
||||
|
||||
addresses, err := g.DeriveAddresses(info.ID, []string{path0, path1})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testAccount.bip44Address0, addresses[path0].Address)
|
||||
assert.Equal(t, testAccount.bip44Address1, addresses[path1].Address)
|
||||
}
|
||||
|
||||
func TestGenerator_DeriveAddresses_FromImportedPrivateKey(t *testing.T) {
|
||||
g := New(nil)
|
||||
assert.Equal(t, 0, len(g.accounts))
|
||||
|
||||
key, err := crypto.GenerateKey()
|
||||
assert.NoError(t, err)
|
||||
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
|
||||
info, err := g.ImportPrivateKey(hex)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(g.accounts))
|
||||
|
||||
// normal imported accounts cannot derive child accounts,
|
||||
// but only the address/pubblic key of the current key.
|
||||
paths := []string{"", "m"}
|
||||
for _, path := range paths {
|
||||
addresses, err := g.DeriveAddresses(info.ID, []string{path})
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedAddress := crypto.PubkeyToAddress(key.PublicKey).Hex()
|
||||
assert.Equal(t, expectedAddress, addresses[path].Address)
|
||||
}
|
||||
|
||||
// cannot derive other child keys from a normal key
|
||||
_, err = g.DeriveAddresses(info.ID, []string{"m/0/1/2"})
|
||||
assert.Equal(t, ErrAccountCannotDeriveChildKeys, err)
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type startingPoint int
|
||||
|
||||
const (
|
||||
tokenMaster = 0x6D // char m
|
||||
tokenSeparator = 0x2F // char /
|
||||
tokenHardened = 0x27 // char '
|
||||
tokenDot = 0x2E // char .
|
||||
|
||||
hardenedStart = 0x80000000 // 2^31
|
||||
)
|
||||
|
||||
const (
|
||||
startingPointMaster startingPoint = iota + 1
|
||||
startingPointCurrent
|
||||
startingPointParent
|
||||
)
|
||||
|
||||
type parseFunc = func() error
|
||||
|
||||
type pathDecoder struct {
|
||||
s string
|
||||
r *strings.Reader
|
||||
f parseFunc
|
||||
pos int
|
||||
path []uint32
|
||||
start startingPoint
|
||||
currentToken string
|
||||
currentTokenHardened bool
|
||||
}
|
||||
|
||||
func newPathDecoder(path string) (*pathDecoder, error) {
|
||||
d := &pathDecoder{
|
||||
s: path,
|
||||
r: strings.NewReader(path),
|
||||
}
|
||||
|
||||
if err := d.reset(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *pathDecoder) reset() error {
|
||||
_, err := d.r.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.pos = 0
|
||||
d.start = startingPointCurrent
|
||||
d.f = d.parseStart
|
||||
d.path = make([]uint32, 0)
|
||||
d.resetCurrentToken()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *pathDecoder) resetCurrentToken() {
|
||||
d.currentToken = ""
|
||||
d.currentTokenHardened = false
|
||||
}
|
||||
|
||||
func (d *pathDecoder) parse() (startingPoint, []uint32, error) {
|
||||
for {
|
||||
err := d.f()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} else {
|
||||
err = fmt.Errorf("error parsing derivation path %s; at position %d, %s", d.s, d.pos, err.Error())
|
||||
}
|
||||
|
||||
return d.start, d.path, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *pathDecoder) readByte() (byte, error) {
|
||||
b, err := d.r.ReadByte()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
d.pos++
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (d *pathDecoder) unreadByte() error {
|
||||
err := d.r.UnreadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.pos--
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *pathDecoder) parseStart() error {
|
||||
b, err := d.readByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b == tokenMaster {
|
||||
d.start = startingPointMaster
|
||||
d.f = d.parseSeparator
|
||||
return nil
|
||||
}
|
||||
|
||||
if b == tokenDot {
|
||||
b2, err := d.readByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b2 == tokenDot {
|
||||
d.f = d.parseSeparator
|
||||
d.start = startingPointParent
|
||||
return nil
|
||||
}
|
||||
|
||||
d.f = d.parseSeparator
|
||||
d.start = startingPointCurrent
|
||||
return d.unreadByte()
|
||||
}
|
||||
|
||||
d.f = d.parseSegment
|
||||
|
||||
return d.unreadByte()
|
||||
}
|
||||
|
||||
func (d *pathDecoder) saveSegment() error {
|
||||
if len(d.currentToken) > 0 {
|
||||
i, err := strconv.ParseUint(d.currentToken, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i >= hardenedStart {
|
||||
d.pos -= len(d.currentToken) - 1
|
||||
return fmt.Errorf("index must be lower than 2^31, got %d", i)
|
||||
}
|
||||
|
||||
if d.currentTokenHardened {
|
||||
i += hardenedStart
|
||||
}
|
||||
|
||||
d.path = append(d.path, uint32(i))
|
||||
}
|
||||
|
||||
d.f = d.parseSegment
|
||||
d.resetCurrentToken()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *pathDecoder) parseSeparator() error {
|
||||
b, err := d.readByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b == tokenSeparator {
|
||||
return d.saveSegment()
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b))
|
||||
}
|
||||
|
||||
func (d *pathDecoder) parseSegment() error {
|
||||
b, err := d.readByte()
|
||||
if err == io.EOF {
|
||||
if len(d.currentToken) == 0 {
|
||||
return fmt.Errorf("expected number, got EOF")
|
||||
}
|
||||
|
||||
if newErr := d.saveSegment(); newErr != nil {
|
||||
return newErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(d.currentToken) > 0 && b == tokenSeparator {
|
||||
return d.saveSegment()
|
||||
}
|
||||
|
||||
if len(d.currentToken) > 0 && b == tokenHardened {
|
||||
d.currentTokenHardened = true
|
||||
d.f = d.parseSeparator
|
||||
return nil
|
||||
}
|
||||
|
||||
if b < 0x30 || b > 0x39 {
|
||||
return fmt.Errorf("expected number, got %s", string(b))
|
||||
}
|
||||
|
||||
d.currentToken = fmt.Sprintf("%s%s", d.currentToken, string(b))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePath(str string) (startingPoint, []uint32, error) {
|
||||
d, err := newPathDecoder(str)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
return d.parse()
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDecodePath(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
path string
|
||||
expectedPath []uint32
|
||||
expectedStartingPoint startingPoint
|
||||
err error
|
||||
}{
|
||||
{
|
||||
path: "",
|
||||
expectedPath: []uint32{},
|
||||
expectedStartingPoint: startingPointCurrent,
|
||||
},
|
||||
{
|
||||
path: "1",
|
||||
expectedPath: []uint32{1},
|
||||
expectedStartingPoint: startingPointCurrent,
|
||||
},
|
||||
{
|
||||
path: "..",
|
||||
expectedPath: []uint32{},
|
||||
expectedStartingPoint: startingPointParent,
|
||||
},
|
||||
{
|
||||
path: "m",
|
||||
expectedPath: []uint32{},
|
||||
expectedStartingPoint: startingPointMaster,
|
||||
},
|
||||
{
|
||||
path: "m/1",
|
||||
expectedPath: []uint32{1},
|
||||
expectedStartingPoint: startingPointMaster,
|
||||
},
|
||||
{
|
||||
path: "m/1/2",
|
||||
expectedPath: []uint32{1, 2},
|
||||
expectedStartingPoint: startingPointMaster,
|
||||
},
|
||||
{
|
||||
path: "m/1/2'/3",
|
||||
expectedPath: []uint32{1, 2147483650, 3},
|
||||
expectedStartingPoint: startingPointMaster,
|
||||
},
|
||||
{
|
||||
path: "m/",
|
||||
err: fmt.Errorf("error parsing derivation path m/; at position 2, expected number, got EOF"),
|
||||
},
|
||||
{
|
||||
path: "m/1//2",
|
||||
err: fmt.Errorf("error parsing derivation path m/1//2; at position 5, expected number, got /"),
|
||||
},
|
||||
{
|
||||
path: "m/1'2",
|
||||
err: fmt.Errorf("error parsing derivation path m/1'2; at position 5, expected /, got 2"),
|
||||
},
|
||||
{
|
||||
path: "m/'/2",
|
||||
err: fmt.Errorf("error parsing derivation path m/'/2; at position 3, expected number, got '"),
|
||||
},
|
||||
{
|
||||
path: "m/2147483648",
|
||||
err: fmt.Errorf("error parsing derivation path m/2147483648; at position 3, index must be lower than 2^31, got 2147483648"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("scenario %d", i), func(t *testing.T) {
|
||||
startingP, path, err := decodePath(s.path)
|
||||
if s.err == nil {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedStartingPoint, startingP)
|
||||
assert.Equal(t, s.expectedPath, path)
|
||||
} else {
|
||||
assert.Equal(t, s.err, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidKeystoreExtendedKey is returned when the decrypted keystore file
|
||||
// contains some old Status keys.
|
||||
// The old version used to store the BIP44 account at index 0 as PrivateKey,
|
||||
// and the BIP44 account at index 1 as ExtendedKey.
|
||||
// The current version stores the same key as PrivateKey and ExtendedKey.
|
||||
ErrInvalidKeystoreExtendedKey = errors.New("PrivateKey and ExtendedKey are different")
|
||||
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
||||
)
|
||||
|
||||
// ValidateKeystoreExtendedKey validates the keystore keys, checking that
|
||||
// ExtendedKey is the extended key of PrivateKey.
|
||||
func ValidateKeystoreExtendedKey(key *keystore.Key) error {
|
||||
if key.ExtendedKey.IsZeroed() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !bytes.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA())) {
|
||||
return ErrInvalidKeystoreExtendedKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MnemonicPhraseLengthToEntropyStrength returns the entropy strength for a given mnemonic length
|
||||
func MnemonicPhraseLengthToEntropyStrength(length int) (extkeys.EntropyStrength, error) {
|
||||
if length < 12 || length > 24 || length%3 != 0 {
|
||||
return 0, ErrInvalidMnemonicPhraseLength
|
||||
}
|
||||
|
||||
bitsLength := length * 11
|
||||
checksumLength := bitsLength % 32
|
||||
|
||||
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func generateTestKey(t *testing.T) *extkeys.ExtendedKey {
|
||||
mnemonic := extkeys.NewMnemonic()
|
||||
mnemonicPhrase, err := mnemonic.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
|
||||
require.NoError(t, err)
|
||||
|
||||
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, ""))
|
||||
require.NoError(t, err)
|
||||
|
||||
return masterExtendedKey
|
||||
}
|
||||
|
||||
func TestValidateKeystoreExtendedKey(t *testing.T) {
|
||||
extendedKey1 := generateTestKey(t)
|
||||
extendedKey2 := generateTestKey(t)
|
||||
|
||||
// new keystore file format
|
||||
key := &keystore.Key{
|
||||
PrivateKey: extendedKey1.ToECDSA(),
|
||||
ExtendedKey: extendedKey1,
|
||||
}
|
||||
assert.NoError(t, ValidateKeystoreExtendedKey(key))
|
||||
|
||||
// old keystore file format where the extended key was
|
||||
// from another derivation path and not the same of the private key
|
||||
oldKey := &keystore.Key{
|
||||
PrivateKey: extendedKey1.ToECDSA(),
|
||||
ExtendedKey: extendedKey2,
|
||||
}
|
||||
assert.Error(t, ValidateKeystoreExtendedKey(oldKey))
|
||||
|
||||
// normal key where we don't have an extended key
|
||||
normalKey := &keystore.Key{
|
||||
PrivateKey: extendedKey1.ToECDSA(),
|
||||
ExtendedKey: nil,
|
||||
}
|
||||
assert.NoError(t, ValidateKeystoreExtendedKey(normalKey))
|
||||
}
|
|
@ -1,19 +1,15 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
// ErrInvalidMnemonicPhraseLength is returned if the requested mnemonic length is invalid.
|
||||
// Valid lengths are 12, 15, 18, 21, and 24.
|
||||
var ErrInvalidMnemonicPhraseLength = errors.New("mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
||||
|
||||
// OnboardingAccount is returned during onboarding and contains its ID and the mnemonic to re-generate the same account Info keys.
|
||||
type OnboardingAccount struct {
|
||||
ID string `json:"id"`
|
||||
|
@ -64,7 +60,7 @@ func (o *Onboarding) Account(id string) (*OnboardingAccount, error) {
|
|||
}
|
||||
|
||||
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
|
||||
entropyStrength, err := mnemonicPhraseLengthToEntropyStrenght(mnemonicPhraseLength)
|
||||
entropyStrength, err := generator.MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -115,14 +111,3 @@ func (o *Onboarding) deriveAccount(masterExtendedKey *extkeys.ExtendedKey, purpo
|
|||
|
||||
return address.Hex(), publicKeyHex, nil
|
||||
}
|
||||
|
||||
func mnemonicPhraseLengthToEntropyStrenght(length int) (extkeys.EntropyStrength, error) {
|
||||
if length < 12 || length > 24 || length%3 != 0 {
|
||||
return 0, ErrInvalidMnemonicPhraseLength
|
||||
}
|
||||
|
||||
bitsLength := length * 11
|
||||
checksumLength := bitsLength % 32
|
||||
|
||||
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
|
||||
}
|
||||
|
|
|
@ -4,35 +4,10 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMnemonicPhraseLengthToEntropyStrenght(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
phraseLength int
|
||||
expectedStrength extkeys.EntropyStrength
|
||||
expectedError error
|
||||
}{
|
||||
{12, 128, nil},
|
||||
{15, 160, nil},
|
||||
{18, 192, nil},
|
||||
{21, 224, nil},
|
||||
{24, 256, nil},
|
||||
// invalid
|
||||
{11, 0, ErrInvalidMnemonicPhraseLength},
|
||||
{14, 0, ErrInvalidMnemonicPhraseLength},
|
||||
{25, 0, ErrInvalidMnemonicPhraseLength},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
strength, err := mnemonicPhraseLengthToEntropyStrenght(s.phraseLength)
|
||||
assert.Equal(t, s.expectedError, err)
|
||||
assert.Equal(t, s.expectedStrength, strength)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnboarding(t *testing.T) {
|
||||
count := 2
|
||||
wordsCount := 24
|
||||
|
|
|
@ -10,7 +10,8 @@ import (
|
|||
|
||||
// errors
|
||||
var (
|
||||
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
|
||||
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
|
||||
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
||||
)
|
||||
|
||||
// Info contains wallet and chat addresses and public keys of an account.
|
||||
|
|
|
@ -6,6 +6,9 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
|
@ -65,3 +68,27 @@ func (suite *AccountUtilsTestSuite) TestHex() {
|
|||
func TestAccountUtilsTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(AccountUtilsTestSuite))
|
||||
}
|
||||
|
||||
func TestMnemonicPhraseLengthToEntropyStrength(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
phraseLength int
|
||||
expectedStrength extkeys.EntropyStrength
|
||||
expectedError error
|
||||
}{
|
||||
{12, 128, nil},
|
||||
{15, 160, nil},
|
||||
{18, 192, nil},
|
||||
{21, 224, nil},
|
||||
{24, 256, nil},
|
||||
// invalid
|
||||
{11, 0, ErrInvalidMnemonicPhraseLength},
|
||||
{14, 0, ErrInvalidMnemonicPhraseLength},
|
||||
{25, 0, ErrInvalidMnemonicPhraseLength},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
strength, err := generator.MnemonicPhraseLengthToEntropyStrength(s.phraseLength)
|
||||
assert.Equal(t, s.expectedError, err)
|
||||
assert.Equal(t, s.expectedStrength, strength)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -486,6 +486,7 @@ func (b *StatusBackend) Logout() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if b.statusNode.Config().BrowsersConfig.Enabled {
|
||||
svc, err := b.statusNode.BrowsersService()
|
||||
switch err {
|
||||
|
|
|
@ -365,9 +365,14 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// IsZeroed returns true if key is nil or empty
|
||||
func (k *ExtendedKey) IsZeroed() bool {
|
||||
return k == nil || len(k.KeyData) == 0
|
||||
}
|
||||
|
||||
// String returns the extended key as a human-readable base58-encoded string.
|
||||
func (k *ExtendedKey) String() string {
|
||||
if k == nil || len(k.KeyData) == 0 {
|
||||
if k.IsZeroed() {
|
||||
return EmptyExtendedKeyString
|
||||
}
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -30,6 +30,6 @@ require (
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
|
||||
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
|
||||
|
||||
replace github.com/NaySoftware/go-fcm => github.com/status-im/go-fcm v1.0.0-status
|
||||
|
|
2
go.sum
2
go.sum
|
@ -440,6 +440,8 @@ github.com/status-im/doubleratchet v2.0.0+incompatible h1:s77lF1lDubK0RKftxN2vH8
|
|||
github.com/status-im/doubleratchet v2.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
|
||||
github.com/status-im/go-ethereum v1.8.27-status.3 h1:h+CsF2Z/HyERLo5qTQnrK0RXHuDJ605hG2BbqNOLAAc=
|
||||
github.com/status-im/go-ethereum v1.8.27-status.3/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
|
||||
github.com/status-im/go-ethereum v1.8.27-status.5 h1:FS+1KwA97yWh9BtHse0BLfEWtldboSiTMA7nCavxtzc=
|
||||
github.com/status-im/go-ethereum v1.8.27-status.5/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
|
||||
github.com/status-im/go-fcm v1.0.0-status h1:eUNKm4ooAXdEf9/GaTYeTELna5aVMOEbzjbm2irQ0gY=
|
||||
github.com/status-im/go-fcm v1.0.0-status/go.mod h1:0JGzul9SfemcbTdw0mkzLR42j+BTNci5aQWlI0o/uk8=
|
||||
github.com/status-im/go-multiaddr-ethv4 v1.2.0 h1:OT84UsUzTCwguqCpJqkrCMiL4VZ1SvUtH9a5MsZupBk=
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
// +build e2e_test
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"C"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
mobile "github.com/status-im/status-go/mobile"
|
||||
)
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
)
|
||||
|
||||
func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{}) {
|
||||
var e struct {
|
||||
Error *string `json:"error"`
|
||||
}
|
||||
|
||||
json.Unmarshal([]byte(C.GoString(respJSON)), &e)
|
||||
if e.Error != nil {
|
||||
t.Errorf("unexpected response error: %s", *e.Error)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(respJSON)), resp); err != nil {
|
||||
t.Fatalf("error unmarshaling response to expected struct: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateDeriveAndStore(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
params := C.CString(`{
|
||||
"n": 2,
|
||||
"mnemonicPhraseLength": 24,
|
||||
"bip39Passphrase": ""
|
||||
}`)
|
||||
|
||||
// generate 2 random accounts
|
||||
rawResp := MultiAccountGenerate(params)
|
||||
var generateResp []generator.GeneratedAccountInfo
|
||||
// check there's no error in the response
|
||||
checkMultiAccountResponse(t, rawResp, &generateResp)
|
||||
if len(generateResp) != 2 {
|
||||
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
|
||||
return false
|
||||
}
|
||||
|
||||
bip44DerivationPath := "m/44'/60'/0'/0/0"
|
||||
eip1581DerivationPath := "m/43'/60'/1581'/0'/0"
|
||||
paths := []string{bip44DerivationPath, eip1581DerivationPath}
|
||||
|
||||
// derive 2 child accounts for each account without storing them
|
||||
for i := 0; i < len(generateResp); i++ {
|
||||
info := generateResp[i]
|
||||
mnemonicLength := len(strings.Split(info.Mnemonic, " "))
|
||||
|
||||
if mnemonicLength != 24 {
|
||||
t.Errorf("expected mnemonic to have 24 words, got %d", mnemonicLength)
|
||||
return false
|
||||
}
|
||||
|
||||
if ok := testMultiAccountDeriveAddresses(t, info.ID, paths); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// store 2 derived child accounts from the first account.
|
||||
// after that all the generated account should be remove from memory.
|
||||
if ok := testMultiAccountStoreDerived(t, generateResp[0].ID, paths); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
|
||||
params := mobile.MultiAccountDeriveAddressesParams{
|
||||
AccountID: accountID,
|
||||
Paths: paths,
|
||||
}
|
||||
|
||||
paramsJSON, err := json.Marshal(¶ms)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding MultiAccountDeriveAddressesParams")
|
||||
return false
|
||||
}
|
||||
|
||||
// derive addresses from account accountID
|
||||
rawResp := MultiAccountDeriveAddresses(C.CString(string(paramsJSON)))
|
||||
var deriveResp map[string]generator.AccountInfo
|
||||
// check the response doesn't have errors
|
||||
checkMultiAccountResponse(t, rawResp, &deriveResp)
|
||||
if len(deriveResp) != 2 {
|
||||
t.Errorf("expected 2 derived accounts info, got %d", len(deriveResp))
|
||||
return false
|
||||
}
|
||||
|
||||
// check that we have an address for each derivation path we used.
|
||||
for _, path := range paths {
|
||||
if _, ok := deriveResp[path]; !ok {
|
||||
t.Errorf("results doesn't contain account info for path %s", path)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
|
||||
password := "test-multiaccount-password"
|
||||
|
||||
params := mobile.MultiAccountStoreDerivedParams{
|
||||
MultiAccountDeriveAddressesParams: mobile.MultiAccountDeriveAddressesParams{
|
||||
AccountID: accountID,
|
||||
Paths: paths,
|
||||
},
|
||||
Password: password,
|
||||
}
|
||||
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding MultiAccountStoreDerivedParams")
|
||||
return false
|
||||
}
|
||||
|
||||
// store one child account for each derivation path.
|
||||
rawResp := MultiAccountStoreDerived(C.CString(string(paramsJSON)))
|
||||
var storeResp map[string]generator.AccountInfo
|
||||
|
||||
// check that we don't have errors in the response
|
||||
checkMultiAccountResponse(t, rawResp, &storeResp)
|
||||
addresses := make([]string, 0)
|
||||
for _, info := range storeResp {
|
||||
addresses = append(addresses, info.Address)
|
||||
}
|
||||
|
||||
if len(addresses) != 2 {
|
||||
t.Errorf("expected 2 addresses, got %d", len(addresses))
|
||||
return false
|
||||
}
|
||||
|
||||
// for each stored account, check that we can decrypt it with the password we used.
|
||||
dir := statusBackend.StatusNode().Config().DataDir
|
||||
for _, address := range addresses {
|
||||
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
|
||||
if err != nil {
|
||||
t.Errorf("failed to verify password on stored derived account")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := []string{"m/0", "m/1"}
|
||||
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
|
||||
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
|
||||
N: 2,
|
||||
MnemonicPhraseLength: 12,
|
||||
},
|
||||
Paths: paths,
|
||||
}
|
||||
|
||||
paramsJSON, err := json.Marshal(¶ms)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding MultiAccountGenerateAndDeriveParams")
|
||||
return false
|
||||
}
|
||||
|
||||
// generate 2 random accounts and derive 2 accounts from each one.
|
||||
rawResp := MultiAccountGenerateAndDeriveAddresses(C.CString(string(paramsJSON)))
|
||||
var generateResp []generator.GeneratedAndDerivedAccountInfo
|
||||
// check there's no error in the response
|
||||
checkMultiAccountResponse(t, rawResp, &generateResp)
|
||||
if len(generateResp) != 2 {
|
||||
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
|
||||
return false
|
||||
}
|
||||
|
||||
// check that for each account we have the 2 derived addresses
|
||||
for _, info := range generateResp {
|
||||
for _, path := range paths {
|
||||
if _, ok := info.Derived[path]; !ok {
|
||||
t.Errorf("results doesn't contain account info for path %s", path)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Errorf("failed generating key")
|
||||
}
|
||||
|
||||
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
|
||||
importParams := mobile.MultiAccountImportPrivateKeyParams{
|
||||
PrivateKey: hex,
|
||||
}
|
||||
|
||||
paramsJSON, err := json.Marshal(&importParams)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding MultiAccountImportPrivateKeyParams")
|
||||
return false
|
||||
}
|
||||
|
||||
// import raw private key
|
||||
rawResp := MultiAccountImportPrivateKey(C.CString(string(paramsJSON)))
|
||||
var importResp generator.IdentifiedAccountInfo
|
||||
// check the response doesn't have errors
|
||||
checkMultiAccountResponse(t, rawResp, &importResp)
|
||||
|
||||
// prepare StoreAccount params
|
||||
password := "test-multiaccount-imported-key-password"
|
||||
storeParams := mobile.MultiAccountStoreAccountParams{
|
||||
AccountID: importResp.ID,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
paramsJSON, err = json.Marshal(storeParams)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding MultiAccountStoreParams")
|
||||
return false
|
||||
}
|
||||
|
||||
// store the imported private key
|
||||
rawResp = MultiAccountStoreAccount(C.CString(string(paramsJSON)))
|
||||
var storeResp generator.AccountInfo
|
||||
// check the response doesn't have errors
|
||||
checkMultiAccountResponse(t, rawResp, &storeResp)
|
||||
|
||||
dir := statusBackend.StatusNode().Config().DataDir
|
||||
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
|
||||
if err != nil {
|
||||
t.Errorf("failed to verify password on stored derived account")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -146,6 +146,18 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
|
|||
"failed single transaction",
|
||||
testFailedTransaction,
|
||||
},
|
||||
{
|
||||
"MultiAccount - Generate/Derive/StoreDerived",
|
||||
testMultiAccountGenerateDeriveAndStore,
|
||||
},
|
||||
{
|
||||
"MultiAccount - GenerateAndDerive",
|
||||
testMultiAccountGenerateAndDerive,
|
||||
},
|
||||
{
|
||||
"MultiAccount - Import/Store",
|
||||
testMultiAccountImportStore,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -1018,16 +1030,3 @@ func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, API
|
|||
|
||||
fn(t, resp)
|
||||
}
|
||||
|
||||
// PanicAfter throws panic() after waitSeconds, unless abort channel receives
|
||||
// notification.
|
||||
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
|
||||
go func() {
|
||||
select {
|
||||
case <-abort:
|
||||
return
|
||||
case <-time.After(waitSeconds):
|
||||
panic("whatever you were doing takes toooo long: " + desc)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
package main
|
||||
|
||||
// #include <stdlib.h>
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mobile "github.com/status-im/status-go/mobile"
|
||||
)
|
||||
|
||||
// MultiAccountGenerate generates account in memory without storing them.
|
||||
//export MultiAccountGenerate
|
||||
func MultiAccountGenerate(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountGenerateParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
|
||||
//export MultiAccountGenerateAndDeriveAddresses
|
||||
func MultiAccountGenerateAndDeriveAddresses(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountGenerateAndDeriveAddressesParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
|
||||
//export MultiAccountDeriveAddresses
|
||||
func MultiAccountDeriveAddresses(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountDeriveAddressesParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
|
||||
//export MultiAccountStoreDerived
|
||||
func MultiAccountStoreDerived(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountStoreDerivedParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
// MultiAccountImportPrivateKey imports a raw private key without storing it.
|
||||
//export MultiAccountImportPrivateKey
|
||||
func MultiAccountImportPrivateKey(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountImportPrivateKeyParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
// MultiAccountStoreAccount stores the select account.
|
||||
//export MultiAccountStoreAccount
|
||||
func MultiAccountStoreAccount(paramsJSON *C.char) *C.char {
|
||||
var p mobile.MultiAccountStoreAccountParams
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(out))
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package statusgo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MultiAccountGenerateParams are the params sent to MultiAccountGenerate.
|
||||
type MultiAccountGenerateParams struct {
|
||||
N int `json:"n"`
|
||||
MnemonicPhraseLength int `json:"mnemonicPhraseLength"`
|
||||
Bip39Passphrase string `json:"bip39Passphrase"`
|
||||
}
|
||||
|
||||
// MultiAccountGenerateAndDeriveAddressesParams are the params sent to MultiAccountGenerateAndDeriveAddresses.
|
||||
type MultiAccountGenerateAndDeriveAddressesParams struct {
|
||||
MultiAccountGenerateParams
|
||||
Paths []string `json:"paths"`
|
||||
}
|
||||
|
||||
// MultiAccountDeriveAddressesParams are the params sent to MultiAccountDeriveAddresses.
|
||||
type MultiAccountDeriveAddressesParams struct {
|
||||
AccountID string `json:"accountID"`
|
||||
Paths []string `json:"paths"`
|
||||
}
|
||||
|
||||
// MultiAccountStoreDerivedParams are the params sent to MultiAccountStoreDerived.
|
||||
type MultiAccountStoreDerivedParams struct {
|
||||
MultiAccountDeriveAddressesParams
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// MultiAccountStoreAccountParams are the params sent to MultiAccountStoreAccount.
|
||||
type MultiAccountStoreAccountParams struct {
|
||||
AccountID string `json:"accountID"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// MultiAccountImportPrivateKeyParams are the params sent to MultiAccountImportPrivateKey.
|
||||
type MultiAccountImportPrivateKeyParams struct {
|
||||
PrivateKey string `json:"privateKey"`
|
||||
}
|
||||
|
||||
// MultiAccountGenerate generates account in memory without storing them.
|
||||
func MultiAccountGenerate(paramsJSON string) string {
|
||||
var p MultiAccountGenerateParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
|
||||
func MultiAccountGenerateAndDeriveAddresses(paramsJSON string) string {
|
||||
var p MultiAccountGenerateAndDeriveAddressesParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
|
||||
func MultiAccountDeriveAddresses(paramsJSON string) string {
|
||||
var p MultiAccountDeriveAddressesParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
|
||||
func MultiAccountStoreDerived(paramsJSON string) string {
|
||||
var p MultiAccountStoreDerivedParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// MultiAccountImportPrivateKey imports a raw private key without storing it.
|
||||
func MultiAccountImportPrivateKey(paramsJSON string) string {
|
||||
var p MultiAccountImportPrivateKeyParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// MultiAccountStoreAccount stores the select account.
|
||||
func MultiAccountStoreAccount(paramsJSON string) string {
|
||||
var p MultiAccountStoreAccountParams
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
|
@ -112,6 +112,14 @@ func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.D
|
|||
}
|
||||
}
|
||||
|
||||
if err := activateNodeServices(stack, config, db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func activateNodeServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
|
||||
// start Whisper service.
|
||||
if err := activateShhService(stack, config, db); err != nil {
|
||||
return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
e2e "github.com/status-im/status-go/t/e2e"
|
||||
. "github.com/status-im/status-go/t/utils"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
@ -82,6 +83,58 @@ func (s *AccountsTestSuite) TestAccountsList() {
|
|||
s.False(!subAccount2MatchesKey1 && !subAccount2MatchesKey2, "subAcount2 not returned")
|
||||
}
|
||||
|
||||
func (s *AccountsTestSuite) TestImportSingleExtendedKey() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||
s.NoError(err)
|
||||
s.NotNil(keyStore)
|
||||
|
||||
// create a master extended key
|
||||
mn := extkeys.NewMnemonic()
|
||||
mnemonic, err := mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
|
||||
s.NoError(err)
|
||||
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
|
||||
s.NoError(err)
|
||||
derivedExtendedKey, err := extKey.EthBIP44Child(0)
|
||||
s.NoError(err)
|
||||
|
||||
// import single extended key
|
||||
password := "test-password-1"
|
||||
addr, _, err := s.Backend.AccountManager().ImportSingleExtendedKey(derivedExtendedKey, password)
|
||||
s.NoError(err)
|
||||
|
||||
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr, password)
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA()))
|
||||
}
|
||||
|
||||
func (s *AccountsTestSuite) TestImportAccount() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||
s.NoError(err)
|
||||
s.NotNil(keyStore)
|
||||
|
||||
// create a private key
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
s.NoError(err)
|
||||
|
||||
// import as normal account
|
||||
password := "test-password-2"
|
||||
addr, err := s.Backend.AccountManager().ImportAccount(privateKey, password)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr.String(), password)
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(crypto.FromECDSA(privateKey), crypto.FromECDSA(key.PrivateKey))
|
||||
s.True(key.ExtendedKey.IsZeroed())
|
||||
}
|
||||
|
||||
func (s *AccountsTestSuite) TestCreateChildAccount() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
|
|
@ -47,8 +47,12 @@ type Key struct {
|
|||
// we only store privkey as pubkey/address can be derived from it
|
||||
// privkey in this struct is always in plaintext
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
// extended key is the root node for new hardened children i.e. sub-accounts
|
||||
// ExtendedKey is the extended key of the PrivateKey itself, and it's used
|
||||
// to derive child keys.
|
||||
ExtendedKey *extkeys.ExtendedKey
|
||||
// SubAccountIndex is DEPRECATED
|
||||
// It was use in Status to keep track of the number of sub-account created
|
||||
// before having multi-account support.
|
||||
// next index to be used for sub-account child derivation
|
||||
SubAccountIndex uint32
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
)
|
||||
|
||||
|
@ -459,14 +460,49 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
|
|||
return ks.importKey(key, passphrase)
|
||||
}
|
||||
|
||||
// ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields
|
||||
// of the Key struct.
|
||||
// ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0,
|
||||
// and ExtendedKey is the extended key of the BIP44 key at index 1.
|
||||
func (ks *KeyStore) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||
privateKeyECDSA := extKey.ToECDSA()
|
||||
id := uuid.NewRandom()
|
||||
key := &Key{
|
||||
Id: id,
|
||||
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
|
||||
PrivateKey: privateKeyECDSA,
|
||||
ExtendedKey: extKey,
|
||||
}
|
||||
|
||||
// if account is already imported, return cached version
|
||||
if ks.cache.hasAddress(key.Address) {
|
||||
a := accounts.Account{
|
||||
Address: key.Address,
|
||||
}
|
||||
ks.cache.maybeReload()
|
||||
ks.cache.mu.Lock()
|
||||
a, err := ks.cache.find(a)
|
||||
ks.cache.mu.Unlock()
|
||||
if err != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
return a, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
return ks.importKey(key, passphrase)
|
||||
}
|
||||
|
||||
// ImportExtendedKey stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
||||
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
||||
// Deprecated: status-go is now using ImportSingleExtendedKey
|
||||
func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||
return ks.ImportExtendedKeyForPurpose(extkeys.KeyPurposeWallet, extKey, passphrase)
|
||||
}
|
||||
|
||||
// ImportExtendedKeyForPurpose stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
||||
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
||||
// Deprecated: status-go is now using ImportSingleExtendedKey
|
||||
func (ks *KeyStore) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||
key, err := newKeyForPurposeFromExtendedKey(keyPurpose, extKey)
|
||||
if err != nil {
|
||||
|
|
|
@ -198,6 +198,9 @@ func (s *dialstate) newTasks(nRunning int, peers map[enode.ID]*Peer, now time.Ti
|
|||
case errNotWhitelisted, errSelf:
|
||||
log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}, "err", err)
|
||||
delete(s.static, t.dest.ID())
|
||||
case errRecentlyDialed:
|
||||
expiry := s.hist.expiry(t.dest.ID())
|
||||
log.Debug("peer was recently dialed", "enode", t.dest.String(), "expires at", expiry, "after", expiry.Sub(time.Now()))
|
||||
case nil:
|
||||
s.dialing[id] = t.flags
|
||||
newtasks = append(newtasks, t)
|
||||
|
@ -411,6 +414,16 @@ func (h dialHistory) contains(id enode.ID) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h dialHistory) expiry(id enode.ID) time.Time {
|
||||
for _, v := range h {
|
||||
if v.id == id {
|
||||
return v.exp
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (h *dialHistory) expire(now time.Time) {
|
||||
for h.Len() > 0 && h.min().exp.Before(now) {
|
||||
heap.Pop(h)
|
||||
|
|
|
@ -693,6 +693,7 @@ running:
|
|||
// ephemeral static peer list. Add it to the dialer,
|
||||
// it will keep the node connected.
|
||||
srv.log.Trace("Adding static node", "node", n)
|
||||
dialstate.removeStatic(n)
|
||||
dialstate.addStatic(n)
|
||||
case n := <-srv.removestatic:
|
||||
// This channel is used by RemovePeer to send a
|
||||
|
|
|
@ -24,7 +24,7 @@ github.com/davecgh/go-spew/spew
|
|||
github.com/deckarep/golang-set
|
||||
# github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712
|
||||
github.com/edsrzf/mmap-go
|
||||
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
|
||||
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
|
||||
github.com/ethereum/go-ethereum/accounts
|
||||
github.com/ethereum/go-ethereum/accounts/keystore
|
||||
github.com/ethereum/go-ethereum/common
|
||||
|
|
Loading…
Reference in New Issue