diff --git a/eth/api_backend.go b/eth/api_backend.go index 91f392f94..471275de5 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -32,14 +32,20 @@ import ( "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) // EthApiBackend implements ethapi.Backend for full nodes type EthApiBackend struct { - eth *Ethereum - gpo *gasprice.Oracle + eth *Ethereum + gpo *gasprice.Oracle + statusBackend *ethapi.StatusBackend +} + +func (b *EthApiBackend) GetStatusBackend() *ethapi.StatusBackend { + return b.statusBackend } func (b *EthApiBackend) ChainConfig() *params.ChainConfig { diff --git a/eth/backend.go b/eth/backend.go index 1cd9e8fff..2fbdec4e2 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -169,7 +169,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine) eth.miner.SetExtra(makeExtraData(config.ExtraData)) - eth.ApiBackend = &EthApiBackend{eth, nil} + eth.ApiBackend = &EthApiBackend{eth, nil, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.GasPrice diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 025f42617..d8f48a890 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -178,15 +178,24 @@ func (s *PublicTxPoolAPI) Inspect() map[string]map[string]map[string]string { // It offers only methods that can retrieve accounts. type PublicAccountAPI struct { am *accounts.Manager + b Backend } // NewPublicAccountAPI creates a new PublicAccountAPI. -func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI { - return &PublicAccountAPI{am: am} +func NewPublicAccountAPI(b Backend) *PublicAccountAPI { + return &PublicAccountAPI{ + am: b.AccountManager(), + b: b, + } } // Accounts returns the collection of accounts this node manages func (s *PublicAccountAPI) Accounts() []common.Address { + backend := s.b.GetStatusBackend() + if backend != nil { + return backend.am.Accounts() + } + addresses := make([]common.Address, 0) // return [] instead of nil if empty for _, wallet := range s.am.Wallets() { for _, account := range wallet.Accounts() { @@ -216,6 +225,11 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI { // ListAccounts will return a list of addresses for accounts this node manages. func (s *PrivateAccountAPI) ListAccounts() []common.Address { + backend := s.b.GetStatusBackend() + if backend != nil { + return backend.am.Accounts() + } + addresses := make([]common.Address, 0) // return [] instead of nil if empty for _, wallet := range s.am.Wallets() { for _, account := range wallet.Accounts() { @@ -1122,10 +1136,46 @@ func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c return tx.Hash(), nil } -// SendTransaction creates a transaction for the given argument, sign it and submit it to the +// SendTransactionWithPassphrase creates a transaction by unpacking queued transaction, signs it and submits to the // transaction pool. -func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { +// @Status +func (s *PublicTransactionPoolAPI) SendTransactionWithPassphrase(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) { + // Look up the wallet containing the requested signer + account := accounts.Account{Address: args.From} + + wallet, err := s.b.AccountManager().Find(account) + if err != nil { + return common.Hash{}, err + } + + if args.Nonce == nil { + // Hold the addresse's mutex around signing to prevent concurrent assignment of + // the same nonce to multiple accounts. + s.nonceLock.LockAddr(args.From) + defer s.nonceLock.UnlockAddr(args.From) + } + // Set some sanity defaults and terminate on failure + if err := args.setDefaults(ctx, s.b); err != nil { + return common.Hash{}, err + } + // Assemble the transaction and sign with the wallet + tx := args.toTransaction() + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := wallet.SignTxWithPassphrase(account, passphrase, tx, chainID) + if err != nil { + return common.Hash{}, err + } + return submitTransaction(ctx, s.b, signed) +} + +// SendTransaction creates a transaction by unpacking queued transaction, signs it and submits to the +// transaction pool. +func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 368fa4872..cac58dfc0 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -68,6 +68,8 @@ type Backend interface { ChainConfig() *params.ChainConfig CurrentBlock() *types.Block + + GetStatusBackend() *StatusBackend } func GetAPIs(apiBackend Backend) []rpc.API { @@ -105,7 +107,7 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "eth", Version: "1.0", - Service: NewPublicAccountAPI(apiBackend.AccountManager()), + Service: NewPublicAccountAPI(apiBackend), Public: true, }, { Namespace: "personal", diff --git a/internal/ethapi/status_backend.go b/internal/ethapi/status_backend.go new file mode 100644 index 000000000..c4e553cae --- /dev/null +++ b/internal/ethapi/status_backend.go @@ -0,0 +1,88 @@ +package ethapi + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/log" +) + +// StatusBackend exposes Ethereum internals to support custom semantics in status-go bindings +type StatusBackend struct { + eapi *PublicEthereumAPI // Wrapper around the Ethereum object to access metadata + bcapi *PublicBlockChainAPI // Wrapper around the blockchain to access chain data + txapi *PublicTransactionPoolAPI // Wrapper around the transaction pool to access transaction data + + am *status.AccountManager +} + +var ( + ErrStatusBackendNotInited = errors.New("StatusIM backend is not properly inited") +) + +// NewStatusBackend creates a new backend using an existing Ethereum object. +func NewStatusBackend(apiBackend Backend) *StatusBackend { + log.Info("StatusIM: backend service inited") + return &StatusBackend{ + eapi: NewPublicEthereumAPI(apiBackend), + bcapi: NewPublicBlockChainAPI(apiBackend), + txapi: NewPublicTransactionPoolAPI(apiBackend, new(AddrLocker)), + am: status.NewAccountManager(apiBackend.AccountManager()), + } +} + +// SetAccountsFilterHandler sets a callback that is triggered when account list is requested +func (b *StatusBackend) SetAccountsFilterHandler(fn status.AccountsFilterHandler) { + b.am.SetAccountsFilterHandler(fn) +} + +// AccountManager returns reference to account manager +func (b *StatusBackend) AccountManager() *status.AccountManager { + return b.am +} + +// SendTransaction wraps call to PublicTransactionPoolAPI.SendTransactionWithPassphrase +func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs, passphrase string) (common.Hash, error) { + if ctx == nil { + ctx = context.Background() + } + + if estimatedGas, err := b.EstimateGas(ctx, args); err == nil { + if estimatedGas.ToInt().Cmp(big.NewInt(defaultGas)) == 1 { // gas > defaultGas + args.Gas = estimatedGas + } + } + + return b.txapi.SendTransactionWithPassphrase(ctx, SendTxArgs(args), passphrase) +} + +// EstimateGas uses underlying blockchain API to obtain gas for a given tx arguments +func (b *StatusBackend) EstimateGas(ctx context.Context, args status.SendTxArgs) (*hexutil.Big, error) { + if args.Gas != nil { + return args.Gas, nil + } + + var gasPrice hexutil.Big + if args.GasPrice != nil { + gasPrice = *args.GasPrice + } + + var value hexutil.Big + if args.Value != nil { + value = *args.Value + } + + callArgs := CallArgs{ + From: args.From, + To: args.To, + GasPrice: gasPrice, + Value: value, + Data: args.Data, + } + + return b.bcapi.EstimateGas(ctx, callArgs) +} diff --git a/les/api_backend.go b/les/api_backend.go index 56f617a7d..f839f24e6 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -32,14 +32,20 @@ import ( "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) type LesApiBackend struct { - eth *LightEthereum - gpo *gasprice.Oracle + eth *LightEthereum + gpo *gasprice.Oracle + statusBackend *ethapi.StatusBackend +} + +func (b *LesApiBackend) GetStatusBackend() *ethapi.StatusBackend { + return b.statusBackend } func (b *LesApiBackend) ChainConfig() *params.ChainConfig { diff --git a/les/backend.go b/les/backend.go index 333df920e..7d8cf3916 100644 --- a/les/backend.go +++ b/les/backend.go @@ -75,6 +75,8 @@ type LightEthereum struct { netRPCService *ethapi.PublicNetAPI wg sync.WaitGroup + + StatusBackend *ethapi.StatusBackend } func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { @@ -126,12 +128,17 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { if leth.protocolManager, err = NewProtocolManager(leth.chainConfig, true, ClientProtocolVersions, config.NetworkId, leth.eventMux, leth.engine, leth.peers, leth.blockchain, nil, chainDb, leth.odr, leth.relay, quitSync, &leth.wg); err != nil { return nil, err } - leth.ApiBackend = &LesApiBackend{leth, nil} + leth.ApiBackend = &LesApiBackend{leth, nil, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.GasPrice } leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) + + // inject status-im backend + leth.ApiBackend.statusBackend = ethapi.NewStatusBackend(leth.ApiBackend) + leth.StatusBackend = leth.ApiBackend.statusBackend // alias + return leth, nil } diff --git a/les/status/accounts.go b/les/status/accounts.go new file mode 100644 index 000000000..78bd2ad92 --- /dev/null +++ b/les/status/accounts.go @@ -0,0 +1,45 @@ +package status + +import ( + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" +) + +// AccountManager abstracts both internal account manager and extra filter status backend requires +type AccountManager struct { + am *accounts.Manager + accountsFilterHandler AccountsFilterHandler +} + +// NewAccountManager creates a new AccountManager +func NewAccountManager(am *accounts.Manager) *AccountManager { + return &AccountManager{ + am: am, + } +} + +// AccountsFilterHandler function to filter out accounts list +type AccountsFilterHandler func([]common.Address) []common.Address + +// Accounts returns accounts' addresses of currently logged in user. +// Since status supports HD keys, the following list is returned: +// [addressCDK#1, addressCKD#2->Child1, addressCKD#2->Child2, .. addressCKD#2->ChildN] +func (d *AccountManager) Accounts() []common.Address { + var addresses []common.Address + for _, wallet := range d.am.Wallets() { + for _, account := range wallet.Accounts() { + addresses = append(addresses, account.Address) + } + } + + if d.accountsFilterHandler != nil { + return d.accountsFilterHandler(addresses) + } + + return addresses +} + +// SetAccountsFilterHandler sets filtering function for accounts list +func (d *AccountManager) SetAccountsFilterHandler(fn AccountsFilterHandler) { + d.accountsFilterHandler = fn +} diff --git a/les/status/types.go b/les/status/types.go new file mode 100644 index 000000000..04437bdb6 --- /dev/null +++ b/les/status/types.go @@ -0,0 +1,17 @@ +package status + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Nonce *hexutil.Uint64 `json:"nonce"` +}