182 lines
4.4 KiB
Go
182 lines
4.4 KiB
Go
package sign
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/status-im/status-go/account"
|
|
)
|
|
|
|
type verifyFunc func(string) (*account.SelectedExtKey, error)
|
|
|
|
// PendingRequests is a capped container that holds pending signing requests.
|
|
type PendingRequests struct {
|
|
mu sync.RWMutex // to guard transactions map
|
|
requests map[string]*Request
|
|
|
|
log log.Logger
|
|
}
|
|
|
|
// NewPendingRequests creates a new requests list
|
|
func NewPendingRequests() *PendingRequests {
|
|
logger := log.New("package", "status-go/sign.PendingRequests")
|
|
|
|
return &PendingRequests{
|
|
requests: make(map[string]*Request),
|
|
log: logger,
|
|
}
|
|
}
|
|
|
|
// Add a new signing request.
|
|
func (rs *PendingRequests) Add(ctx context.Context, method string, meta Meta, completeFunc CompleteFunc) (*Request, error) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
|
|
request := newRequest(ctx, method, meta, completeFunc)
|
|
rs.requests[request.ID] = request
|
|
rs.log.Info("signing request is created", "ID", request.ID)
|
|
|
|
go SendSignRequestAdded(request)
|
|
|
|
return request, nil
|
|
}
|
|
|
|
// Get returns a signing request by it's ID.
|
|
func (rs *PendingRequests) Get(id string) (*Request, error) {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
if request, ok := rs.requests[id]; ok {
|
|
return request, nil
|
|
}
|
|
return nil, ErrSignReqNotFound
|
|
}
|
|
|
|
// First returns a first signing request (if exists, nil otherwise).
|
|
func (rs *PendingRequests) First() *Request {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
for _, req := range rs.requests {
|
|
return req
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Approve a signing request by it's ID. Requires a valid password and a verification function.
|
|
func (rs *PendingRequests) Approve(id string, password string, args *TxArgs, verify verifyFunc) Result {
|
|
rs.log.Info("complete sign request", "id", id)
|
|
request, err := rs.tryLock(id)
|
|
if err != nil {
|
|
rs.log.Warn("can't process transaction", "err", err)
|
|
return newErrResult(err)
|
|
}
|
|
|
|
selectedAccount, err := verify(password)
|
|
if err != nil {
|
|
rs.complete(request, EmptyResponse, err)
|
|
return newErrResult(err)
|
|
}
|
|
|
|
response, err := request.completeFunc(selectedAccount, password, args)
|
|
rs.log.Info("completed sign request ", "id", request.ID, "response", response, "err", err)
|
|
|
|
rs.complete(request, response, err)
|
|
|
|
return Result{
|
|
Response: response,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// Discard remove a signing request from the list of pending requests.
|
|
func (rs *PendingRequests) Discard(id string) error {
|
|
request, err := rs.Get(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rs.complete(request, EmptyResponse, ErrSignReqDiscarded)
|
|
return nil
|
|
}
|
|
|
|
// Wait blocks until a request with a specified ID is completed (approved or discarded)
|
|
func (rs *PendingRequests) Wait(id string, timeout time.Duration) Result {
|
|
request, err := rs.Get(id)
|
|
if err != nil {
|
|
return newErrResult(err)
|
|
}
|
|
for {
|
|
select {
|
|
case rst := <-request.result:
|
|
return rst
|
|
case <-time.After(timeout):
|
|
_, err := rs.tryLock(request.ID)
|
|
// if request is not already in progress, we complete it.
|
|
if err == nil {
|
|
rs.complete(request, EmptyResponse, ErrSignReqTimedOut)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count returns number of currently pending requests
|
|
func (rs *PendingRequests) Count() int {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
return len(rs.requests)
|
|
}
|
|
|
|
// Has checks whether a pending request with a given identifier exists in the list
|
|
func (rs *PendingRequests) Has(id string) bool {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
_, ok := rs.requests[id]
|
|
return ok
|
|
}
|
|
|
|
// tryLock is used to avoid double-completion of the same request.
|
|
// it returns a request instance if it isn't processing yet, returns an error otherwise.
|
|
func (rs *PendingRequests) tryLock(id string) (*Request, error) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
if tx, ok := rs.requests[id]; ok {
|
|
if tx.locked {
|
|
return nil, ErrSignReqInProgress
|
|
}
|
|
tx.locked = true
|
|
return tx, nil
|
|
}
|
|
return nil, ErrSignReqNotFound
|
|
}
|
|
|
|
// complete removes the request from the list if there is no error or an error is non-transient
|
|
func (rs *PendingRequests) complete(request *Request, response Response, err error) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
|
|
request.locked = false
|
|
|
|
if err != nil {
|
|
// TODO(divan): do we need the goroutine here?
|
|
go SendSignRequestFailed(request, err)
|
|
}
|
|
|
|
if err != nil && isTransient(err) {
|
|
return
|
|
}
|
|
|
|
delete(rs.requests, request.ID)
|
|
|
|
// response is updated only if err is nil, but transaction is not removed from a queue
|
|
result := Result{Error: err}
|
|
if err == nil {
|
|
result.Response = response
|
|
}
|
|
|
|
request.result <- result
|
|
}
|