feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,38 @@
Settings service
================
Settings service provides private API for storing all configuration for a selected account.
To enable:
1. Client must ensure that settings db is initialized in the api.Backend.
2. Add `settings` to APIModules in config.
API
---
### settings_saveConfig
#### Parameters
- `type`: `string` - configuratin type. if not unique error is raised.
- `conf`: `bytes` - raw json.
### settings_getConfig
#### Parameters
- `type`: string
#### Returns
- `conf` raw json
### settings_saveNodeConfig
Special case of the settings_saveConfig. In status-go we are using constant `node-config` as a type for node configuration.
Application depends on this value and will try to load it when node is started. This method is provided
in order to remove syncing mentioned constant between status-go and users.
#### Parameters
- `conf`: params.NodeConfig

View File

@@ -0,0 +1,614 @@
package accounts
import (
"context"
"errors"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
walletsettings "github.com/status-im/status-go/multiaccounts/settings_wallet"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/services/accounts/accountsevent"
)
func NewAccountsAPI(manager *account.GethManager, config *params.NodeConfig, db *accounts.Database, feed *event.Feed, messenger **protocol.Messenger) *API {
return &API{manager, config, db, feed, messenger}
}
// API is class with methods available over RPC.
type API struct {
manager *account.GethManager
config *params.NodeConfig
db *accounts.Database
feed *event.Feed
messenger **protocol.Messenger
}
type DerivedAddress struct {
Address common.Address `json:"address"`
Path string `json:"path"`
HasActivity bool `json:"hasActivity"`
AlreadyCreated bool `json:"alreadyCreated"`
}
func (api *API) SaveAccount(ctx context.Context, account *accounts.Account) error {
log.Info("[AccountsAPI::SaveAccount]")
err := (*api.messenger).SaveOrUpdateAccount(account)
if err != nil {
return err
}
api.feed.Send(accountsevent.Event{
Type: accountsevent.EventTypeAdded,
Accounts: []common.Address{common.Address(account.Address)},
})
return nil
}
// Setting `Keypair` without `Accounts` will update keypair only, `Keycards` won't be saved/updated this way.
func (api *API) SaveKeypair(ctx context.Context, keypair *accounts.Keypair) error {
log.Info("[AccountsAPI::SaveKeypair]")
err := (*api.messenger).SaveOrUpdateKeypair(keypair)
if err != nil {
return err
}
commonAddresses := []common.Address{}
for _, acc := range keypair.Accounts {
commonAddresses = append(commonAddresses, common.Address(acc.Address))
}
api.feed.Send(accountsevent.Event{
Type: accountsevent.EventTypeAdded,
Accounts: commonAddresses,
})
return nil
}
func (api *API) HasPairedDevices(ctx context.Context) bool {
return (*api.messenger).HasPairedDevices()
}
// Setting `Keypair` without `Accounts` will update keypair only.
func (api *API) UpdateKeypairName(ctx context.Context, keyUID string, name string) error {
return (*api.messenger).UpdateKeypairName(keyUID, name)
}
func (api *API) MoveWalletAccount(ctx context.Context, fromPosition int64, toPosition int64) error {
return (*api.messenger).MoveWalletAccount(fromPosition, toPosition)
}
func (api *API) UpdateTokenPreferences(ctx context.Context, preferences []walletsettings.TokenPreferences) error {
return (*api.messenger).UpdateTokenPreferences(preferences)
}
func (api *API) GetTokenPreferences(ctx context.Context) ([]walletsettings.TokenPreferences, error) {
return (*api.messenger).GetTokenPreferences()
}
func (api *API) UpdateCollectiblePreferences(ctx context.Context, preferences []walletsettings.CollectiblePreferences) error {
return (*api.messenger).UpdateCollectiblePreferences(preferences)
}
func (api *API) GetCollectiblePreferences(ctx context.Context) ([]walletsettings.CollectiblePreferences, error) {
return (*api.messenger).GetCollectiblePreferences()
}
func (api *API) GetAccounts(ctx context.Context) ([]*accounts.Account, error) {
return api.db.GetActiveAccounts()
}
func (api *API) GetWatchOnlyAccounts(ctx context.Context) ([]*accounts.Account, error) {
return api.db.GetActiveWatchOnlyAccounts()
}
func (api *API) GetKeypairs(ctx context.Context) ([]*accounts.Keypair, error) {
return api.db.GetActiveKeypairs()
}
func (api *API) GetAccountByAddress(ctx context.Context, address types.Address) (*accounts.Account, error) {
return api.db.GetAccountByAddress(address)
}
func (api *API) GetKeypairByKeyUID(ctx context.Context, keyUID string) (*accounts.Keypair, error) {
return api.db.GetKeypairByKeyUID(keyUID)
}
func (api *API) DeleteAccount(ctx context.Context, address types.Address) error {
err := (*api.messenger).DeleteAccount(address)
if err != nil {
return err
}
api.feed.Send(accountsevent.Event{
Type: accountsevent.EventTypeRemoved,
Accounts: []common.Address{common.Address(address)},
})
return nil
}
func (api *API) DeleteKeypair(ctx context.Context, keyUID string) error {
keypair, err := api.db.GetKeypairByKeyUID(keyUID)
if err != nil {
return err
}
err = (*api.messenger).DeleteKeypair(keyUID)
if err != nil {
return err
}
var addresses []common.Address
for _, acc := range keypair.Accounts {
if acc.Chat {
continue
}
addresses = append(addresses, common.Address(acc.Address))
}
api.feed.Send(accountsevent.Event{
Type: accountsevent.EventTypeRemoved,
Accounts: addresses,
})
return nil
}
func (api *API) AddKeypair(ctx context.Context, password string, keypair *accounts.Keypair) error {
if len(keypair.KeyUID) == 0 {
return errors.New("`KeyUID` field of a keypair must be set")
}
if len(keypair.Name) == 0 {
return errors.New("`Name` field of a keypair must be set")
}
if len(keypair.Type) == 0 {
return errors.New("`Type` field of a keypair must be set")
}
if keypair.Type != accounts.KeypairTypeKey {
if len(keypair.DerivedFrom) == 0 {
return errors.New("`DerivedFrom` field of a keypair must be set")
}
}
for _, acc := range keypair.Accounts {
if acc.KeyUID != keypair.KeyUID {
return errors.New("all accounts of a keypair must have the same `KeyUID` as keypair key uid")
}
err := api.checkAccountValidity(acc)
if err != nil {
return err
}
}
err := api.SaveKeypair(ctx, keypair)
if err != nil {
return err
}
if len(password) > 0 {
for _, acc := range keypair.Accounts {
if acc.Type == accounts.AccountTypeGenerated || acc.Type == accounts.AccountTypeSeed {
err = api.createKeystoreFileForAccount(keypair.DerivedFrom, password, acc)
if err != nil {
return err
}
}
}
}
return nil
}
func (api *API) checkAccountValidity(account *accounts.Account) error {
if len(account.Address) == 0 {
return errors.New("`Address` field of an account must be set")
}
if len(account.Type) == 0 {
return errors.New("`Type` field of an account must be set")
}
if account.Wallet || account.Chat {
return errors.New("default wallet and chat account cannot be added this way")
}
if len(account.Name) == 0 {
return errors.New("`Name` field of an account must be set")
}
if len(account.Emoji) == 0 {
return errors.New("`Emoji` field of an account must be set")
}
if len(account.ColorID) == 0 {
return errors.New("`ColorID` field of an account must be set")
}
if account.Type != accounts.AccountTypeWatch {
if len(account.KeyUID) == 0 {
return errors.New("`KeyUID` field of an account must be set")
}
if len(account.PublicKey) == 0 {
return errors.New("`PublicKey` field of an account must be set")
}
if account.Type != accounts.AccountTypeKey {
if len(account.Path) == 0 {
return errors.New("`Path` field of an account must be set")
}
}
}
addressExists, err := api.db.AddressExists(account.Address)
if err != nil {
return err
}
if addressExists {
return errors.New("account already exists")
}
return nil
}
func (api *API) createKeystoreFileForAccount(masterAddress string, password string, account *accounts.Account) error {
if account.Type != accounts.AccountTypeGenerated && account.Type != accounts.AccountTypeSeed {
return errors.New("cannot create keystore file if account is not of `generated` or `seed` type")
}
if masterAddress == "" {
return errors.New("cannot create keystore file if master address is empty")
}
if password == "" {
return errors.New("cannot create keystore file if password is empty")
}
info, err := api.manager.AccountsGenerator().LoadAccount(masterAddress, password)
if err != nil {
return err
}
_, err = api.manager.AccountsGenerator().StoreDerivedAccounts(info.ID, password, []string{account.Path})
return err
}
func (api *API) AddAccount(ctx context.Context, password string, account *accounts.Account) error {
err := api.checkAccountValidity(account)
if err != nil {
return err
}
if account.Type != accounts.AccountTypeWatch {
kp, err := api.db.GetKeypairByKeyUID(account.KeyUID)
if err != nil {
if err == accounts.ErrDbKeypairNotFound {
return errors.New("cannot add an account for an unknown keypair")
}
return err
}
// we need to create local keystore file only if password is provided and the account is being added is of
// "generated" or "seed" type.
if (account.Type == accounts.AccountTypeGenerated || account.Type == accounts.AccountTypeSeed) && len(password) > 0 {
err = api.createKeystoreFileForAccount(kp.DerivedFrom, password, account)
if err != nil {
return err
}
}
}
if account.Type == accounts.AccountTypeGenerated {
account.AddressWasNotShown = true
}
return api.SaveAccount(ctx, account)
}
// Imports a new private key and creates local keystore file.
func (api *API) ImportPrivateKey(ctx context.Context, privateKey string, password string) error {
info, err := api.manager.AccountsGenerator().ImportPrivateKey(privateKey)
if err != nil {
return err
}
kp, err := api.db.GetKeypairByKeyUID(info.KeyUID)
if err != nil && err != accounts.ErrDbKeypairNotFound {
return err
}
if kp != nil {
return errors.New("provided private key was already imported")
}
_, err = api.manager.AccountsGenerator().StoreAccount(info.ID, password)
return err
}
// Creates all keystore files for a keypair and mark it in db as fully operable.
func (api *API) MakePrivateKeyKeypairFullyOperable(ctx context.Context, privateKey string, password string) error {
info, err := api.manager.AccountsGenerator().ImportPrivateKey(privateKey)
if err != nil {
return err
}
kp, err := api.db.GetKeypairByKeyUID(info.KeyUID)
if err != nil {
return err
}
if kp == nil {
return errors.New("keypair for the provided private key is not known")
}
_, err = api.manager.AccountsGenerator().StoreAccount(info.ID, password)
if err != nil {
return err
}
return (*api.messenger).MarkKeypairFullyOperable(info.KeyUID)
}
func (api *API) MakePartiallyOperableAccoutsFullyOperable(ctx context.Context, password string) (addresses []types.Address, err error) {
profileKeypair, err := api.db.GetProfileKeypair()
if err != nil {
return
}
if !profileKeypair.MigratedToKeycard() && !api.VerifyPassword(password) {
err = errors.New("wrong password provided")
return
}
keypairs, err := api.db.GetActiveKeypairs()
if err != nil {
return
}
for _, kp := range keypairs {
for _, acc := range kp.Accounts {
if acc.Operable != accounts.AccountPartiallyOperable {
continue
}
err = api.createKeystoreFileForAccount(kp.DerivedFrom, password, acc)
if err != nil {
return
}
err = api.db.MarkAccountFullyOperable(acc.Address)
if err != nil {
return
}
addresses = append(addresses, acc.Address)
}
}
return
}
// Imports a new mnemonic and creates local keystore file.
func (api *API) ImportMnemonic(ctx context.Context, mnemonic string, password string) error {
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
generatedAccountInfo, err := api.manager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
if err != nil {
return err
}
kp, err := api.db.GetKeypairByKeyUID(generatedAccountInfo.KeyUID)
if err != nil && err != accounts.ErrDbKeypairNotFound {
return err
}
if kp != nil {
return errors.New("provided mnemonic was already imported, to add new account use `AddAccount` endpoint")
}
_, err = api.manager.AccountsGenerator().StoreAccount(generatedAccountInfo.ID, password)
return err
}
// Creates all keystore files for a keypair and mark it in db as fully operable.
func (api *API) MakeSeedPhraseKeypairFullyOperable(ctx context.Context, mnemonic string, password string) error {
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
generatedAccountInfo, err := api.manager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
if err != nil {
return err
}
kp, err := api.db.GetKeypairByKeyUID(generatedAccountInfo.KeyUID)
if err != nil {
return err
}
if kp == nil {
return errors.New("keypair for the provided seed phrase is not known")
}
_, err = api.manager.AccountsGenerator().StoreAccount(generatedAccountInfo.ID, password)
if err != nil {
return err
}
var paths []string
for _, acc := range kp.Accounts {
paths = append(paths, acc.Path)
}
_, err = api.manager.AccountsGenerator().StoreDerivedAccounts(generatedAccountInfo.ID, password, paths)
if err != nil {
return err
}
return (*api.messenger).MarkKeypairFullyOperable(generatedAccountInfo.KeyUID)
}
// Creates a random new mnemonic.
func (api *API) GetRandomMnemonic(ctx context.Context) (string, error) {
return account.GetRandomMnemonic()
}
func (api *API) VerifyKeystoreFileForAccount(address types.Address, password string) bool {
_, err := api.manager.VerifyAccountPassword(api.config.KeyStoreDir, address.Hex(), password)
return err == nil
}
func (api *API) VerifyPassword(password string) bool {
address, err := api.db.GetChatAddress()
if err != nil {
return false
}
return api.VerifyKeystoreFileForAccount(address, password)
}
func (api *API) MigrateNonProfileKeycardKeypairToApp(ctx context.Context, mnemonic string, password string) error {
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
generatedAccountInfo, err := api.manager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
if err != nil {
return err
}
kp, err := api.db.GetKeypairByKeyUID(generatedAccountInfo.KeyUID)
if err != nil {
return err
}
if kp.Type == accounts.KeypairTypeProfile {
return errors.New("cannot migrate profile keypair")
}
if !kp.MigratedToKeycard() {
return errors.New("keypair being migrated is not a keycard keypair")
}
profileKeypair, err := api.db.GetProfileKeypair()
if err != nil {
return err
}
if !profileKeypair.MigratedToKeycard() && !api.VerifyPassword(password) {
return errors.New("wrong password provided")
}
_, err = api.manager.AccountsGenerator().StoreAccount(generatedAccountInfo.ID, password)
if err != nil {
return err
}
for _, acc := range kp.Accounts {
err = api.createKeystoreFileForAccount(kp.DerivedFrom, password, acc)
if err != nil {
return err
}
}
// this will emit SyncKeypair message
return (*api.messenger).DeleteAllKeycardsWithKeyUID(ctx, generatedAccountInfo.KeyUID)
}
// If keypair is migrated from keycard to app, then `accountsComingFromKeycard` should be set to true, otherwise false.
// If keycard is new `Position` will be determined and set by the backend and `KeycardLocked` will be set to false.
// If keycard is already added, `Position` and `KeycardLocked` will be unchanged.
func (api *API) SaveOrUpdateKeycard(ctx context.Context, keycard *accounts.Keycard, accountsComingFromKeycard bool) error {
if len(keycard.AccountsAddresses) == 0 {
return errors.New("cannot migrate a keypair without accounts")
}
kpDb, err := api.db.GetKeypairByKeyUID(keycard.KeyUID)
if err != nil {
if err == accounts.ErrDbKeypairNotFound {
return errors.New("cannot migrate an unknown keypair")
}
return err
}
err = (*api.messenger).SaveOrUpdateKeycard(ctx, keycard)
if err != nil {
return err
}
if !accountsComingFromKeycard {
// Once we migrate a keypair, corresponding keystore files need to be deleted
// if the keypair being migrated is not already migrated (in case user is creating a copy of an existing Keycard)
// and if keypair operability is different from non operable (otherwise there are not keystore files to be deleted).
if !kpDb.MigratedToKeycard() && kpDb.Operability() != accounts.AccountNonOperable {
for _, acc := range kpDb.Accounts {
if acc.Operable != accounts.AccountFullyOperable {
continue
}
err = api.manager.DeleteAccount(acc.Address)
if err != nil {
return err
}
}
err = api.manager.DeleteAccount(types.Address(common.HexToAddress(kpDb.DerivedFrom)))
if err != nil {
return err
}
}
err = (*api.messenger).MarkKeypairFullyOperable(keycard.KeyUID)
if err != nil {
return err
}
}
return nil
}
func (api *API) GetAllKnownKeycards(ctx context.Context) ([]*accounts.Keycard, error) {
return api.db.GetAllKnownKeycards()
}
func (api *API) GetKeycardsWithSameKeyUID(ctx context.Context, keyUID string) ([]*accounts.Keycard, error) {
return api.db.GetKeycardsWithSameKeyUID(keyUID)
}
func (api *API) GetKeycardByKeycardUID(ctx context.Context, keycardUID string) (*accounts.Keycard, error) {
return api.db.GetKeycardByKeycardUID(keycardUID)
}
func (api *API) SetKeycardName(ctx context.Context, keycardUID string, kpName string) error {
return (*api.messenger).SetKeycardName(ctx, keycardUID, kpName)
}
func (api *API) KeycardLocked(ctx context.Context, keycardUID string) error {
return (*api.messenger).KeycardLocked(ctx, keycardUID)
}
func (api *API) KeycardUnlocked(ctx context.Context, keycardUID string) error {
return (*api.messenger).KeycardUnlocked(ctx, keycardUID)
}
func (api *API) DeleteKeycardAccounts(ctx context.Context, keycardUID string, accountAddresses []types.Address) error {
return (*api.messenger).DeleteKeycardAccounts(ctx, keycardUID, accountAddresses)
}
func (api *API) DeleteKeycard(ctx context.Context, keycardUID string) error {
return (*api.messenger).DeleteKeycard(ctx, keycardUID)
}
func (api *API) DeleteAllKeycardsWithKeyUID(ctx context.Context, keyUID string) error {
return (*api.messenger).DeleteAllKeycardsWithKeyUID(ctx, keyUID)
}
func (api *API) UpdateKeycardUID(ctx context.Context, oldKeycardUID string, newKeycardUID string) error {
return (*api.messenger).UpdateKeycardUID(ctx, oldKeycardUID, newKeycardUID)
}
func (api *API) AddressWasShown(address types.Address) error {
return api.db.AddressWasShown(address)
}

View File

@@ -0,0 +1,19 @@
package accountsevent
import "github.com/ethereum/go-ethereum/common"
// EventType type for event types.
type EventType string
// Event is a type for accounts events.
type Event struct {
Type EventType `json:"type"`
Accounts []common.Address `json:"accounts"`
}
const (
// EventTypeAdded is emitted when a new account is added.
EventTypeAdded EventType = "added"
// EventTypeRemoved is emitted when an account is removed.
EventTypeRemoved EventType = "removed"
)

View File

@@ -0,0 +1,85 @@
package accountsevent
import (
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/services/wallet/async"
)
type AccountsChangeCb func(changedAddresses []common.Address, eventType EventType, currentAddresses []common.Address)
// Watcher executes a given callback whenever an account gets added/removed
type Watcher struct {
accountsDB *accounts.Database
accountFeed *event.Feed
group *async.Group
callback AccountsChangeCb
}
func NewWatcher(accountsDB *accounts.Database, accountFeed *event.Feed, callback AccountsChangeCb) *Watcher {
return &Watcher{
accountsDB: accountsDB,
accountFeed: accountFeed,
callback: callback,
}
}
func (w *Watcher) Start() {
if w.group != nil {
return
}
w.group = async.NewGroup(context.Background())
w.group.Add(func(ctx context.Context) error {
return watch(ctx, w.accountsDB, w.accountFeed, w.callback)
})
}
func (w *Watcher) Stop() {
if w.group != nil {
w.group.Stop()
w.group.Wait()
w.group = nil
}
}
func onAccountsChange(accountsDB *accounts.Database, callback AccountsChangeCb, changedAddresses []common.Address, eventType EventType) {
currentEthAddresses, err := accountsDB.GetWalletAddresses()
if err != nil {
log.Error("failed getting wallet addresses", "error", err)
return
}
currentAddresses := make([]common.Address, 0, len(currentEthAddresses))
for _, ethAddress := range currentEthAddresses {
currentAddresses = append(currentAddresses, common.Address(ethAddress))
}
if callback != nil {
callback(changedAddresses, eventType, currentAddresses)
}
}
func watch(ctx context.Context, accountsDB *accounts.Database, accountFeed *event.Feed, callback AccountsChangeCb) error {
ch := make(chan Event, 1)
sub := accountFeed.Subscribe(ch)
defer sub.Unsubscribe()
for {
select {
case <-ctx.Done():
return nil
case err := <-sub.Err():
if err != nil {
log.Error("accounts watcher subscription failed", "error", err)
}
case ev := <-ch:
onAccountsChange(accountsDB, callback, ev.Accounts, ev.Type)
}
}
}

View File

@@ -0,0 +1,93 @@
package accounts
import (
"errors"
"github.com/status-im/status-go/timesource"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts"
)
var (
// ErrUpdatingWrongAccount raised if caller tries to update any other account except one used for login.
ErrUpdatingWrongAccount = errors.New("failed to update wrong account. Please login with that account first")
)
func NewMultiAccountsAPI(db *multiaccounts.Database, mediaServer *server.MediaServer) *MultiAccountsAPI {
return &MultiAccountsAPI{db: db, mediaServer: mediaServer}
}
// MultiAccountsAPI is class with methods available over RPC.
type MultiAccountsAPI struct {
db *multiaccounts.Database
mediaServer *server.MediaServer
}
func (api *MultiAccountsAPI) UpdateAccount(account multiaccounts.Account) error {
oldAcc, err := api.db.GetAccount(account.KeyUID)
if err != nil {
return err
}
if oldAcc == nil {
return errors.New("UpdateAccount but account not found")
}
if oldAcc.CustomizationColor != account.CustomizationColor {
updatedAt := timesource.GetCurrentTimeInMillis()
account.CustomizationColorClock = updatedAt
}
return api.db.UpdateAccount(account)
}
//
// Profile Images
//
// GetIdentityImages returns an array of json marshalled IdentityImages assigned to the user's identity
func (api *MultiAccountsAPI) GetIdentityImages(keyUID string) ([]*images.IdentityImage, error) {
return api.db.GetIdentityImages(keyUID)
}
// GetIdentityImage returns a json object representing the image with the given name
func (api *MultiAccountsAPI) GetIdentityImage(keyUID, name string) (*images.IdentityImage, error) {
return api.db.GetIdentityImage(keyUID, name)
}
// StoreIdentityImage takes the filepath of an image, crops it as per the rect coords and finally resizes the image.
// The resulting image(s) will be stored in the DB along with other user account information.
// aX and aY represent the pixel coordinates of the upper left corner of the image's cropping area
// bX and bY represent the pixel coordinates of the lower right corner of the image's cropping area
func (api *MultiAccountsAPI) StoreIdentityImage(keyUID, filepath string, aX, aY, bX, bY int) ([]images.IdentityImage, error) {
iis, err := images.GenerateIdentityImages(filepath, aX, aY, bX, bY)
if err != nil {
return nil, err
}
err = api.db.StoreIdentityImages(keyUID, iis, true)
if err != nil {
return nil, err
}
return iis, err
}
func (api *MultiAccountsAPI) StoreIdentityImageFromURL(keyUID, url string) ([]images.IdentityImage, error) {
iis, err := images.GenerateIdentityImagesFromURL(url)
if err != nil {
return nil, err
}
err = api.db.StoreIdentityImages(keyUID, iis, true)
if err != nil {
return nil, err
}
return iis, err
}
// DeleteIdentityImage deletes an IdentityImage from the db with the given name
func (api *MultiAccountsAPI) DeleteIdentityImage(keyUID string) error {
return api.db.DeleteIdentityImage(keyUID)
}

View File

@@ -0,0 +1 @@
{"ClusterConfig":{"Enabled":true,"Fleet":"eth.beta","BootNodes":["enode://7427dfe38bd4cf7c58bb96417806fab25782ec3e6046a8053370022cbaa281536e8d64ecd1b02e1f8f72768e295d06258ba43d88304db068e6f2417ae8bcb9a6@104.154.88.123:443","enode://e8a7c03b58911e98bbd66accb2a55d57683f35b23bf9dfca89e5e244eb5cc3f25018b4112db507faca34fb69ffb44b362f79eda97a669a8df29c72e654416784@47.91.224.35:443","enode://5395aab7833f1ecb671b59bf0521cf20224fe8162fc3d2675de4ee4d5636a75ec32d13268fc184df8d1ddfa803943906882da62a4df42d4fccf6d17808156a87@206.189.243.57:443","enode://43947863cfa5aad1178f482ac35a8ebb9116cded1c23f7f9af1a47badfc1ee7f0dd9ec0543417cc347225a6e47e46c6873f647559e43434596c54e17a4d3a1e4@47.52.74.140:443"],"TrustedMailServers":["enode://744098ab6d3308af5cd03920aea60c46d16b2cd3d33bf367cbaf1d01c2fcd066ff8878576d0967897cd7dbb0e63f873cc0b4f7e4b0f1d7222e6b3451a78d9bda@47.89.20.15:443","enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@206.189.243.168:443","enode://7de99e4cb1b3523bd26ca212369540646607c721ad4f3e5c821ed9148150ce6ce2e72631723002210fac1fd52dfa8bbdf3555e05379af79515e1179da37cc3db@35.188.19.210:443","enode://da61e9eff86a56633b635f887d8b91e0ff5236bbc05b8169834292e92afb92929dcf6efdbf373a37903da8fe0384d5a0a8247e83f1ce211aa429200b6d28c548@47.91.156.93:443","enode://74957e361ab290e6af45a124536bc9adee39fbd2f995a77ace6ed7d05d9a1c7c98b78b2df5f8071c439b9c0afe4a69893ede4ad633473f96bc195ddf33f6ce00@47.52.255.195:443","enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@206.189.243.162:443"],"StaticNodes":["enode://887cbd92d95afc2c5f1e227356314a53d3d18855880ac0509e0c0870362aee03939d4074e6ad31365915af41d34320b5094bfcc12a67c381788cd7298d06c875@206.189.243.177:443","enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@206.189.243.172:443"],"RendezvousNodes":["/ip4/206.189.243.57/tcp/30703/ethv4/16Uiu2HAmLqTXuY4Sb6G28HNooaFUXUKzpzKXCcgyJxgaEE2i5vnf","/ip4/174.138.105.243/tcp/30703/ethv4/16Uiu2HAmRHPzF3rQg55PgYPcQkyvPVH9n2hWsYPhUJBZ6kVjJgdV"]},"DataDir":"/ethereum/mainnet_rpc_dev","LogLevel":"INFO","Rendezvous":true,"WhisperConfig":{"Enabled":true,"LightClient":true,"MinimumPoW":0.001,"EnableNTPSync":true},"LogEnabled":true,"BrowsersConfig":{"Enabled":true},"RequireTopics":{"whisper":{"Min":2,"Max":2}},"UpstreamConfig":{"Enabled":true,"URL":"https://mainnet.infura.io/v3/f315575765b14720b32382a61a89341a"},"ListenAddr":":30304","PermissionsConfig":{"Enabled":true},"NetworkId":1,"Name":"StatusIM","NoDiscovery":false,"ShhextConfig":{"BackupDisabledDataDir":"/data/user/0/im.status.ethereum.debug/files/../no_backup","InstallationID":"cf40e6c9-262f-5a76-9621-7b6fe0a91cd2","MaxMessageDeliveryAttempts":6,"MailServerConfirmations":true,"DataSyncEnabled":false,"DisableGenericDiscoveryTopic":false,"SendV1Messages":false,"PFSEnabled":true},"WalletConfig":{"Enabled":true},"StatusAccountsConfig":{"Enabled":true}}

View File

@@ -0,0 +1,97 @@
package accounts
import (
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
)
// NewService initializes service instance.
func NewService(db *accounts.Database, mdb *multiaccounts.Database, manager *account.GethManager, config *params.NodeConfig, feed *event.Feed, mediaServer *server.MediaServer) *Service {
return &Service{db, mdb, manager, config, feed, nil, mediaServer}
}
// Service is a browsers service.
type Service struct {
db *accounts.Database
mdb *multiaccounts.Database
manager *account.GethManager
config *params.NodeConfig
feed *event.Feed
messenger *protocol.Messenger
mediaServer *server.MediaServer
}
func (s *Service) Init(messenger *protocol.Messenger) {
s.messenger = messenger
}
// Start a service.
func (s *Service) Start() error {
return s.manager.InitKeystore(s.config.KeyStoreDir)
}
// Stop a service.
func (s *Service) Stop() error {
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "settings",
Version: "0.1.0",
Service: NewSettingsAPI(&s.messenger, s.db, s.config),
},
{
Namespace: "accounts",
Version: "0.1.0",
Service: s.AccountsAPI(),
},
{
Namespace: "multiaccounts",
Version: "0.1.0",
Service: NewMultiAccountsAPI(s.mdb, s.mediaServer),
},
}
}
func (s *Service) AccountsAPI() *API {
return NewAccountsAPI(s.manager, s.config, s.db, s.feed, &s.messenger)
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}
func (s *Service) GetKeypairByKeyUID(keyUID string) (*accounts.Keypair, error) {
return s.db.GetKeypairByKeyUID(keyUID)
}
func (s *Service) GetSettings() (settings.Settings, error) {
return s.db.GetSettings()
}
func (s *Service) GetMessenger() *protocol.Messenger {
return s.messenger
}
func (s *Service) VerifyPassword(password string) bool {
address, err := s.db.GetChatAddress()
if err != nil {
return false
}
_, err = s.manager.VerifyAccountPassword(s.config.KeyStoreDir, address.Hex(), password)
return err == nil
}

View File

@@ -0,0 +1,193 @@
package accounts
import (
"context"
"errors"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/nodecfg"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/identity"
)
func NewSettingsAPI(messenger **protocol.Messenger, db *accounts.Database, config *params.NodeConfig) *SettingsAPI {
return &SettingsAPI{messenger, db, config}
}
// SettingsAPI is class with methods available over RPC.
type SettingsAPI struct {
messenger **protocol.Messenger
db *accounts.Database
config *params.NodeConfig
}
func (api *SettingsAPI) SaveSetting(ctx context.Context, typ string, val interface{}) error {
// NOTE(Ferossgp): v0.62.0 Backward compatibility, skip this for older clients instead of returning error
if typ == "waku-enabled" {
return nil
}
err := api.db.SaveSetting(typ, val)
if err != nil {
return err
}
return nil
}
func (api *SettingsAPI) GetSettings(ctx context.Context) (settings.Settings, error) {
return api.db.GetSettings()
}
// NodeConfig returns the currently used node configuration
func (api *SettingsAPI) NodeConfig(ctx context.Context) (*params.NodeConfig, error) {
return api.config, nil
}
// Saves the nodeconfig in the database. The node must be restarted for the changes to be applied
func (api *SettingsAPI) SaveNodeConfig(ctx context.Context, n *params.NodeConfig) error {
return nodecfg.SaveNodeConfig(api.db.DB(), n)
}
// Notifications Settings
func (api *SettingsAPI) NotificationsGetAllowNotifications() (bool, error) {
return api.db.GetAllowNotifications()
}
func (api *SettingsAPI) NotificationsSetAllowNotifications(value bool) error {
return api.db.SetAllowNotifications(value)
}
func (api *SettingsAPI) NotificationsGetOneToOneChats() (string, error) {
return api.db.GetOneToOneChats()
}
func (api *SettingsAPI) NotificationsSetOneToOneChats(value string) error {
return api.db.SetOneToOneChats(value)
}
func (api *SettingsAPI) NotificationsGetGroupChats() (string, error) {
return api.db.GetGroupChats()
}
func (api *SettingsAPI) NotificationsSetGroupChats(value string) error {
return api.db.SetGroupChats(value)
}
func (api *SettingsAPI) NotificationsGetPersonalMentions() (string, error) {
return api.db.GetPersonalMentions()
}
func (api *SettingsAPI) NotificationsSetPersonalMentions(value string) error {
return api.db.SetPersonalMentions(value)
}
func (api *SettingsAPI) NotificationsGetGlobalMentions() (string, error) {
return api.db.GetGlobalMentions()
}
func (api *SettingsAPI) NotificationsSetGlobalMentions(value string) error {
return api.db.SetGlobalMentions(value)
}
func (api *SettingsAPI) NotificationsGetAllMessages() (string, error) {
return api.db.GetAllMessages()
}
func (api *SettingsAPI) NotificationsSetAllMessages(value string) error {
return api.db.SetAllMessages(value)
}
func (api *SettingsAPI) NotificationsGetContactRequests() (string, error) {
return api.db.GetContactRequests()
}
func (api *SettingsAPI) NotificationsSetContactRequests(value string) error {
return api.db.SetContactRequests(value)
}
func (api *SettingsAPI) NotificationsGetIdentityVerificationRequests() (string, error) {
return api.db.GetIdentityVerificationRequests()
}
func (api *SettingsAPI) NotificationsSetIdentityVerificationRequests(value string) error {
return api.db.SetIdentityVerificationRequests(value)
}
func (api *SettingsAPI) NotificationsGetSoundEnabled() (bool, error) {
return api.db.GetSoundEnabled()
}
func (api *SettingsAPI) NotificationsSetSoundEnabled(value bool) error {
return api.db.SetSoundEnabled(value)
}
func (api *SettingsAPI) NotificationsGetVolume() (int, error) {
return api.db.GetVolume()
}
func (api *SettingsAPI) NotificationsSetVolume(value int) error {
return api.db.SetVolume(value)
}
func (api *SettingsAPI) NotificationsGetMessagePreview() (int, error) {
return api.db.GetMessagePreview()
}
func (api *SettingsAPI) NotificationsSetMessagePreview(value int) error {
return api.db.SetMessagePreview(value)
}
// Notifications Settings - Exemption settings
func (api *SettingsAPI) NotificationsGetExMuteAllMessages(id string) (bool, error) {
return api.db.GetExMuteAllMessages(id)
}
func (api *SettingsAPI) NotificationsGetExPersonalMentions(id string) (string, error) {
return api.db.GetExPersonalMentions(id)
}
func (api *SettingsAPI) NotificationsGetExGlobalMentions(id string) (string, error) {
return api.db.GetExGlobalMentions(id)
}
func (api *SettingsAPI) NotificationsGetExOtherMessages(id string) (string, error) {
return api.db.GetExOtherMessages(id)
}
func (api *SettingsAPI) NotificationsSetExemptions(id string, muteAllMessages bool, personalMentions string,
globalMentions string, otherMessages string) error {
return api.db.SetExemptions(id, muteAllMessages, personalMentions, globalMentions, otherMessages)
}
func (api *SettingsAPI) DeleteExemptions(id string) error {
return api.db.DeleteExemptions(id)
}
// Deprecated: Use api.go/SetBio instead
func (api *SettingsAPI) SetBio(bio string) error {
return (*api.messenger).SetBio(bio)
}
func (api *SettingsAPI) GetSocialLinks() (identity.SocialLinks, error) {
return api.db.GetSocialLinks()
}
func (api *SettingsAPI) AddOrReplaceSocialLinks(links identity.SocialLinks) error {
for _, link := range links {
if len(link.Text) == 0 {
return errors.New("`Text` field of a social link must be set")
}
if len(link.URL) == 0 {
return errors.New("`URL` field of a social link must be set")
}
}
return (*api.messenger).AddOrReplaceSocialLinks(links)
}
func (api *SettingsAPI) MnemonicWasShown() error {
return api.db.MnemonicWasShown()
}

View File

@@ -0,0 +1,17 @@
package settingsevent
import "github.com/status-im/status-go/multiaccounts/settings"
// EventType type for event types.
type EventType string
// Event is a type for accounts events.
type Event struct {
Type EventType `json:"type"`
Setting settings.SettingField `json:"setting"`
Value interface{} `json:"value"`
}
const (
EventTypeChanged EventType = "changed"
)

View File

@@ -0,0 +1,72 @@
package settingsevent
import (
"context"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/services/wallet/async"
)
type SettingChangeCb func(setting settings.SettingField, value interface{})
// Watcher executes a given callback whenever an account gets added/removed
type Watcher struct {
feed *event.Feed
group *async.Group
callback SettingChangeCb
}
func NewWatcher(feed *event.Feed, callback SettingChangeCb) *Watcher {
return &Watcher{
feed: feed,
callback: callback,
}
}
func (w *Watcher) Start() {
if w.group != nil {
return
}
w.group = async.NewGroup(context.Background())
w.group.Add(func(ctx context.Context) error {
return watch(ctx, w.feed, w.callback)
})
}
func (w *Watcher) Stop() {
if w.group != nil {
w.group.Stop()
w.group.Wait()
w.group = nil
}
}
func onSettingChanged(callback SettingChangeCb, setting settings.SettingField, value interface{}) {
if callback != nil {
callback(setting, value)
}
}
func watch(ctx context.Context, feed *event.Feed, callback SettingChangeCb) error {
ch := make(chan Event, 1)
sub := feed.Subscribe(ch)
defer sub.Unsubscribe()
for {
select {
case <-ctx.Done():
return nil
case err := <-sub.Err():
if err != nil {
log.Error("settings watcher subscription failed", "error", err)
}
case ev := <-ch:
if ev.Type == EventTypeChanged {
onSettingChanged(callback, ev.Setting, ev.Value)
}
}
}
}

View File

@@ -0,0 +1,34 @@
package appmetrics
import (
"context"
"github.com/pborman/uuid"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/appmetrics"
)
func NewAPI(db *appmetrics.Database) *API {
return &API{db: db, sessionID: uuid.NewRandom().String()}
}
type API struct {
db *appmetrics.Database
sessionID string
}
func (api *API) ValidateAppMetrics(ctx context.Context, appMetrics []appmetrics.AppMetric) error {
log.Debug("[AppMetricsAPI::ValidateAppMetrics]")
return api.db.ValidateAppMetrics(appMetrics)
}
func (api *API) SaveAppMetrics(ctx context.Context, appMetrics []appmetrics.AppMetric) error {
log.Debug("[AppMetricsAPI::SaveAppMetrics]")
return api.db.SaveAppMetrics(appMetrics, api.sessionID)
}
func (api *API) GetAppMetrics(ctx context.Context, limit int, offset int) (appmetrics.Page, error) {
log.Debug("[AppMetricsAPI::GetAppMetrics]")
return api.db.GetAppMetrics(limit, offset)
}

View File

@@ -0,0 +1,39 @@
package appmetrics
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/appmetrics"
)
func NewService(db *appmetrics.Database) *Service {
return &Service{db: db}
}
type Service struct {
db *appmetrics.Database
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "appmetrics",
Version: "0.1.0",
Service: NewAPI(s.db),
Public: true,
},
}
}
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,48 @@
Browsers Service
================
Browser service provides read/write API for browser object.
To enable include browsers config part and add `browsers` to APIModules:
```json
{
"BrowsersConfig": {
"Enabled": true,
},
APIModules: "browsers"
}
```
API
---
Enabling service will expose three additional methods:
#### browsers_addBrowser
Stores browser in the database.
All fields are specified below:
```json
{
"browser-id": "1",
"name": "first",
"timestamp": 10,
"dapp?": true,
"history-index": 1,
"history": [
"one",
"two"
]
}
```
#### browsers_getBrowsers
Reads all browsers, returns in the format specified above. List is sorted by timestamp.
#### browsers_deleteBrowser
Delete browser from database. Accepts browser `id`.

View File

@@ -0,0 +1,51 @@
package browsers
import (
"context"
"github.com/ethereum/go-ethereum/log"
)
func NewAPI(db *Database) *API {
return &API{db: db}
}
// API is class with methods available over RPC.
type API struct {
db *Database
}
func (api *API) GetBookmarks(ctx context.Context) ([]*Bookmark, error) {
log.Debug("call to get bookmarks")
rst, err := api.db.GetBookmarks()
log.Debug("result from database for bookmarks", "len", len(rst))
return rst, err
}
func (api *API) StoreBookmark(ctx context.Context, bookmark Bookmark) (Bookmark, error) {
log.Debug("call to create a bookmark")
bookmarkResult, err := api.db.StoreBookmark(bookmark)
log.Debug("result from database for creating a bookmark", "err", err)
return bookmarkResult, err
}
func (api *API) UpdateBookmark(ctx context.Context, originalURL string, bookmark Bookmark) error {
log.Debug("call to update a bookmark")
err := api.db.UpdateBookmark(originalURL, bookmark)
log.Debug("result from database for updating a bookmark", "err", err)
return err
}
func (api *API) DeleteBookmark(ctx context.Context, url string) error {
log.Debug("call to remove a bookmark")
err := api.db.DeleteBookmark(url)
log.Debug("result from database for remove a bookmark", "err", err)
return err
}
func (api *API) RemoveBookmark(ctx context.Context, url string) error {
log.Debug("call to remove a bookmark logically")
err := api.db.RemoveBookmark(url)
log.Debug("result from database for remove a bookmark logically", "err", err)
return err
}

View File

@@ -0,0 +1,200 @@
package browsers
import (
"context"
"database/sql"
"github.com/mat/besticon/besticon"
"github.com/ethereum/go-ethereum/log"
)
// Database sql wrapper for operations with browser objects.
type Database struct {
db *sql.DB
}
// Close closes database.
func (db Database) Close() error {
return db.db.Close()
}
func NewDB(db *sql.DB) *Database {
return &Database{db: db}
}
type BookmarksType string
type Bookmark struct {
URL string `json:"url"`
Name string `json:"name"`
ImageURL string `json:"imageUrl"`
Removed bool `json:"removed"`
Clock uint64 `json:"-"` //used to sync
DeletedAt uint64 `json:"deletedAt,omitempty"`
}
type Browser struct {
ID string `json:"browser-id"`
Name string `json:"name"`
Timestamp uint64 `json:"timestamp"`
Dapp bool `json:"dapp?"`
HistoryIndex int `json:"history-index"`
History []string `json:"history,omitempty"`
}
func (db *Database) GetBookmarks() ([]*Bookmark, error) {
rows, err := db.db.Query(`SELECT url, name, image_url, removed, deleted_at FROM bookmarks`)
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Bookmark
for rows.Next() {
bookmark := &Bookmark{}
err := rows.Scan(&bookmark.URL, &bookmark.Name, &bookmark.ImageURL, &bookmark.Removed, &bookmark.DeletedAt)
if err != nil {
return nil, err
}
rst = append(rst, bookmark)
}
return rst, nil
}
func (db *Database) StoreBookmark(bookmark Bookmark) (Bookmark, error) {
insert, err := db.db.Prepare("INSERT OR REPLACE INTO bookmarks (url, name, image_url, removed, clock, deleted_at) VALUES (?, ?, ?, ?, ?, ?)")
if err != nil {
return bookmark, err
}
// Get the right icon
finder := besticon.IconFinder{}
icons, iconError := finder.FetchIcons(bookmark.URL)
if iconError == nil && len(icons) > 0 {
icon := finder.IconInSizeRange(besticon.SizeRange{Min: 48, Perfect: 48, Max: 100})
if icon != nil {
bookmark.ImageURL = icon.URL
} else {
bookmark.ImageURL = icons[0].URL
}
} else {
log.Error("error getting the bookmark icon", "iconError", iconError)
}
_, err = insert.Exec(bookmark.URL, bookmark.Name, bookmark.ImageURL, bookmark.Removed, bookmark.Clock, bookmark.DeletedAt)
return bookmark, err
}
func (db *Database) StoreBookmarkWithoutFetchIcon(bookmark *Bookmark, tx *sql.Tx) (err error) {
if tx == nil {
tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
}
insert, err := tx.Prepare("INSERT OR REPLACE INTO bookmarks (url, name, image_url, removed, clock, deleted_at) VALUES (?, ?, ?, ?, ?, ?)")
if err != nil {
return err
}
defer insert.Close()
_, err = insert.Exec(bookmark.URL, bookmark.Name, bookmark.ImageURL, bookmark.Removed, bookmark.Clock, bookmark.DeletedAt)
return err
}
func (db *Database) StoreSyncBookmarks(bookmarks []*Bookmark) ([]*Bookmark, error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return nil, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
var storedBookmarks []*Bookmark
for _, bookmark := range bookmarks {
shouldSync, err := db.shouldSyncBookmark(bookmark, tx)
if err != nil {
return storedBookmarks, err
}
if shouldSync {
err := db.StoreBookmarkWithoutFetchIcon(bookmark, tx)
if err != nil {
return storedBookmarks, err
}
storedBookmarks = append(storedBookmarks, bookmark)
}
}
return storedBookmarks, nil
}
func (db *Database) shouldSyncBookmark(bookmark *Bookmark, tx *sql.Tx) (shouldSync bool, err error) {
if tx == nil {
tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return false, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
}
qr := tx.QueryRow(`SELECT 1 FROM bookmarks WHERE url = ? AND clock > ?`, bookmark.URL, bookmark.Clock)
var result int
err = qr.Scan(&result)
switch err {
case sql.ErrNoRows:
// Query does not match, therefore synced_at value is not older than the new clock value or id was not found
return true, nil
case nil:
// Error is nil, therefore query matched and synced_at is older than the new clock
return false, nil
default:
// Error is not nil and is not sql.ErrNoRows, therefore pass out the error
return false, err
}
}
func (db *Database) UpdateBookmark(originalURL string, bookmark Bookmark) error {
insert, err := db.db.Prepare("UPDATE bookmarks SET url = ?, name = ?, image_url = ?, removed = ?, clock = ?, deleted_at = ? WHERE url = ?")
if err != nil {
return err
}
_, err = insert.Exec(bookmark.URL, bookmark.Name, bookmark.ImageURL, bookmark.Removed, bookmark.Clock, bookmark.DeletedAt, originalURL)
return err
}
func (db *Database) DeleteBookmark(url string) error {
_, err := db.db.Exec(`DELETE FROM bookmarks WHERE url = ?`, url)
return err
}
func (db *Database) RemoveBookmark(url string) error {
_, err := db.db.Exec(`UPDATE bookmarks SET removed = 1 WHERE url = ?`, url)
return err
}

View File

@@ -0,0 +1,42 @@
package browsers
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// NewService initializes service instance.
func NewService(db *Database) *Service {
return &Service{db: db}
}
// Service is a browsers service.
type Service struct {
db *Database
}
// Start a service.
func (s *Service) Start() error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "browsers",
Version: "0.1.0",
Service: NewAPI(s.db),
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,558 @@
package chat
import (
"context"
"errors"
"strings"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/common/shard"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
v1protocol "github.com/status-im/status-go/protocol/v1"
)
var (
ErrChatNotFound = errors.New("can't find chat")
ErrCommunityNotFound = errors.New("can't find community")
ErrCommunitiesNotSupported = errors.New("communities are not supported")
ErrChatTypeNotSupported = errors.New("chat type not supported")
)
type ChannelGroupType string
const Personal ChannelGroupType = "personal"
const Community ChannelGroupType = "community"
type PinnedMessages struct {
Cursor string
PinnedMessages []*common.PinnedMessage
}
type Member struct {
// Community Role
Role protobuf.CommunityMember_Roles `json:"role,omitempty"`
// Joined indicates if the member has joined the group chat
Joined bool `json:"joined"`
}
type Chat struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
Emoji string `json:"emoji"`
Active bool `json:"active"`
ChatType protocol.ChatType `json:"chatType"`
Timestamp int64 `json:"timestamp"`
LastClockValue uint64 `json:"lastClockValue"`
DeletedAtClockValue uint64 `json:"deletedAtClockValue"`
ReadMessagesAtClockValue uint64 `json:"readMessagesAtClockValue"`
UnviewedMessagesCount uint `json:"unviewedMessagesCount"`
UnviewedMentionsCount uint `json:"unviewedMentionsCount"`
LastMessage *common.Message `json:"lastMessage"`
Members map[string]Member `json:"members,omitempty"`
MembershipUpdates []v1protocol.MembershipUpdateEvent `json:"membershipUpdateEvents"`
Alias string `json:"alias,omitempty"`
Identicon string `json:"identicon"`
Muted bool `json:"muted"`
InvitationAdmin string `json:"invitationAdmin,omitempty"`
ReceivedInvitationAdmin string `json:"receivedInvitationAdmin,omitempty"`
Profile string `json:"profile,omitempty"`
CommunityID string `json:"communityId"`
CategoryID string `json:"categoryId"`
Position int32 `json:"position,omitempty"`
Permissions *protobuf.CommunityPermissions `json:"permissions,omitempty"`
Joined int64 `json:"joined,omitempty"`
SyncedTo uint32 `json:"syncedTo,omitempty"`
SyncedFrom uint32 `json:"syncedFrom,omitempty"`
FirstMessageTimestamp uint32 `json:"firstMessageTimestamp,omitempty"`
Highlight bool `json:"highlight,omitempty"`
PinnedMessages *PinnedMessages `json:"pinnedMessages,omitempty"`
CanPost bool `json:"canPost"`
Base64Image string `json:"image,omitempty"`
}
type ChannelGroup struct {
Type ChannelGroupType `json:"channelGroupType"`
Name string `json:"name"`
Images map[string]images.IdentityImage `json:"images"`
Color string `json:"color"`
Chats map[string]*Chat `json:"chats"`
Categories map[string]communities.CommunityCategory `json:"categories"`
EnsName string `json:"ensName"`
MemberRole protobuf.CommunityMember_Roles `json:"memberRole"`
Verified bool `json:"verified"`
Description string `json:"description"`
IntroMessage string `json:"introMessage"`
OutroMessage string `json:"outroMessage"`
Tags []communities.CommunityTag `json:"tags"`
Permissions *protobuf.CommunityPermissions `json:"permissions"`
Members map[string]*protobuf.CommunityMember `json:"members"`
CanManageUsers bool `json:"canManageUsers"`
Muted bool `json:"muted"`
BanList []string `json:"banList"`
Encrypted bool `json:"encrypted"`
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
UnviewedMessagesCount int `json:"unviewedMessagesCount"`
UnviewedMentionsCount int `json:"unviewedMentionsCount"`
CheckChannelPermissionResponses map[string]*communities.CheckChannelPermissionsResponse `json:"checkChannelPermissionResponses"`
PubsubTopic string `json:"pubsubTopic"`
PubsubTopicKey string `json:"pubsubTopicKey"`
Shard *shard.Shard `json:"shard"`
}
func NewAPI(service *Service) *API {
return &API{
s: service,
log: log.New("package", "status-go/services/chat.API"),
}
}
type API struct {
s *Service
log log.Logger
}
func unique(communities []*communities.Community) (result []*communities.Community) {
inResult := make(map[string]bool)
for _, community := range communities {
if _, ok := inResult[community.IDString()]; !ok {
inResult[community.IDString()] = true
result = append(result, community)
}
}
return result
}
func (api *API) getChannelGroups(ctx context.Context, channelGroupID string) (map[string]ChannelGroup, error) {
joinedCommunities, err := api.s.messenger.JoinedCommunities()
if err != nil {
return nil, err
}
spectatedCommunities, err := api.s.messenger.SpectatedCommunities()
if err != nil {
return nil, err
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
result := make(map[string]ChannelGroup)
// Get chats from cache to get unviewed messages counts
channels := api.s.messenger.Chats()
totalUnviewedMessageCount := 0
totalUnviewedMentionsCount := 0
if channelGroupID == "" || channelGroupID == pubKey {
chats := make(map[string]*Chat)
for _, chat := range channels {
if !chat.IsActivePersonalChat() {
continue
}
if !chat.Muted || chat.UnviewedMentionsCount > 0 {
totalUnviewedMessageCount += int(chat.UnviewedMessagesCount)
}
totalUnviewedMentionsCount += int(chat.UnviewedMentionsCount)
c, err := api.toAPIChat(chat, nil, pubKey, true)
if err != nil {
return nil, err
}
chats[chat.ID] = c
}
result[pubKey] = ChannelGroup{
Type: Personal,
Name: "",
Images: make(map[string]images.IdentityImage),
Color: "",
Chats: chats,
Categories: make(map[string]communities.CommunityCategory),
EnsName: "", // Not implemented yet in communities
MemberRole: protobuf.CommunityMember_ROLE_OWNER,
Verified: true,
Description: "",
IntroMessage: "",
OutroMessage: "",
Tags: []communities.CommunityTag{},
Permissions: &protobuf.CommunityPermissions{},
Muted: false,
CommunityTokensMetadata: []*protobuf.CommunityTokenMetadata{},
UnviewedMessagesCount: totalUnviewedMessageCount,
UnviewedMentionsCount: totalUnviewedMentionsCount,
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
}
}
if channelGroupID == pubKey {
// They asked for the personal channel group only, so we return now
return result, nil
}
for _, community := range unique(append(joinedCommunities, spectatedCommunities...)) {
if channelGroupID != "" && channelGroupID != community.IDString() {
continue
}
totalUnviewedMessageCount = 0
totalUnviewedMentionsCount = 0
for _, chat := range channels {
if chat.CommunityID != community.IDString() || !chat.Active {
continue
}
if !chat.Muted || chat.UnviewedMentionsCount > 0 {
totalUnviewedMessageCount += int(chat.UnviewedMessagesCount)
}
totalUnviewedMentionsCount += int(chat.UnviewedMentionsCount)
}
chGrp := ChannelGroup{
Type: Community,
Name: community.Name(),
Color: community.Color(),
Images: make(map[string]images.IdentityImage),
Chats: make(map[string]*Chat),
Categories: make(map[string]communities.CommunityCategory),
MemberRole: community.MemberRole(community.MemberIdentity()),
Verified: community.Verified(),
Description: community.DescriptionText(),
IntroMessage: community.IntroMessage(),
OutroMessage: community.OutroMessage(),
Tags: community.Tags(),
Permissions: community.Description().Permissions,
Members: community.Description().Members,
CanManageUsers: community.CanManageUsers(community.MemberIdentity()),
Muted: community.Muted(),
BanList: community.Description().BanList,
Encrypted: community.Encrypted(),
CommunityTokensMetadata: community.Description().CommunityTokensMetadata,
UnviewedMessagesCount: totalUnviewedMessageCount,
UnviewedMentionsCount: totalUnviewedMentionsCount,
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
PubsubTopic: community.PubsubTopic(),
PubsubTopicKey: community.PubsubTopicKey(),
Shard: community.Shard(),
}
for t, i := range community.Images() {
chGrp.Images[t] = images.IdentityImage{Name: t, Payload: i.Payload}
}
for _, cat := range community.Categories() {
chGrp.Categories[cat.CategoryId] = communities.CommunityCategory{
ID: cat.CategoryId,
Name: cat.Name,
Position: int(cat.Position),
}
}
for _, chat := range channels {
if chat.CommunityID == community.IDString() && chat.Active {
_, exists := community.Chats()[chat.CommunityChatID()]
if !exists {
api.log.Warn("Chat not found in the community", "chat.ID", chat.ID)
continue
}
c, err := api.toAPIChat(chat, community, pubKey, true)
if err != nil {
return nil, err
}
chGrp.Chats[c.ID] = c
}
}
response, err := api.s.messenger.GetCommunityCheckChannelPermissionResponses(community.ID())
if err != nil {
return nil, err
}
chGrp.CheckChannelPermissionResponses = response.Channels
result[community.IDString()] = chGrp
if channelGroupID == community.IDString() {
// We asked for this particular community, so we return now
return result, nil
}
}
return result, nil
}
func (api *API) GetChannelGroups(ctx context.Context) (map[string]ChannelGroup, error) {
return api.getChannelGroups(ctx, "")
}
func (api *API) GetChannelGroupByID(ctx context.Context, channelGroupID string) (map[string]ChannelGroup, error) {
return api.getChannelGroups(ctx, channelGroupID)
}
func (api *API) GetChat(ctx context.Context, communityID types.HexBytes, chatID string) (*Chat, error) {
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
messengerChat, community, err := api.getChatAndCommunity(pubKey, communityID, chatID)
if err != nil {
return nil, err
}
if messengerChat == nil {
return nil, ErrChatNotFound
}
result, err := api.toAPIChat(messengerChat, community, pubKey, false)
if err != nil {
return nil, err
}
return result, nil
}
func (api *API) GetMembers(ctx context.Context, communityID types.HexBytes, chatID string) (map[string]Member, error) {
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
messengerChat, community, err := api.getChatAndCommunity(pubKey, communityID, chatID)
if err != nil {
return nil, err
}
return getChatMembers(messengerChat, community, pubKey)
}
func (api *API) JoinChat(ctx context.Context, communityID types.HexBytes, chatID string) (*Chat, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
response, err := api.s.messenger.CreatePublicChat(&requests.CreatePublicChat{ID: chatID})
if err != nil {
return nil, err
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
return api.toAPIChat(response.Chats()[0], nil, pubKey, false)
}
func (api *API) toAPIChat(protocolChat *protocol.Chat, community *communities.Community, pubKey string, skipPinnedMessages bool) (*Chat, error) {
chat := &Chat{
ID: strings.TrimPrefix(protocolChat.ID, protocolChat.CommunityID),
Name: protocolChat.Name,
Description: protocolChat.Description,
Color: protocolChat.Color,
Emoji: protocolChat.Emoji,
Active: protocolChat.Active,
ChatType: protocolChat.ChatType,
Timestamp: protocolChat.Timestamp,
LastClockValue: protocolChat.LastClockValue,
DeletedAtClockValue: protocolChat.DeletedAtClockValue,
ReadMessagesAtClockValue: protocolChat.ReadMessagesAtClockValue,
UnviewedMessagesCount: protocolChat.UnviewedMessagesCount,
UnviewedMentionsCount: protocolChat.UnviewedMentionsCount,
LastMessage: protocolChat.LastMessage,
MembershipUpdates: protocolChat.MembershipUpdates,
Alias: protocolChat.Alias,
Identicon: protocolChat.Identicon,
Muted: protocolChat.Muted,
InvitationAdmin: protocolChat.InvitationAdmin,
ReceivedInvitationAdmin: protocolChat.ReceivedInvitationAdmin,
Profile: protocolChat.Profile,
CommunityID: protocolChat.CommunityID,
CategoryID: protocolChat.CategoryID,
Joined: protocolChat.Joined,
SyncedTo: protocolChat.SyncedTo,
SyncedFrom: protocolChat.SyncedFrom,
FirstMessageTimestamp: protocolChat.FirstMessageTimestamp,
Highlight: protocolChat.Highlight,
Base64Image: protocolChat.Base64Image,
}
if protocolChat.OneToOne() {
chat.Name = "" // Emptying since it contains non useful data
}
if !skipPinnedMessages {
pinnedMessages, cursor, err := api.s.messenger.PinnedMessageByChatID(protocolChat.ID, "", -1)
if err != nil {
return nil, err
}
if len(pinnedMessages) != 0 {
chat.PinnedMessages = &PinnedMessages{
Cursor: cursor,
PinnedMessages: pinnedMessages,
}
}
}
err := chat.populateCommunityFields(community)
if err != nil {
return nil, err
}
chatMembers, err := getChatMembers(protocolChat, community, pubKey)
if err != nil {
return nil, err
}
chat.Members = chatMembers
return chat, nil
}
func getChatMembers(sourceChat *protocol.Chat, community *communities.Community, userPubKey string) (map[string]Member, error) {
result := make(map[string]Member)
if sourceChat != nil {
if sourceChat.ChatType == protocol.ChatTypePrivateGroupChat && len(sourceChat.Members) > 0 {
for _, m := range sourceChat.Members {
result[m.ID] = Member{
Role: func() protobuf.CommunityMember_Roles {
if m.Admin {
return protobuf.CommunityMember_ROLE_OWNER
}
return protobuf.CommunityMember_ROLE_NONE
}(),
Joined: true,
}
}
return result, nil
}
if sourceChat.ChatType == protocol.ChatTypeOneToOne {
result[sourceChat.ID] = Member{
Joined: true,
}
result[userPubKey] = Member{
Joined: true,
}
return result, nil
}
}
if community != nil {
channel, exists := community.Chats()[sourceChat.CommunityChatID()]
if !exists {
// Skip unknown community chats. They might be channels that were deleted. We shouldn't get here
return result, nil
}
for member := range channel.Members {
pubKey, err := common.HexToPubkey(member)
if err != nil {
return nil, err
}
result[member] = Member{
Role: community.MemberRole(pubKey),
Joined: community.Joined(),
}
}
return result, nil
}
return nil, nil
}
func (api *API) getCommunityByID(id string) (*communities.Community, error) {
communityID, err := hexutil.Decode(id)
if err != nil {
return nil, err
}
community, err := api.s.messenger.GetCommunityByID(communityID)
if community == nil && err == nil {
return nil, ErrCommunityNotFound
}
return community, err
}
func (chat *Chat) populateCommunityFields(community *communities.Community) error {
if community == nil {
return nil
}
commChat, exists := community.Chats()[chat.ID]
if !exists {
// Skip unknown community chats. They might be channels that were deleted
return nil
}
canPost, err := community.CanMemberIdentityPost(chat.ID)
if err != nil {
return err
}
chat.CategoryID = commChat.CategoryId
chat.Position = commChat.Position
chat.Permissions = commChat.Permissions
chat.Emoji = commChat.Identity.Emoji
chat.Name = commChat.Identity.DisplayName
chat.Description = commChat.Identity.Description
chat.CanPost = canPost
return nil
}
func (api *API) getChatAndCommunity(pubKey string, communityID types.HexBytes, chatID string) (*protocol.Chat, *communities.Community, error) {
fullChatID := chatID
if string(communityID.Bytes()) == pubKey { // Obtaining chats from personal
communityID = []byte{}
}
if len(communityID) != 0 {
id := string(communityID.Bytes())
if chatID == "" {
community, err := api.getCommunityByID(id)
return nil, community, err
}
fullChatID = id + chatID
}
messengerChat := api.s.messenger.Chat(fullChatID)
if messengerChat == nil {
return nil, nil, ErrChatNotFound
}
var community *communities.Community
if messengerChat.CommunityID != "" {
var err error
community, err = api.getCommunityByID(messengerChat.CommunityID)
if err != nil {
return nil, nil, err
}
}
return messengerChat, community, nil
}
func (api *API) EditChat(ctx context.Context, communityID types.HexBytes, chatID string, name string, color string, image images.CroppedImage) (*Chat, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
chatToEdit := api.s.messenger.Chat(chatID)
if chatToEdit == nil {
return nil, ErrChatNotFound
}
if chatToEdit.ChatType != protocol.ChatTypePrivateGroupChat {
return nil, ErrChatTypeNotSupported
}
response, err := api.s.messenger.EditGroupChat(ctx, chatID, name, color, image)
if err != nil {
return nil, err
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
return api.toAPIChat(response.Chats()[0], nil, pubKey, false)
}

View File

@@ -0,0 +1,236 @@
package chat
import (
"context"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/requests"
)
type GroupChatResponse struct {
Chat *Chat `json:"chat"`
Messages []*common.Message `json:"messages"`
}
type GroupChatResponseWithInvitations struct {
Chat *Chat `json:"chat"`
Messages []*common.Message `json:"messages"`
Invitations []*protocol.GroupChatInvitation `json:"invitations"`
}
type CreateOneToOneChatResponse struct {
Chat *Chat `json:"chat,omitempty"`
Contact *protocol.Contact `json:"contact,omitempty"`
}
type StartGroupChatResponse struct {
Chat *Chat `json:"chat,omitempty"`
Contacts []*protocol.Contact `json:"contacts"`
Messages []*common.Message `json:"messages,omitempty"`
}
func (api *API) CreateOneToOneChat(ctx context.Context, communityID types.HexBytes, ID types.HexBytes, ensName string) (*CreateOneToOneChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
response, err := api.s.messenger.CreateOneToOneChat(&requests.CreateOneToOneChat{ID: ID, ENSName: ensName})
if err != nil {
return nil, err
}
chat, err := api.toAPIChat(response.Chats()[0], nil, pubKey, false)
if err != nil {
return nil, err
}
var contact *protocol.Contact
if ensName != "" {
contact = response.Contacts[0]
}
return &CreateOneToOneChatResponse{
Chat: chat,
Contact: contact,
}, nil
}
func (api *API) CreateGroupChat(ctx context.Context, communityID types.HexBytes, name string, members []string) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.CreateGroupChatWithMembers(ctx, name, members)
})
}
func (api *API) CreateGroupChatFromInvitation(communityID types.HexBytes, name string, chatID string, adminPK string) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.CreateGroupChatFromInvitation(name, chatID, adminPK)
})
}
func (api *API) LeaveChat(ctx context.Context, communityID types.HexBytes, chatID string, remove bool) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.LeaveGroupChat(ctx, chatID, remove)
})
}
func (api *API) AddMembers(ctx context.Context, communityID types.HexBytes, chatID string, members []string) (*GroupChatResponseWithInvitations, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponseWithInvitations(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.AddMembersToGroupChat(ctx, chatID, members)
})
}
func (api *API) RemoveMember(ctx context.Context, communityID types.HexBytes, chatID string, member string) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.RemoveMembersFromGroupChat(ctx, chatID, []string{member})
})
}
func (api *API) MakeAdmin(ctx context.Context, communityID types.HexBytes, chatID string, member string) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.AddAdminsToGroupChat(ctx, chatID, []string{member})
})
}
func (api *API) RenameChat(ctx context.Context, communityID types.HexBytes, chatID string, name string) (*GroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponse(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.ChangeGroupChatName(ctx, chatID, name)
})
}
func (api *API) SendGroupChatInvitationRequest(ctx context.Context, communityID types.HexBytes, chatID string, adminPK string, message string) (*GroupChatResponseWithInvitations, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
return api.execAndGetGroupChatResponseWithInvitations(func() (*protocol.MessengerResponse, error) {
return api.s.messenger.SendGroupChatInvitationRequest(ctx, chatID, adminPK, message)
})
}
func (api *API) GetGroupChatInvitations() ([]*protocol.GroupChatInvitation, error) {
return api.s.messenger.GetGroupChatInvitations()
}
func (api *API) SendGroupChatInvitationRejection(ctx context.Context, invitationRequestID string) ([]*protocol.GroupChatInvitation, error) {
response, err := api.s.messenger.SendGroupChatInvitationRejection(ctx, invitationRequestID)
if err != nil {
return nil, err
}
return response.Invitations, nil
}
func (api *API) StartGroupChat(ctx context.Context, communityID types.HexBytes, name string, members []string) (*StartGroupChatResponse, error) {
if len(communityID) != 0 {
return nil, ErrCommunitiesNotSupported
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
var response *protocol.MessengerResponse
var err error
if len(members) == 1 {
memberPk, err := common.HexToPubkey(members[0])
if err != nil {
return nil, err
}
response, err = api.s.messenger.CreateOneToOneChat(&requests.CreateOneToOneChat{
ID: types.HexBytes(crypto.FromECDSAPub(memberPk)),
})
if err != nil {
return nil, err
}
} else {
response, err = api.s.messenger.CreateGroupChatWithMembers(ctx, name, members)
if err != nil {
return nil, err
}
}
chat, err := api.toAPIChat(response.Chats()[0], nil, pubKey, false)
if err != nil {
return nil, err
}
return &StartGroupChatResponse{
Chat: chat,
Contacts: response.Contacts,
Messages: response.Messages(),
}, nil
}
func (api *API) toGroupChatResponse(pubKey string, response *protocol.MessengerResponse) (*GroupChatResponse, error) {
chat, err := api.toAPIChat(response.Chats()[0], nil, pubKey, false)
if err != nil {
return nil, err
}
return &GroupChatResponse{
Chat: chat,
Messages: response.Messages(),
}, nil
}
func (api *API) toGroupChatResponseWithInvitations(pubKey string, response *protocol.MessengerResponse) (*GroupChatResponseWithInvitations, error) {
g, err := api.toGroupChatResponse(pubKey, response)
if err != nil {
return nil, err
}
return &GroupChatResponseWithInvitations{
Chat: g.Chat,
Messages: g.Messages,
Invitations: response.Invitations,
}, nil
}
func (api *API) execAndGetGroupChatResponse(fn func() (*protocol.MessengerResponse, error)) (*GroupChatResponse, error) {
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
response, err := fn()
if err != nil {
return nil, err
}
return api.toGroupChatResponse(pubKey, response)
}
func (api *API) execAndGetGroupChatResponseWithInvitations(fn func() (*protocol.MessengerResponse, error)) (*GroupChatResponseWithInvitations, error) {
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
response, err := fn()
if err != nil {
return nil, err
}
return api.toGroupChatResponseWithInvitations(pubKey, response)
}

View File

@@ -0,0 +1,161 @@
package chat
import (
"context"
"github.com/forPelevin/gomoji"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
type SendMessageResponse struct {
Chat *Chat `json:"chat"`
Messages []*common.Message `json:"messages"`
}
func (api *API) SendSticker(ctx context.Context, communityID types.HexBytes, chatID string, packID int32, hash string, responseTo string) (*SendMessageResponse, error) {
ensName, _ := api.s.accountsDB.GetPreferredUsername()
msg := &common.Message{
CommunityID: string(communityID.Bytes()),
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
ContentType: protobuf.ChatMessage_STICKER,
Text: "Update to latest version to see a nice sticker here!",
Payload: &protobuf.ChatMessage_Sticker{
Sticker: &protobuf.StickerMessage{
Hash: hash,
Pack: packID,
},
},
ResponseTo: responseTo,
EnsName: ensName,
},
}
response, err := api.s.messenger.SendChatMessage(ctx, msg)
if err != nil {
return nil, err
}
return api.toSendMessageResponse(response)
}
func (api *API) toSendMessageResponse(response *protocol.MessengerResponse) (*SendMessageResponse, error) {
protocolChat := response.Chats()[0]
community, err := api.s.messenger.GetCommunityByID(types.HexBytes(protocolChat.CommunityID))
if err != nil {
return nil, err
}
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
chat, err := api.toAPIChat(protocolChat, community, pubKey, false)
if err != nil {
return nil, err
}
return &SendMessageResponse{
Chat: chat,
Messages: response.Messages(),
}, nil
}
func isTextOrEmoji(text string) protobuf.ChatMessage_ContentType {
contentType := protobuf.ChatMessage_TEXT_PLAIN
if gomoji.RemoveEmojis(text) == "" && len(gomoji.FindAll(text)) != 0 {
contentType = protobuf.ChatMessage_EMOJI
}
return contentType
}
func (api *API) SendMessage(ctx context.Context, communityID types.HexBytes, chatID string, text string, responseTo string) (*SendMessageResponse, error) {
ensName, _ := api.s.accountsDB.GetPreferredUsername()
msg := &common.Message{
CommunityID: string(communityID.Bytes()),
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
ContentType: isTextOrEmoji(text),
Text: text,
ResponseTo: responseTo,
EnsName: ensName,
},
}
response, err := api.s.messenger.SendChatMessage(ctx, msg)
if err != nil {
return nil, err
}
return api.toSendMessageResponse(response)
}
func (api *API) SendImages(ctx context.Context, communityID types.HexBytes, chatID string, imagePaths []string, text string, responseTo string) (*SendMessageResponse, error) {
ensName, _ := api.s.accountsDB.GetPreferredUsername()
var messages []*common.Message
for _, imagePath := range imagePaths {
messages = append(messages, &common.Message{
CommunityID: string(communityID.Bytes()),
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
ContentType: protobuf.ChatMessage_IMAGE,
Text: "Update to latest version to see a nice image here!",
ResponseTo: responseTo,
EnsName: ensName,
},
ImagePath: imagePath,
})
}
if text != "" {
messages = append(messages, &common.Message{
CommunityID: string(communityID.Bytes()),
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
ContentType: isTextOrEmoji(text),
Text: text,
ResponseTo: responseTo,
EnsName: ensName,
},
})
}
response, err := api.s.messenger.SendChatMessages(ctx, messages)
if err != nil {
return nil, err
}
return api.toSendMessageResponse(response)
}
func (api *API) SendAudio(ctx context.Context, communityID types.HexBytes, chatID string, audioPath string, responseTo string) (*SendMessageResponse, error) {
ensName, _ := api.s.accountsDB.GetPreferredUsername()
msg := &common.Message{
CommunityID: string(communityID.Bytes()),
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
Text: "Update to latest version to listen to an audio message here!",
ContentType: protobuf.ChatMessage_AUDIO,
ResponseTo: responseTo,
EnsName: ensName,
},
AudioPath: audioPath,
}
response, err := api.s.messenger.SendChatMessage(ctx, msg)
if err != nil {
return nil, err
}
return api.toSendMessageResponse(response)
}

View File

@@ -0,0 +1,46 @@
package chat
import (
"github.com/ethereum/go-ethereum/p2p"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/protocol"
)
func NewService(accountsDB *accounts.Database) *Service {
return &Service{
accountsDB: accountsDB,
}
}
type Service struct {
messenger *protocol.Messenger
accountsDB *accounts.Database
}
func (s *Service) Init(messenger *protocol.Messenger) {
s.messenger = messenger
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) APIs() []gethrpc.API {
return []gethrpc.API{
{
Namespace: "chat",
Version: "0.1.0",
Service: NewAPI(s),
},
}
}
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,914 @@
package communitytokens
import (
"context"
"errors"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer"
"github.com/status-im/status-go/contracts/community-tokens/mastertoken"
"github.com/status-im/status-go/contracts/community-tokens/ownertoken"
communityownertokenregistry "github.com/status-im/status-go/contracts/community-tokens/registry"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/utils"
"github.com/status-im/status-go/services/wallet/bigint"
wcommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/transactions"
)
func NewAPI(s *Service) *API {
return &API{
s: s,
}
}
type API struct {
s *Service
}
type DeploymentDetails struct {
ContractAddress string `json:"contractAddress"`
TransactionHash string `json:"transactionHash"`
}
const maxSupply = 999999999
type DeploymentParameters struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Supply *bigint.BigInt `json:"supply"`
InfiniteSupply bool `json:"infiniteSupply"`
Transferable bool `json:"transferable"`
RemoteSelfDestruct bool `json:"remoteSelfDestruct"`
TokenURI string `json:"tokenUri"`
OwnerTokenAddress string `json:"ownerTokenAddress"`
MasterTokenAddress string `json:"masterTokenAddress"`
}
func (d *DeploymentParameters) GetSupply() *big.Int {
if d.InfiniteSupply {
return d.GetInfiniteSupply()
}
return d.Supply.Int
}
// infinite supply for ERC721 is 2^256-1
func (d *DeploymentParameters) GetInfiniteSupply() *big.Int {
return GetInfiniteSupply()
}
func GetInfiniteSupply() *big.Int {
max := new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil)
max.Sub(max, big.NewInt(1))
return max
}
func (d *DeploymentParameters) Validate(isAsset bool) error {
if len(d.Name) <= 0 {
return errors.New("empty collectible name")
}
if len(d.Symbol) <= 0 {
return errors.New("empty collectible symbol")
}
var maxForType = big.NewInt(maxSupply)
if isAsset {
assetMultiplier, _ := big.NewInt(0).SetString("1000000000000000000", 10)
maxForType = maxForType.Mul(maxForType, assetMultiplier)
}
if !d.InfiniteSupply && (d.Supply.Cmp(big.NewInt(0)) < 0 || d.Supply.Cmp(maxForType) > 0) {
return fmt.Errorf("wrong supply value: %v", d.Supply)
}
return nil
}
func (api *API) DeployCollectibles(ctx context.Context, chainID uint64, deploymentParameters DeploymentParameters, txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := deploymentParameters.Validate(false)
if err != nil {
return DeploymentDetails{}, err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
address, tx, _, err := collectibles.DeployCollectibles(transactOpts, ethClient, deploymentParameters.Name,
deploymentParameters.Symbol, deploymentParameters.GetSupply(),
deploymentParameters.RemoteSelfDestruct, deploymentParameters.Transferable,
deploymentParameters.TokenURI, common.HexToAddress(deploymentParameters.OwnerTokenAddress),
common.HexToAddress(deploymentParameters.MasterTokenAddress))
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.DeployCommunityToken,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return DeploymentDetails{}, err
}
return DeploymentDetails{address.Hex(), tx.Hash().Hex()}, nil
}
func decodeSignature(sig []byte) (r [32]byte, s [32]byte, v uint8, err error) {
if len(sig) != crypto.SignatureLength {
return [32]byte{}, [32]byte{}, 0, fmt.Errorf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)
}
copy(r[:], sig[:32])
copy(s[:], sig[32:64])
v = sig[64] + 27
return r, s, v, nil
}
func prepareDeploymentSignatureStruct(signature string, communityID string, addressFrom common.Address) (communitytokendeployer.CommunityTokenDeployerDeploymentSignature, error) {
r, s, v, err := decodeSignature(common.FromHex(signature))
if err != nil {
return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err
}
communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err
}
communitySignature := communitytokendeployer.CommunityTokenDeployerDeploymentSignature{
V: v,
R: r,
S: s,
Deployer: addressFrom,
Signer: communityEthAddress,
}
return communitySignature, nil
}
func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64,
ownerTokenParameters DeploymentParameters, masterTokenParameters DeploymentParameters,
signature string, communityID string, signerPubKey string,
txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := ownerTokenParameters.Validate(false)
if err != nil {
return DeploymentDetails{}, err
}
if len(signerPubKey) <= 0 {
return DeploymentDetails{}, fmt.Errorf("signerPubKey is empty")
}
err = masterTokenParameters.Validate(false)
if err != nil {
return DeploymentDetails{}, err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return DeploymentDetails{}, err
}
ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: ownerTokenParameters.Name,
Symbol: ownerTokenParameters.Symbol,
BaseURI: ownerTokenParameters.TokenURI,
}
masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: masterTokenParameters.Name,
Symbol: masterTokenParameters.Symbol,
BaseURI: masterTokenParameters.TokenURI,
}
communitySignature, err := prepareDeploymentSignatureStruct(signature, communityID, common.Address(txArgs.From))
if err != nil {
return DeploymentDetails{}, err
}
log.Debug("Signature:", communitySignature)
tx, err := deployerContractInst.Deploy(transactOpts, ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey))
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.DeployOwnerToken,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return DeploymentDetails{}, err
}
return DeploymentDetails{"", tx.Hash().Hex()}, nil
}
func (api *API) GetMasterTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return "", err
}
receipt, err := ethClient.TransactionReceipt(ctx, common.HexToHash(txHash))
if err != nil {
return "", err
}
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
logMasterTokenCreatedSig := []byte("DeployMasterToken(address)")
logMasterTokenCreatedSigHash := crypto.Keccak256Hash(logMasterTokenCreatedSig)
for _, vLog := range receipt.Logs {
if vLog.Topics[0].Hex() == logMasterTokenCreatedSigHash.Hex() {
event, err := deployerContractInst.ParseDeployMasterToken(*vLog)
if err != nil {
return "", err
}
return event.Arg0.Hex(), nil
}
}
return "", fmt.Errorf("can't find master token address in transaction: %v", txHash)
}
func (api *API) GetOwnerTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return "", err
}
receipt, err := ethClient.TransactionReceipt(ctx, common.HexToHash(txHash))
if err != nil {
return "", err
}
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
logOwnerTokenCreatedSig := []byte("DeployOwnerToken(address)")
logOwnerTokenCreatedSigHash := crypto.Keccak256Hash(logOwnerTokenCreatedSig)
for _, vLog := range receipt.Logs {
if vLog.Topics[0].Hex() == logOwnerTokenCreatedSigHash.Hex() {
event, err := deployerContractInst.ParseDeployOwnerToken(*vLog)
if err != nil {
return "", err
}
return event.Arg0.Hex(), nil
}
}
return "", fmt.Errorf("can't find owner token address in transaction: %v", txHash)
}
func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentParameters DeploymentParameters, txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := deploymentParameters.Validate(true)
if err != nil {
return DeploymentDetails{}, err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
const decimals = 18
address, tx, _, err := assets.DeployAssets(transactOpts, ethClient, deploymentParameters.Name,
deploymentParameters.Symbol, decimals, deploymentParameters.GetSupply(),
deploymentParameters.TokenURI,
common.HexToAddress(deploymentParameters.OwnerTokenAddress),
common.HexToAddress(deploymentParameters.MasterTokenAddress))
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.DeployCommunityToken,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return DeploymentDetails{}, err
}
return DeploymentDetails{address.Hex(), tx.Hash().Hex()}, nil
}
// Returns gas units + 10%
func (api *API) DeployCollectiblesEstimate(ctx context.Context) (uint64, error) {
// TODO investigate why the code below does not return correct values
/*ethClient, err := api.s.manager.rpcClient.EthClient(420)
if err != nil {
log.Error(err.Error())
return 0, err
}
collectiblesABI, err := abi.JSON(strings.NewReader(collectibles.CollectiblesABI))
if err != nil {
return 0, err
}
data, err := collectiblesABI.Pack("", "name", "SYMBOL", big.NewInt(20), true, false, "tokenUriwhcih is very long asdkfjlsdkjflk",
common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"))
if err != nil {
return 0, err
}
callMsg := ethereum.CallMsg{
From: common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"),
To: nil,
Value: big.NewInt(0),
Data: data,
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil*/
// TODO compute fee dynamically
// the code above returns too low fees, need to investigate
gasAmount := uint64(2500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
}
// Returns gas units + 10%
func (api *API) DeployAssetsEstimate(ctx context.Context) (uint64, error) {
// TODO compute fee dynamically
gasAmount := uint64(1500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
}
func (api *API) DeployOwnerTokenEstimate(ctx context.Context, chainID uint64, fromAddress string,
ownerTokenParameters DeploymentParameters, masterTokenParameters DeploymentParameters,
signature string, communityID string, signerPubKey string) (uint64, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return 0, err
}
deployerAddress, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return 0, err
}
deployerABI, err := abi.JSON(strings.NewReader(communitytokendeployer.CommunityTokenDeployerABI))
if err != nil {
return 0, err
}
ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: ownerTokenParameters.Name,
Symbol: ownerTokenParameters.Symbol,
BaseURI: ownerTokenParameters.TokenURI,
}
masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: masterTokenParameters.Name,
Symbol: masterTokenParameters.Symbol,
BaseURI: masterTokenParameters.TokenURI,
}
communitySignature, err := prepareDeploymentSignatureStruct(signature, communityID, common.HexToAddress(fromAddress))
if err != nil {
return 0, err
}
data, err := deployerABI.Pack("deploy", ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey))
if err != nil {
return 0, err
}
toAddr := deployerAddress
fromAddr := common.HexToAddress(fromAddress)
callMsg := ethereum.CallMsg{
From: fromAddr,
To: &toAddr,
Value: big.NewInt(0),
Data: data,
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil
}
func (api *API) NewMasterTokenInstance(chainID uint64, contractAddress string) (*mastertoken.MasterToken, error) {
backend, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return mastertoken.NewMasterToken(common.HexToAddress(contractAddress), backend)
}
func (api *API) NewOwnerTokenInstance(chainID uint64, contractAddress string) (*ownertoken.OwnerToken, error) {
return api.s.NewOwnerTokenInstance(chainID, contractAddress)
}
func (api *API) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) {
return api.s.manager.NewCommunityTokenDeployerInstance(chainID)
}
func (api *API) NewCommunityOwnerTokenRegistryInstance(chainID uint64, contractAddress string) (*communityownertokenregistry.CommunityOwnerTokenRegistry, error) {
return api.s.NewCommunityOwnerTokenRegistryInstance(chainID, contractAddress)
}
func (api *API) NewCollectiblesInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
return api.s.manager.NewCollectiblesInstance(chainID, contractAddress)
}
func (api *API) NewAssetsInstance(chainID uint64, contractAddress string) (*assets.Assets, error) {
return api.s.manager.NewAssetsInstance(chainID, contractAddress)
}
// if we want to mint 2 tokens to addresses ["a", "b"] we need to mint
// twice to every address - we need to send to smart contract table ["a", "a", "b", "b"]
func (api *API) multiplyWalletAddresses(amount *bigint.BigInt, contractAddresses []string) []string {
var totalAddresses []string
for i := big.NewInt(1); i.Cmp(amount.Int) <= 0; {
totalAddresses = append(totalAddresses, contractAddresses...)
i.Add(i, big.NewInt(1))
}
return totalAddresses
}
func (api *API) PrepareMintCollectiblesData(walletAddresses []string, amount *bigint.BigInt) []common.Address {
totalAddresses := api.multiplyWalletAddresses(amount, walletAddresses)
var usersAddresses = []common.Address{}
for _, k := range totalAddresses {
usersAddresses = append(usersAddresses, common.HexToAddress(k))
}
return usersAddresses
}
// Universal minting function for every type of token.
func (api *API) MintTokens(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, walletAddresses []string, amount *bigint.BigInt) (string, error) {
err := api.ValidateWalletsAndAmounts(walletAddresses, amount)
if err != nil {
return "", err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
contractInst, err := NewTokenInstance(api, chainID, contractAddress)
if err != nil {
return "", err
}
tx, err := contractInst.Mint(transactOpts, walletAddresses, amount)
if err != nil {
return "", err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.AirdropCommunityToken,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
return tx.Hash().Hex(), nil
}
func (api *API) EstimateMintTokens(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) {
tokenType, err := api.s.db.GetTokenType(chainID, contractAddress)
if err != nil {
return 0, err
}
switch tokenType {
case protobuf.CommunityTokenType_ERC721:
return api.EstimateMintCollectibles(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount)
case protobuf.CommunityTokenType_ERC20:
return api.EstimateMintAssets(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount)
default:
return 0, fmt.Errorf("unknown token type: %v", tokenType)
}
}
func (api *API) EstimateMintCollectibles(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) {
err := api.ValidateWalletsAndAmounts(walletAddresses, amount)
if err != nil {
return 0, err
}
usersAddresses := api.PrepareMintCollectiblesData(walletAddresses, amount)
return api.estimateMethod(ctx, chainID, contractAddress, fromAddress, "mintTo", usersAddresses)
}
func (api *API) PrepareMintAssetsData(walletAddresses []string, amount *bigint.BigInt) ([]common.Address, []*big.Int) {
var usersAddresses = []common.Address{}
var amountsList = []*big.Int{}
for _, k := range walletAddresses {
usersAddresses = append(usersAddresses, common.HexToAddress(k))
amountsList = append(amountsList, amount.Int)
}
return usersAddresses, amountsList
}
// Estimate MintAssets cost.
func (api *API) EstimateMintAssets(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) {
err := api.ValidateWalletsAndAmounts(walletAddresses, amount)
if err != nil {
return 0, err
}
usersAddresses, amountsList := api.PrepareMintAssetsData(walletAddresses, amount)
return api.estimateMethod(ctx, chainID, contractAddress, fromAddress, "mintTo", usersAddresses, amountsList)
}
// This is only ERC721 function
func (api *API) RemoteDestructedAmount(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
// total supply = airdropped only (w/o burnt)
totalSupply, err := contractInst.TotalSupply(callOpts)
if err != nil {
return nil, err
}
// minted = all created tokens (airdropped and remotely destructed)
mintedCount, err := contractInst.MintedCount(callOpts)
if err != nil {
return nil, err
}
var res = new(big.Int)
res.Sub(mintedCount, totalSupply)
return &bigint.BigInt{Int: res}, nil
}
// This is only ERC721 function
func (api *API) RemoteBurn(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, tokenIds []*bigint.BigInt) (string, error) {
err := api.validateTokens(tokenIds)
if err != nil {
return "", err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
var tempTokenIds []*big.Int
for _, v := range tokenIds {
tempTokenIds = append(tempTokenIds, v.Int)
}
contractInst, err := NewTokenInstance(api, chainID, contractAddress)
if err != nil {
return "", err
}
tx, err := contractInst.RemoteBurn(transactOpts, tempTokenIds)
if err != nil {
return "", err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.RemoteDestructCollectible,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
return tx.Hash().Hex(), nil
}
// This is only ERC721 function
func (api *API) EstimateRemoteBurn(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, tokenIds []*bigint.BigInt) (uint64, error) {
err := api.validateTokens(tokenIds)
if err != nil {
return 0, err
}
var tempTokenIds []*big.Int
for _, v := range tokenIds {
tempTokenIds = append(tempTokenIds, v.Int)
}
return api.estimateMethod(ctx, chainID, contractAddress, fromAddress, "remoteBurn", tempTokenIds)
}
func (api *API) GetCollectiblesContractInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
return api.s.manager.GetCollectiblesContractInstance(chainID, contractAddress)
}
func (api *API) GetAssetContractInstance(chainID uint64, contractAddress string) (*assets.Assets, error) {
return api.s.manager.GetAssetContractInstance(chainID, contractAddress)
}
func (api *API) RemainingSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) {
tokenType, err := api.s.db.GetTokenType(chainID, contractAddress)
if err != nil {
return nil, err
}
switch tokenType {
case protobuf.CommunityTokenType_ERC721:
return api.remainingCollectiblesSupply(ctx, chainID, contractAddress)
case protobuf.CommunityTokenType_ERC20:
return api.remainingAssetsSupply(ctx, chainID, contractAddress)
default:
return nil, fmt.Errorf("unknown token type: %v", tokenType)
}
}
// RemainingSupply = MaxSupply - MintedCount
func (api *API) remainingCollectiblesSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
maxSupply, err := contractInst.MaxSupply(callOpts)
if err != nil {
return nil, err
}
mintedCount, err := contractInst.MintedCount(callOpts)
if err != nil {
return nil, err
}
var res = new(big.Int)
res.Sub(maxSupply, mintedCount)
return &bigint.BigInt{Int: res}, nil
}
// RemainingSupply = MaxSupply - TotalSupply
func (api *API) remainingAssetsSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewAssetsInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
maxSupply, err := contractInst.MaxSupply(callOpts)
if err != nil {
return nil, err
}
totalSupply, err := contractInst.TotalSupply(callOpts)
if err != nil {
return nil, err
}
var res = new(big.Int)
res.Sub(maxSupply, totalSupply)
return &bigint.BigInt{Int: res}, nil
}
func (api *API) maxSupplyCollectibles(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return contractInst.MaxSupply(callOpts)
}
func (api *API) maxSupplyAssets(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewAssetsInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return contractInst.MaxSupply(callOpts)
}
func (api *API) maxSupply(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) {
tokenType, err := api.s.db.GetTokenType(chainID, contractAddress)
if err != nil {
return nil, err
}
switch tokenType {
case protobuf.CommunityTokenType_ERC721:
return api.maxSupplyCollectibles(ctx, chainID, contractAddress)
case protobuf.CommunityTokenType_ERC20:
return api.maxSupplyAssets(ctx, chainID, contractAddress)
default:
return nil, fmt.Errorf("unknown token type: %v", tokenType)
}
}
func (api *API) prepareNewMaxSupply(ctx context.Context, chainID uint64, contractAddress string, burnAmount *bigint.BigInt) (*big.Int, error) {
maxSupply, err := api.maxSupply(ctx, chainID, contractAddress)
if err != nil {
return nil, err
}
var newMaxSupply = new(big.Int)
newMaxSupply.Sub(maxSupply, burnAmount.Int)
return newMaxSupply, nil
}
func (api *API) Burn(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, burnAmount *bigint.BigInt) (string, error) {
err := api.validateBurnAmount(ctx, burnAmount, chainID, contractAddress)
if err != nil {
return "", err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
newMaxSupply, err := api.prepareNewMaxSupply(ctx, chainID, contractAddress, burnAmount)
if err != nil {
return "", err
}
contractInst, err := NewTokenInstance(api, chainID, contractAddress)
if err != nil {
return "", err
}
tx, err := contractInst.SetMaxSupply(transactOpts, newMaxSupply)
if err != nil {
return "", err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.BurnCommunityToken,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
return tx.Hash().Hex(), nil
}
func (api *API) EstimateBurn(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, burnAmount *bigint.BigInt) (uint64, error) {
err := api.validateBurnAmount(ctx, burnAmount, chainID, contractAddress)
if err != nil {
return 0, err
}
newMaxSupply, err := api.prepareNewMaxSupply(ctx, chainID, contractAddress, burnAmount)
if err != nil {
return 0, err
}
return api.estimateMethod(ctx, chainID, contractAddress, fromAddress, "setMaxSupply", newMaxSupply)
}
func (api *API) ValidateWalletsAndAmounts(walletAddresses []string, amount *bigint.BigInt) error {
if len(walletAddresses) == 0 {
return errors.New("wallet addresses list is empty")
}
if amount.Cmp(big.NewInt(0)) <= 0 {
return errors.New("amount is <= 0")
}
return nil
}
func (api *API) validateTokens(tokenIds []*bigint.BigInt) error {
if len(tokenIds) == 0 {
return errors.New("token list is empty")
}
return nil
}
func (api *API) validateBurnAmount(ctx context.Context, burnAmount *bigint.BigInt, chainID uint64, contractAddress string) error {
if burnAmount.Cmp(big.NewInt(0)) <= 0 {
return errors.New("burnAmount is less than 0")
}
remainingSupply, err := api.RemainingSupply(ctx, chainID, contractAddress)
if err != nil {
return err
}
if burnAmount.Cmp(remainingSupply.Int) > 1 {
return errors.New("burnAmount is bigger than remaining amount")
}
return nil
}
func (api *API) estimateMethodForTokenInstance(ctx context.Context, contractInstance TokenInstance, chainID uint64, contractAddress string, fromAddress string, methodName string, args ...interface{}) (uint64, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return 0, err
}
data, err := contractInstance.PackMethod(ctx, methodName, args...)
if err != nil {
return 0, err
}
toAddr := common.HexToAddress(contractAddress)
fromAddr := common.HexToAddress(fromAddress)
callMsg := ethereum.CallMsg{
From: fromAddr,
To: &toAddr,
Value: big.NewInt(0),
Data: data,
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil
}
func (api *API) estimateMethod(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, methodName string, args ...interface{}) (uint64, error) {
contractInst, err := NewTokenInstance(api, chainID, contractAddress)
if err != nil {
return 0, err
}
return api.estimateMethodForTokenInstance(ctx, contractInst, chainID, contractAddress, fromAddress, methodName, args...)
}
// Gets signer public key from smart contract with a given chainId and address
func (api *API) GetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string) (string, error) {
return api.s.GetSignerPubKey(ctx, chainID, contractAddress)
}
// Gets signer public key directly from deployer contract
func (api *API) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) {
return api.s.SafeGetSignerPubKey(ctx, chainID, communityID)
}
// Gets owner token contract address from deployer contract
func (api *API) SafeGetOwnerTokenAddress(ctx context.Context, chainID uint64, communityID string) (string, error) {
return api.s.SafeGetOwnerTokenAddress(ctx, chainID, communityID)
}
func (api *API) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, newSignerPubKey string) (string, error) {
return api.s.SetSignerPubKey(ctx, chainID, contractAddress, txArgs, password, newSignerPubKey)
}
func (api *API) EstimateSetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, newSignerPubKey string) (uint64, error) {
if len(newSignerPubKey) <= 0 {
return 0, fmt.Errorf("signerPubKey is empty")
}
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return 0, err
}
ownerTokenInstance := &OwnerTokenInstance{instance: contractInst}
return api.estimateMethodForTokenInstance(ctx, ownerTokenInstance, chainID, contractAddress, fromAddress, "setSignerPublicKey", common.FromHex(newSignerPubKey))
}
func (api *API) OwnerTokenOwnerAddress(ctx context.Context, chainID uint64, contractAddress string) (string, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return "", err
}
ownerAddress, err := contractInst.OwnerOf(callOpts, big.NewInt(0))
if err != nil {
return "", err
}
return ownerAddress.Hex(), nil
}

View File

@@ -0,0 +1,8 @@
package communitytokens
import "github.com/status-im/status-go/services/wallet/bigint"
type AssetContractData struct {
TotalSupply *bigint.BigInt
InfiniteSupply bool
}

View File

@@ -0,0 +1,10 @@
package communitytokens
import "github.com/status-im/status-go/services/wallet/bigint"
type CollectibleContractData struct {
TotalSupply *bigint.BigInt
Transferable bool
RemoteBurnable bool
InfiniteSupply bool
}

View File

@@ -0,0 +1,66 @@
package communitytokens
import (
"database/sql"
"fmt"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/protobuf"
)
type Database struct {
db *sql.DB
}
func NewCommunityTokensDatabase(db *sql.DB) *Database {
return &Database{db: db}
}
func (db *Database) GetTokenType(chainID uint64, contractAddress string) (protobuf.CommunityTokenType, error) {
var result = protobuf.CommunityTokenType_UNKNOWN_TOKEN_TYPE
rows, err := db.db.Query(`SELECT type FROM community_tokens WHERE chain_id=? AND address=? LIMIT 1`, chainID, contractAddress)
if err != nil {
return result, err
}
defer rows.Close()
if rows.Next() {
err := rows.Scan(&result)
return result, err
}
return result, fmt.Errorf("can't find token: chainId %v, contractAddress %v", chainID, contractAddress)
}
func (db *Database) GetTokenPrivilegesLevel(chainID uint64, contractAddress string) (token.PrivilegesLevel, error) {
var result = token.CommunityLevel
rows, err := db.db.Query(`SELECT privileges_level FROM community_tokens WHERE chain_id=? AND address=? LIMIT 1`, chainID, contractAddress)
if err != nil {
return result, err
}
defer rows.Close()
if rows.Next() {
err := rows.Scan(&result)
return result, err
}
return result, fmt.Errorf("can't find privileges level: chainId %v, contractAddress %v", chainID, contractAddress)
}
func (db *Database) GetCommunityERC20Metadata() ([]*token.CommunityToken, error) {
rows, err := db.db.Query(`SELECT community_id, address, name, symbol, chain_id FROM community_tokens WHERE type = ?`, protobuf.CommunityTokenType_ERC20)
if err != nil {
return nil, err
}
defer rows.Close()
var result []*token.CommunityToken
for rows.Next() {
token := token.CommunityToken{}
err := rows.Scan(&token.CommunityID, &token.Address, &token.Name, &token.Symbol, &token.ChainID)
if err != nil {
return nil, err
}
result = append(result, &token)
}
return result, rows.Err()
}

View File

@@ -0,0 +1,208 @@
package communitytokens
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/wallet/bigint"
)
type Manager struct {
rpcClient *rpc.Client
}
func NewManager(rpcClient *rpc.Client) *Manager {
return &Manager{
rpcClient: rpcClient,
}
}
func (m *Manager) NewCollectiblesInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
backend, err := m.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return collectibles.NewCollectibles(common.HexToAddress(contractAddress), backend)
}
func (m *Manager) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) {
backend, err := m.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
deployerAddr, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return nil, err
}
return communitytokendeployer.NewCommunityTokenDeployer(deployerAddr, backend)
}
func (m *Manager) GetCollectiblesContractInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
contractInst, err := m.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return contractInst, nil
}
func (m *Manager) NewAssetsInstance(chainID uint64, contractAddress string) (*assets.Assets, error) {
backend, err := m.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return assets.NewAssets(common.HexToAddress(contractAddress), backend)
}
func (m *Manager) GetAssetContractInstance(chainID uint64, contractAddress string) (*assets.Assets, error) {
contractInst, err := m.NewAssetsInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return contractInst, nil
}
func (m *Manager) GetCollectibleContractData(chainID uint64, contractAddress string) (*CollectibleContractData, error) {
callOpts := &bind.CallOpts{Context: context.Background(), Pending: false}
contract, err := m.GetCollectiblesContractInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
totalSupply, err := contract.MaxSupply(callOpts)
if err != nil {
return nil, err
}
transferable, err := contract.Transferable(callOpts)
if err != nil {
return nil, err
}
remoteBurnable, err := contract.RemoteBurnable(callOpts)
if err != nil {
return nil, err
}
return &CollectibleContractData{
TotalSupply: &bigint.BigInt{Int: totalSupply},
Transferable: transferable,
RemoteBurnable: remoteBurnable,
InfiniteSupply: GetInfiniteSupply().Cmp(totalSupply) == 0,
}, nil
}
func (m *Manager) GetAssetContractData(chainID uint64, contractAddress string) (*AssetContractData, error) {
callOpts := &bind.CallOpts{Context: context.Background(), Pending: false}
contract, err := m.GetAssetContractInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
totalSupply, err := contract.MaxSupply(callOpts)
if err != nil {
return nil, err
}
return &AssetContractData{
TotalSupply: &bigint.BigInt{Int: totalSupply},
InfiniteSupply: GetInfiniteSupply().Cmp(totalSupply) == 0,
}, nil
}
func convert33BytesPubKeyToEthAddress(pubKey string) (common.Address, error) {
decoded, err := types.DecodeHex(pubKey)
if err != nil {
return common.Address{}, err
}
communityPubKey, err := crypto.DecompressPubkey(decoded)
if err != nil {
return common.Address{}, err
}
return common.Address(crypto.PubkeyToAddress(*communityPubKey)), nil
}
// Simpler version of hashing typed structured data alternative to typedStructuredDataHash. Keeping this for reference.
func customTypedStructuredDataHash(domainSeparator []byte, signatureTypedHash []byte, signer string, deployer string) types.Hash {
// every field should be 32 bytes, eth address is 20 bytes so padding should be added
emptyOffset := [12]byte{}
hashedEncoded := crypto.Keccak256Hash(signatureTypedHash, emptyOffset[:], common.HexToAddress(signer).Bytes(),
emptyOffset[:], common.HexToAddress(deployer).Bytes())
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, hashedEncoded.Bytes()))
return crypto.Keccak256Hash(rawData)
}
// Returns a typed structured hash according to https://eips.ethereum.org/EIPS/eip-712
// Domain separator from smart contract is used.
func typedStructuredDataHash(domainSeparator []byte, signer string, addressFrom string, deployerContractAddress string, chainID uint64) (types.Hash, error) {
myTypedData := apitypes.TypedData{
Types: apitypes.Types{
"Deploy": []apitypes.Type{
{Name: "signer", Type: "address"},
{Name: "deployer", Type: "address"},
},
"EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
},
PrimaryType: "Deploy",
// Domain field should be here to keep correct structure but
// domainSeparator from smart contract is used.
Domain: apitypes.TypedDataDomain{
Name: "CommunityTokenDeployer", // name from Deployer smart contract
Version: "1", // version from Deployer smart contract
ChainId: math.NewHexOrDecimal256(int64(chainID)),
VerifyingContract: deployerContractAddress,
},
Message: apitypes.TypedDataMessage{
"signer": signer,
"deployer": addressFrom,
},
}
typedDataHash, err := myTypedData.HashStruct(myTypedData.PrimaryType, myTypedData.Message)
if err != nil {
return types.Hash{}, err
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, string(typedDataHash)))
return crypto.Keccak256Hash(rawData), nil
}
// Creates
func (m *Manager) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
callOpts := &bind.CallOpts{Pending: false}
communityEthAddr, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return nil, err
}
deployerAddr, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return nil, err
}
deployerContractInst, err := m.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return nil, err
}
domainSeparator, err := deployerContractInst.DOMAINSEPARATOR(callOpts)
if err != nil {
return nil, err
}
structedHash, err := typedStructuredDataHash(domainSeparator[:], communityEthAddr.Hex(), addressFrom, deployerAddr.Hex(), chainID)
if err != nil {
return nil, err
}
return structedHash.Bytes(), nil
}

View File

@@ -0,0 +1,188 @@
package communitytokens
import (
"context"
"database/sql"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts/community-tokens/ownertoken"
communityownertokenregistry "github.com/status-im/status-go/contracts/community-tokens/registry"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/utils"
wcommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/transactions"
)
type ServiceInterface interface {
GetCollectibleContractData(chainID uint64, contractAddress string) (*CollectibleContractData, error)
SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, newSignerPubKey string) (string, error)
GetAssetContractData(chainID uint64, contractAddress string) (*AssetContractData, error)
SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error)
DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error)
}
// Collectibles service
type Service struct {
manager *Manager
accountsManager *account.GethManager
pendingTracker *transactions.PendingTxTracker
config *params.NodeConfig
db *Database
}
// Returns a new Collectibles Service.
func NewService(rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, config *params.NodeConfig, appDb *sql.DB) *Service {
return &Service{
manager: &Manager{rpcClient: rpcClient},
accountsManager: accountsManager,
pendingTracker: pendingTracker,
config: config,
db: NewCommunityTokensDatabase(appDb),
}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []ethRpc.API {
return []ethRpc.API{
{
Namespace: "communitytokens",
Version: "0.1.0",
Service: NewAPI(s),
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return nil
}
func (s *Service) NewCommunityOwnerTokenRegistryInstance(chainID uint64, contractAddress string) (*communityownertokenregistry.CommunityOwnerTokenRegistry, error) {
backend, err := s.manager.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return communityownertokenregistry.NewCommunityOwnerTokenRegistry(common.HexToAddress(contractAddress), backend)
}
func (s *Service) NewOwnerTokenInstance(chainID uint64, contractAddress string) (*ownertoken.OwnerToken, error) {
backend, err := s.manager.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return ownertoken.NewOwnerToken(common.HexToAddress(contractAddress), backend)
}
func (s *Service) GetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string) (string, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return "", err
}
signerPubKey, err := contractInst.SignerPublicKey(callOpts)
if err != nil {
return "", err
}
return types.ToHex(signerPubKey), nil
}
func (s *Service) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) {
// 1. Get Owner Token contract address from deployer contract - SafeGetOwnerTokenAddress()
ownerTokenAddr, err := s.SafeGetOwnerTokenAddress(ctx, chainID, communityID)
if err != nil {
return "", err
}
// 2. Get Signer from owner token contract - GetSignerPubKey()
return s.GetSignerPubKey(ctx, chainID, ownerTokenAddr)
}
func (s *Service) SafeGetOwnerTokenAddress(ctx context.Context, chainID uint64, communityID string) (string, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
deployerContractInst, err := s.manager.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
registryAddr, err := deployerContractInst.DeploymentRegistry(callOpts)
if err != nil {
return "", err
}
registryContractInst, err := s.NewCommunityOwnerTokenRegistryInstance(chainID, registryAddr.Hex())
if err != nil {
return "", err
}
communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return "", err
}
ownerTokenAddress, err := registryContractInst.GetEntry(callOpts, communityEthAddress)
return ownerTokenAddress.Hex(), err
}
func (s *Service) GetCollectibleContractData(chainID uint64, contractAddress string) (*CollectibleContractData, error) {
return s.manager.GetCollectibleContractData(chainID, contractAddress)
}
func (s *Service) GetAssetContractData(chainID uint64, contractAddress string) (*AssetContractData, error) {
return s.manager.GetAssetContractData(chainID, contractAddress)
}
func (s *Service) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
return s.manager.DeploymentSignatureDigest(chainID, addressFrom, communityID)
}
func (s *Service) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, newSignerPubKey string) (string, error) {
if len(newSignerPubKey) <= 0 {
return "", fmt.Errorf("signerPubKey is empty")
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, s.accountsManager, s.config.KeyStoreDir, txArgs.From, password))
contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return "", err
}
tx, err := contractInst.SetSignerPublicKey(transactOpts, common.FromHex(newSignerPubKey))
if err != nil {
return "", err
}
err = s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.SetSignerPublicKey,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
return tx.Hash().Hex(), nil
}

View File

@@ -0,0 +1,179 @@
package communitytokens
import (
"context"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
"github.com/status-im/status-go/contracts/community-tokens/mastertoken"
"github.com/status-im/status-go/contracts/community-tokens/ownertoken"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/wallet/bigint"
)
type TokenInstance interface {
RemoteBurn(*bind.TransactOpts, []*big.Int) (*types.Transaction, error)
Mint(*bind.TransactOpts, []string, *bigint.BigInt) (*types.Transaction, error)
SetMaxSupply(*bind.TransactOpts, *big.Int) (*types.Transaction, error)
PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error)
}
// Owner Token
type OwnerTokenInstance struct {
TokenInstance
instance *ownertoken.OwnerToken
}
func (t OwnerTokenInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("remote destruction for owner token not implemented")
}
func (t OwnerTokenInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) {
return nil, fmt.Errorf("minting for owner token not implemented")
}
func (t OwnerTokenInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("setting max supply for owner token not implemented")
}
func (t OwnerTokenInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) {
ownerTokenABI, err := abi.JSON(strings.NewReader(ownertoken.OwnerTokenABI))
if err != nil {
return []byte{}, err
}
return ownerTokenABI.Pack(methodName, args...)
}
// Master Token
type MasterTokenInstance struct {
TokenInstance
instance *mastertoken.MasterToken
api *API
}
func (t MasterTokenInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return t.instance.RemoteBurn(transactOpts, tokenIds)
}
func (t MasterTokenInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) {
usersAddresses := t.api.PrepareMintCollectiblesData(walletAddresses, amount)
return t.instance.MintTo(transactOpts, usersAddresses)
}
func (t MasterTokenInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) {
return t.instance.SetMaxSupply(transactOpts, maxSupply)
}
func (t MasterTokenInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) {
masterTokenABI, err := abi.JSON(strings.NewReader(mastertoken.MasterTokenABI))
if err != nil {
return []byte{}, err
}
return masterTokenABI.Pack(methodName, args...)
}
// Collectible
type CollectibleInstance struct {
TokenInstance
instance *collectibles.Collectibles
api *API
}
func (t CollectibleInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return t.instance.RemoteBurn(transactOpts, tokenIds)
}
func (t CollectibleInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) {
usersAddresses := t.api.PrepareMintCollectiblesData(walletAddresses, amount)
return t.instance.MintTo(transactOpts, usersAddresses)
}
func (t CollectibleInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) {
return t.instance.SetMaxSupply(transactOpts, maxSupply)
}
func (t CollectibleInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) {
collectiblesABI, err := abi.JSON(strings.NewReader(collectibles.CollectiblesABI))
if err != nil {
return []byte{}, err
}
return collectiblesABI.Pack(methodName, args...)
}
// Asset
type AssetInstance struct {
TokenInstance
instance *assets.Assets
api *API
}
func (t AssetInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("remote destruction for assets not implemented")
}
// The amount should be in smallest denomination of the asset (like wei) with decimal = 18, eg.
// if we want to mint 2.34 of the token, then amount should be 234{16 zeros}.
func (t AssetInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) {
usersAddresses, amountsList := t.api.PrepareMintAssetsData(walletAddresses, amount)
return t.instance.MintTo(transactOpts, usersAddresses, amountsList)
}
func (t AssetInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) {
return t.instance.SetMaxSupply(transactOpts, maxSupply)
}
func (t AssetInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) {
assetsABI, err := abi.JSON(strings.NewReader(assets.AssetsABI))
if err != nil {
return []byte{}, err
}
return assetsABI.Pack(methodName, args...)
}
// creator
func NewTokenInstance(api *API, chainID uint64, contractAddress string) (TokenInstance, error) {
tokenType, err := api.s.db.GetTokenType(chainID, contractAddress)
if err != nil {
return nil, err
}
privLevel, err := api.s.db.GetTokenPrivilegesLevel(chainID, contractAddress)
if err != nil {
return nil, err
}
switch {
case privLevel == token.OwnerLevel:
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return &OwnerTokenInstance{instance: contractInst}, nil
case privLevel == token.MasterLevel:
contractInst, err := api.NewMasterTokenInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return &MasterTokenInstance{instance: contractInst}, nil
case tokenType == protobuf.CommunityTokenType_ERC721:
contractInst, err := api.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return &CollectibleInstance{instance: contractInst}, nil
case tokenType == protobuf.CommunityTokenType_ERC20:
contractInst, err := api.NewAssetsInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return &AssetInstance{instance: contractInst}, nil
}
return nil, fmt.Errorf("unknown type of contract: chain=%v, address=%v", chainID, contractAddress)
}

View File

@@ -0,0 +1,770 @@
package ens
import (
"context"
"database/sql"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"net/url"
"strings"
"sync"
"time"
"github.com/ipfs/go-cid"
"github.com/multiformats/go-multibase"
"github.com/multiformats/go-multihash"
"github.com/pkg/errors"
"github.com/wealdtech/go-ens/v3"
"github.com/wealdtech/go-multicodec"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/registrar"
"github.com/status-im/status-go/contracts/resolver"
"github.com/status-im/status-go/contracts/snt"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/utils"
wcommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/transactions"
)
const StatusDomain = "stateofus.eth"
func NewAPI(rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, config *params.NodeConfig, appDb *sql.DB, timeSource func() time.Time, syncUserDetailFunc *syncUsernameDetail) *API {
return &API{
contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient,
},
accountsManager: accountsManager,
pendingTracker: pendingTracker,
config: config,
addrPerChain: make(map[uint64]common.Address),
db: NewEnsDatabase(appDb),
quit: make(chan struct{}),
timeSource: timeSource,
syncUserDetailFunc: syncUserDetailFunc,
}
}
type URI struct {
Scheme string
Host string
Path string
}
// use this to avoid using messenger directly to avoid circular dependency (protocol->ens->protocol)
type syncUsernameDetail func(context.Context, *UsernameDetail) error
type API struct {
contractMaker *contracts.ContractMaker
accountsManager *account.GethManager
pendingTracker *transactions.PendingTxTracker
config *params.NodeConfig
addrPerChain map[uint64]common.Address
addrPerChainMutex sync.Mutex
quitOnce sync.Once
quit chan struct{}
db *Database
syncUserDetailFunc *syncUsernameDetail
timeSource func() time.Time
}
func (api *API) Stop() {
api.quitOnce.Do(func() {
close(api.quit)
})
}
func (api *API) unixTime() uint64 {
return uint64(api.timeSource().Unix())
}
func (api *API) GetEnsUsernames(ctx context.Context) ([]*UsernameDetail, error) {
removed := false
return api.db.GetEnsUsernames(&removed)
}
func (api *API) Add(ctx context.Context, chainID uint64, username string) error {
ud := &UsernameDetail{Username: username, ChainID: chainID, Clock: api.unixTime()}
err := api.db.AddEnsUsername(ud)
if err != nil {
return err
}
return (*api.syncUserDetailFunc)(ctx, ud)
}
func (api *API) Remove(ctx context.Context, chainID uint64, username string) error {
ud := &UsernameDetail{Username: username, ChainID: chainID, Clock: api.unixTime()}
affected, err := api.db.RemoveEnsUsername(ud)
if err != nil {
return err
}
if affected {
return (*api.syncUserDetailFunc)(ctx, ud)
}
return nil
}
func (api *API) GetRegistrarAddress(ctx context.Context, chainID uint64) (common.Address, error) {
return api.usernameRegistrarAddr(ctx, chainID)
}
func (api *API) Resolver(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
registry, err := api.contractMaker.NewRegistry(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
resolver, err := registry.Resolver(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &resolver, nil
}
func (api *API) GetName(ctx context.Context, chainID uint64, address common.Address) (string, error) {
backend, err := api.contractMaker.RPCClient.EthClient(chainID)
if err != nil {
return "", err
}
return ens.ReverseResolve(backend, address)
}
func (api *API) OwnerOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
registry, err := api.contractMaker.NewRegistry(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
owner, err := registry.Owner(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &owner, nil
}
func (api *API) ContentHash(ctx context.Context, chainID uint64, username string) ([]byte, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return nil, err
}
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contentHash, err := resolver.Contenthash(callOpts, nameHash(username))
if err != nil {
return nil, nil
}
return contentHash, nil
}
func (api *API) PublicKeyOf(ctx context.Context, chainID uint64, username string) (string, error) {
err := validateENSUsername(username)
if err != nil {
return "", err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return "", err
}
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
if err != nil {
return "", err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
pubKey, err := resolver.Pubkey(callOpts, nameHash(username))
if err != nil {
return "", err
}
return "0x04" + hex.EncodeToString(pubKey.X[:]) + hex.EncodeToString(pubKey.Y[:]), nil
}
func (api *API) AddressOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return nil, err
}
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
addr, err := resolver.Addr(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &addr, nil
}
func (api *API) usernameRegistrarAddr(ctx context.Context, chainID uint64) (common.Address, error) {
log.Info("obtaining username registrar address")
api.addrPerChainMutex.Lock()
defer api.addrPerChainMutex.Unlock()
addr, ok := api.addrPerChain[chainID]
if ok {
return addr, nil
}
registryAddr, err := api.OwnerOf(ctx, chainID, StatusDomain)
if err != nil {
return common.Address{}, err
}
api.addrPerChain[chainID] = *registryAddr
go func() {
registry, err := api.contractMaker.NewRegistry(chainID)
if err != nil {
return
}
logs := make(chan *resolver.ENSRegistryWithFallbackNewOwner)
sub, err := registry.WatchNewOwner(&bind.WatchOpts{}, logs, nil, nil)
if err != nil {
return
}
for {
select {
case <-api.quit:
log.Info("quitting ens contract subscription")
sub.Unsubscribe()
return
case err := <-sub.Err():
if err != nil {
log.Error("ens contract subscription error: " + err.Error())
}
return
case vLog := <-logs:
api.addrPerChainMutex.Lock()
api.addrPerChain[chainID] = vLog.Owner
api.addrPerChainMutex.Unlock()
}
}
}()
return *registryAddr, nil
}
func (api *API) ExpireAt(ctx context.Context, chainID uint64, username string) (string, error) {
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return "", err
}
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID, registryAddr)
if err != nil {
return "", err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
expTime, err := registrar.GetExpirationTime(callOpts, usernameToLabel(username))
if err != nil {
return "", err
}
return fmt.Sprintf("%x", expTime), nil
}
func (api *API) Price(ctx context.Context, chainID uint64) (string, error) {
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return "", err
}
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID, registryAddr)
if err != nil {
return "", err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
price, err := registrar.GetPrice(callOpts)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", price), nil
}
func (api *API) Release(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string) (string, error) {
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return "", err
}
registrar, err := api.contractMaker.NewUsernameRegistrar(chainID, registryAddr)
if err != nil {
return "", err
}
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := registrar.Release(txOpts, usernameToLabel(username))
if err != nil {
return "", err
}
err = api.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.ReleaseENS,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
err = api.Remove(ctx, chainID, fullDomainName(username))
if err != nil {
log.Warn("Releasing ENS username: transaction successful, but removing failed")
}
return tx.Hash().String(), nil
}
func (api *API) ReleasePrepareTxCallMsg(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string) (ethereum.CallMsg, error) {
registrarABI, err := abi.JSON(strings.NewReader(registrar.UsernameRegistrarABI))
if err != nil {
return ethereum.CallMsg{}, err
}
data, err := registrarABI.Pack("release", usernameToLabel(username))
if err != nil {
return ethereum.CallMsg{}, err
}
sntAddress, err := snt.ContractAddress(chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
return ethereum.CallMsg{
From: common.Address(txArgs.From),
To: &sntAddress,
Value: big.NewInt(0),
Data: data,
}, nil
}
func (api *API) ReleasePrepareTx(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string) (interface{}, error) {
callMsg, err := api.ReleasePrepareTxCallMsg(ctx, chainID, txArgs, username)
if err != nil {
return nil, err
}
return toCallArg(callMsg), nil
}
func (api *API) ReleaseEstimate(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string) (uint64, error) {
registrarABI, err := abi.JSON(strings.NewReader(registrar.UsernameRegistrarABI))
if err != nil {
return 0, err
}
data, err := registrarABI.Pack("release", usernameToLabel(username))
if err != nil {
return 0, err
}
ethClient, err := api.contractMaker.RPCClient.EthClient(chainID)
if err != nil {
return 0, err
}
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return 0, err
}
estimate, err := ethClient.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(txArgs.From),
To: &registryAddr,
Value: big.NewInt(0),
Data: data,
})
if err != nil {
return 0, err
}
return estimate + 1000, nil
}
func (api *API) Register(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string, pubkey string) (string, error) {
snt, err := api.contractMaker.NewSNT(chainID)
if err != nil {
return "", err
}
priceHex, err := api.Price(ctx, chainID)
if err != nil {
return "", err
}
price := new(big.Int)
price.SetString(priceHex, 16)
registrarABI, err := abi.JSON(strings.NewReader(registrar.UsernameRegistrarABI))
if err != nil {
return "", err
}
x, y := extractCoordinates(pubkey)
extraData, err := registrarABI.Pack("register", usernameToLabel(username), common.Address(txArgs.From), x, y)
if err != nil {
return "", err
}
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return "", err
}
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := snt.ApproveAndCall(
txOpts,
registryAddr,
price,
extraData,
)
if err != nil {
return "", err
}
err = api.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.RegisterENS,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
err = api.Add(ctx, chainID, fullDomainName(username))
if err != nil {
log.Warn("Registering ENS username: transaction successful, but adding failed")
}
return tx.Hash().String(), nil
}
func (api *API) RegisterPrepareTxCallMsg(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (ethereum.CallMsg, error) {
priceHex, err := api.Price(ctx, chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
price := new(big.Int)
price.SetString(priceHex, 16)
registrarABI, err := abi.JSON(strings.NewReader(registrar.UsernameRegistrarABI))
if err != nil {
return ethereum.CallMsg{}, err
}
x, y := extractCoordinates(pubkey)
extraData, err := registrarABI.Pack("register", usernameToLabel(username), common.Address(txArgs.From), x, y)
if err != nil {
return ethereum.CallMsg{}, err
}
sntABI, err := abi.JSON(strings.NewReader(snt.SNTABI))
if err != nil {
return ethereum.CallMsg{}, err
}
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
data, err := sntABI.Pack("approveAndCall", registryAddr, price, extraData)
if err != nil {
return ethereum.CallMsg{}, err
}
sntAddress, err := snt.ContractAddress(chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
return ethereum.CallMsg{
From: common.Address(txArgs.From),
To: &sntAddress,
Value: big.NewInt(0),
Data: data,
}, nil
}
func (api *API) RegisterPrepareTx(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (interface{}, error) {
callMsg, err := api.RegisterPrepareTxCallMsg(ctx, chainID, txArgs, username, pubkey)
if err != nil {
return nil, err
}
return toCallArg(callMsg), nil
}
func (api *API) RegisterEstimate(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (uint64, error) {
ethClient, err := api.contractMaker.RPCClient.EthClient(chainID)
if err != nil {
return 0, err
}
callMsg, err := api.RegisterPrepareTxCallMsg(ctx, chainID, txArgs, username, pubkey)
if err != nil {
return 0, err
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + 1000, nil
}
func (api *API) SetPubKey(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string, pubkey string) (string, error) {
err := validateENSUsername(username)
if err != nil {
return "", err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return "", err
}
resolver, err := api.contractMaker.NewPublicResolver(chainID, resolverAddress)
if err != nil {
return "", err
}
x, y := extractCoordinates(pubkey)
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := resolver.SetPubkey(txOpts, nameHash(username), x, y)
if err != nil {
return "", err
}
err = api.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.SetPubKey,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
err = api.Add(ctx, chainID, fullDomainName(username))
if err != nil {
log.Warn("Registering ENS username: transaction successful, but adding failed")
}
return tx.Hash().String(), nil
}
func (api *API) SetPubKeyPrepareTxCallMsg(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (ethereum.CallMsg, error) {
err := validateENSUsername(username)
if err != nil {
return ethereum.CallMsg{}, err
}
x, y := extractCoordinates(pubkey)
resolverABI, err := abi.JSON(strings.NewReader(resolver.PublicResolverABI))
if err != nil {
return ethereum.CallMsg{}, err
}
data, err := resolverABI.Pack("setPubkey", nameHash(username), x, y)
if err != nil {
return ethereum.CallMsg{}, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return ethereum.CallMsg{}, err
}
return ethereum.CallMsg{
From: common.Address(txArgs.From),
To: resolverAddress,
Value: big.NewInt(0),
Data: data,
}, nil
}
func (api *API) SetPubKeyPrepareTx(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (interface{}, error) {
callMsg, err := api.SetPubKeyPrepareTxCallMsg(ctx, chainID, txArgs, username, pubkey)
if err != nil {
return nil, err
}
return toCallArg(callMsg), nil
}
func (api *API) SetPubKeyEstimate(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, username string, pubkey string) (uint64, error) {
ethClient, err := api.contractMaker.RPCClient.EthClient(chainID)
if err != nil {
return 0, err
}
callMsg, err := api.SetPubKeyPrepareTxCallMsg(ctx, chainID, txArgs, username, pubkey)
if err != nil {
return 0, err
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + 1000, nil
}
func (api *API) ResourceURL(ctx context.Context, chainID uint64, username string) (*URI, error) {
scheme := "https"
contentHash, err := api.ContentHash(ctx, chainID, username)
if err != nil {
return nil, err
}
if len(contentHash) == 0 {
return &URI{}, nil
}
data, codec, err := multicodec.RemoveCodec(contentHash)
if err != nil {
return nil, err
}
codecName, err := multicodec.Name(codec)
if err != nil {
return nil, err
}
switch codecName {
case "ipfs-ns":
thisCID, err := cid.Parse(data)
if err != nil {
return nil, errors.Wrap(err, "failed to parse CID")
}
str, err := thisCID.StringOfBase(multibase.Base32)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain base36 representation")
}
parsedURL, _ := url.Parse(params.IpfsGatewayURL)
// Remove scheme from the url
host := parsedURL.Hostname() + parsedURL.Path + str
return &URI{scheme, host, ""}, nil
case "ipns-ns":
id, offset := binary.Uvarint(data)
if id == 0 {
return nil, fmt.Errorf("unknown CID")
}
data, _, err := multicodec.RemoveCodec(data[offset:])
if err != nil {
return nil, err
}
decodedMHash, err := multihash.Decode(data)
if err != nil {
return nil, err
}
return &URI{scheme, string(decodedMHash.Digest), ""}, nil
case "swarm-ns":
id, offset := binary.Uvarint(data)
if id == 0 {
return nil, fmt.Errorf("unknown CID")
}
data, _, err := multicodec.RemoveCodec(data[offset:])
if err != nil {
return nil, err
}
decodedMHash, err := multihash.Decode(data)
if err != nil {
return nil, err
}
path := "/bzz:/" + hex.EncodeToString(decodedMHash.Digest) + "/"
return &URI{scheme, "swarm-gateways.net", path}, nil
default:
return nil, fmt.Errorf("unknown codec name %s", codecName)
}
}
func toCallArg(msg ethereum.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
"to": msg.To,
}
if len(msg.Data) > 0 {
arg["data"] = hexutil.Bytes(msg.Data)
}
if msg.Value != nil {
arg["value"] = (*hexutil.Big)(msg.Value)
}
if msg.Gas != 0 {
arg["gas"] = hexutil.Uint64(msg.Gas)
}
if msg.GasPrice != nil {
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
}
return arg
}
func fullDomainName(username string) string {
return username + "." + StatusDomain
}

View File

@@ -0,0 +1,81 @@
package ens
import (
"database/sql"
)
type Database struct {
db *sql.DB
}
type UsernameDetail struct {
Username string `json:"username"`
ChainID uint64 `json:"chainId"`
Clock uint64 `json:"clock"`
Removed bool `json:"removed"`
}
func NewEnsDatabase(db *sql.DB) *Database {
return &Database{db: db}
}
func (db *Database) GetEnsUsernames(removed *bool) (result []*UsernameDetail, err error) {
var sqlQuery = `SELECT username, chain_id, clock, removed
FROM ens_usernames`
var rows *sql.Rows
if removed == nil {
rows, err = db.db.Query(sqlQuery)
} else {
sqlQuery += " WHERE removed = ?"
rows, err = db.db.Query(sqlQuery, removed)
}
if err != nil {
return result, err
}
defer rows.Close()
for rows.Next() {
var ensUsername UsernameDetail
err = rows.Scan(&ensUsername.Username, &ensUsername.ChainID, &ensUsername.Clock, &ensUsername.Removed)
if err != nil {
return nil, err
}
result = append(result, &ensUsername)
}
return result, nil
}
func (db *Database) AddEnsUsername(details *UsernameDetail) error {
const sqlQuery = `INSERT OR REPLACE INTO ens_usernames(username, chain_id, clock, removed)
VALUES (?, ?, ?, ?)`
_, err := db.db.Exec(sqlQuery, details.Username, details.ChainID, details.Clock, details.Removed)
return err
}
func (db *Database) RemoveEnsUsername(details *UsernameDetail) (bool, error) {
const sqlQuery = `UPDATE ens_usernames SET removed = 1, clock = ?
WHERE username = (?) AND chain_id = ?`
result, err := db.db.Exec(sqlQuery, details.Clock, details.Username, details.ChainID)
if err != nil {
return false, err
}
n, err := result.RowsAffected()
if err != nil {
return false, err
}
return n > 0, nil
}
func (db *Database) SaveOrUpdateEnsUsername(details *UsernameDetail) error {
const sqlQuery = `INSERT OR REPLACE INTO ens_usernames (username, chain_id, clock, removed)
SELECT ?, ?, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM ens_usernames WHERE username = ? AND chain_id = ? AND clock >= ?);`
_, err := db.db.Exec(sqlQuery, details.Username, details.ChainID, details.Clock, details.Removed, details.Username, details.ChainID, details.Clock)
return err
}

View File

@@ -0,0 +1,72 @@
package ens
import (
"database/sql"
"time"
"github.com/ethereum/go-ethereum/p2p"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/transactions"
)
// NewService initializes service instance.
func NewService(rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, config *params.NodeConfig, appDb *sql.DB, timeSource func() time.Time) *Service {
service := &Service{
rpcClient,
accountsManager,
pendingTracker,
config,
nil,
nil,
}
service.api = NewAPI(rpcClient, accountsManager, pendingTracker, config, appDb, timeSource, &service.syncUserDetailFunc)
return service
}
// Service is a browsers service.
type Service struct {
rpcClient *rpc.Client
accountsManager *account.GethManager
pendingTracker *transactions.PendingTxTracker
config *params.NodeConfig
api *API
syncUserDetailFunc syncUsernameDetail
}
func (s *Service) Init(syncUserDetailFunc syncUsernameDetail) {
s.syncUserDetailFunc = syncUserDetailFunc
}
// Start a service.
func (s *Service) Start() error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
s.api.Stop()
return nil
}
func (s *Service) API() *API {
return s.api
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []ethRpc.API {
return []ethRpc.API{
{
Namespace: "ens",
Version: "0.1.0",
Service: s.api,
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,54 @@
package ens
import (
"encoding/hex"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func nameHash(name string) common.Hash {
node := common.Hash{}
if len(name) > 0 {
labels := strings.Split(name, ".")
for i := len(labels) - 1; i >= 0; i-- {
labelSha := crypto.Keccak256Hash([]byte(labels[i]))
node = crypto.Keccak256Hash(node.Bytes(), labelSha.Bytes())
}
}
return node
}
func validateENSUsername(username string) error {
if !strings.HasSuffix(username, ".eth") {
return fmt.Errorf("username must end with .eth")
}
return nil
}
func usernameToLabel(username string) [32]byte {
usernameHashed := crypto.Keccak256([]byte(username))
var label [32]byte
copy(label[:], usernameHashed)
return label
}
func extractCoordinates(pubkey string) ([32]byte, [32]byte) {
x, _ := hex.DecodeString(pubkey[4:68])
y, _ := hex.DecodeString(pubkey[68:132])
var xByte [32]byte
copy(xByte[:], x)
var yByte [32]byte
copy(yByte[:], y)
return xByte, yByte
}

View File

@@ -0,0 +1,82 @@
Whisper API Extension
=====================
API
---
#### shhext_getNewFilterMessages
Accepts the same input as [`shh_getFilterMessages`](https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_getFilterChanges).
##### Returns
Returns a list of whisper messages matching the specified filter. Filters out
the messages already confirmed received by [`shhext_confirmMessagesProcessed`](#shhextconfirmmessagesprocessed)
Deduplication is made using the whisper envelope content and topic only, so the
same content received in different whisper envelopes will be deduplicated.
#### shhext_confirmMessagesProcessed
Confirms whisper messages received and processed on the client side. These
messages won't appear anymore when [`shhext_getNewFilterMessages`](#shhextgetnewfiltermessages)
is called.
##### Parameters
Gets a list of whisper envelopes.
#### shhext_post
Accepts same input as [`shh_post`](https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_post).
##### Returns
`DATA`, 32 Bytes - the envelope hash
#### shhext_requestMessages
Sends a request for historic messages to a mail server.
##### Parameters
1. `Object` - The message request object:
- `mailServerPeer`:`URL` - Mail servers' enode addess
- `from`:`QUANTITY` - (optional) Lower bound of time range as unix timestamp, default is 24 hours back from now
- `to`:`QUANTITY`- (optional) Upper bound of time range as unix timestamp, default is now
- `topic`:`DATA`, 4 Bytes - Regular whisper topic
- `symKeyID`:`DATA`- ID of a symmetric key to authenticate to mail server, derived from mail server password
##### Returns
`Boolean` - returns `true` if the request was send, otherwise `false`.
Signals
-------
Sends sent signal once per envelope.
```json
{
"type": "envelope.sent",
"event": {
"hash": "0xea0b93079ed32588628f1cabbbb5ed9e4d50b7571064c2962c3853972db67790"
}
}
```
Sends expired signal if envelope dropped from whisper local queue before it was
sent to any peer on the network.
```json
{
"type": "envelope.expired",
"event": {
"hash": "0x754f4c12dccb14886f791abfeb77ffb86330d03d5a4ba6f37a8c21281988b69e"
}
}
```

1808
vendor/github.com/status-im/status-go/services/ext/api.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
package ext
import (
"context"
"time"
"github.com/status-im/status-go/db"
)
// ContextKey is a type used for keys in ext Context.
type ContextKey struct {
Name string
}
// NewContextKey returns new ContextKey instance.
func NewContextKey(name string) ContextKey {
return ContextKey{Name: name}
}
var (
historyDBKey = NewContextKey("history_db")
requestRegistryKey = NewContextKey("request_registry")
timeKey = NewContextKey("time")
)
// NewContext creates Context with all required fields.
func NewContext(ctx context.Context, source TimeSource, registry *RequestsRegistry, storage db.Storage) Context {
ctx = context.WithValue(ctx, historyDBKey, db.NewHistoryStore(storage))
ctx = context.WithValue(ctx, timeKey, source)
ctx = context.WithValue(ctx, requestRegistryKey, registry)
return Context{ctx}
}
// TimeSource is a type used for current time.
type TimeSource func() time.Time
// Context provides access to request-scoped values.
type Context struct {
context.Context
}
// HistoryStore returns db.HistoryStore instance associated with this request.
func (c Context) HistoryStore() db.HistoryStore {
return c.Value(historyDBKey).(db.HistoryStore)
}
// Time returns current time using time function associated with this request.
func (c Context) Time() time.Time {
return c.Value(timeKey).(TimeSource)()
}
// RequestRegistry returns RequestRegistry that tracks each request life-span.
func (c Context) RequestRegistry() *RequestsRegistry {
return c.Value(requestRegistryKey).(*RequestsRegistry)
}

View File

@@ -0,0 +1,48 @@
package ext
import (
"github.com/status-im/status-go/eth-node/types"
)
type failureMessage struct {
IDs [][]byte
Error error
}
func NewHandlerMock(buf int) HandlerMock {
return HandlerMock{
confirmations: make(chan [][]byte, buf),
expirations: make(chan failureMessage, buf),
requestsCompleted: make(chan types.Hash, buf),
requestsExpired: make(chan types.Hash, buf),
requestsFailed: make(chan types.Hash, buf),
}
}
type HandlerMock struct {
confirmations chan [][]byte
expirations chan failureMessage
requestsCompleted chan types.Hash
requestsExpired chan types.Hash
requestsFailed chan types.Hash
}
func (t HandlerMock) EnvelopeSent(ids [][]byte) {
t.confirmations <- ids
}
func (t HandlerMock) EnvelopeExpired(ids [][]byte, err error) {
t.expirations <- failureMessage{IDs: ids, Error: err}
}
func (t HandlerMock) MailServerRequestCompleted(requestID types.Hash, lastEnvelopeHash types.Hash, cursor []byte, err error) {
if err == nil {
t.requestsCompleted <- requestID
} else {
t.requestsFailed <- requestID
}
}
func (t HandlerMock) MailServerRequestExpired(hash types.Hash) {
t.requestsExpired <- hash
}

View File

@@ -0,0 +1,136 @@
package ext
import (
"sync"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/ext/mailservers"
)
// EnvelopeState in local tracker
type EnvelopeState int
const (
// NotRegistered returned if asked hash wasn't registered in the tracker.
NotRegistered EnvelopeState = -1
// MailServerRequestSent is set when p2p request is sent to the mailserver
MailServerRequestSent
)
// MailRequestMonitor is responsible for monitoring history request to mailservers.
type MailRequestMonitor struct {
eventSub mailservers.EnvelopeEventSubscriber
handler EnvelopeEventsHandler
mu sync.Mutex
cache map[types.Hash]EnvelopeState
requestsRegistry *RequestsRegistry
wg sync.WaitGroup
quit chan struct{}
}
func NewMailRequestMonitor(eventSub mailservers.EnvelopeEventSubscriber, h EnvelopeEventsHandler, reg *RequestsRegistry) *MailRequestMonitor {
return &MailRequestMonitor{
eventSub: eventSub,
handler: h,
cache: make(map[types.Hash]EnvelopeState),
requestsRegistry: reg,
}
}
// Start processing events.
func (m *MailRequestMonitor) Start() {
m.quit = make(chan struct{})
m.wg.Add(1)
go func() {
m.handleEnvelopeEvents()
m.wg.Done()
}()
}
// Stop process events.
func (m *MailRequestMonitor) Stop() {
close(m.quit)
m.wg.Wait()
}
func (m *MailRequestMonitor) GetState(hash types.Hash) EnvelopeState {
m.mu.Lock()
defer m.mu.Unlock()
state, exist := m.cache[hash]
if !exist {
return NotRegistered
}
return state
}
// handleEnvelopeEvents processes whisper envelope events
func (m *MailRequestMonitor) handleEnvelopeEvents() {
events := make(chan types.EnvelopeEvent, 100) // must be buffered to prevent blocking whisper
sub := m.eventSub.SubscribeEnvelopeEvents(events)
defer sub.Unsubscribe()
for {
select {
case <-m.quit:
return
case event := <-events:
m.handleEvent(event)
}
}
}
// handleEvent based on type of the event either triggers
// confirmation handler or removes hash from MailRequestMonitor
func (m *MailRequestMonitor) handleEvent(event types.EnvelopeEvent) {
handlers := map[types.EventType]func(types.EnvelopeEvent){
types.EventMailServerRequestSent: m.handleRequestSent,
types.EventMailServerRequestCompleted: m.handleEventMailServerRequestCompleted,
types.EventMailServerRequestExpired: m.handleEventMailServerRequestExpired,
}
if handler, ok := handlers[event.Event]; ok {
handler(event)
}
}
func (m *MailRequestMonitor) handleRequestSent(event types.EnvelopeEvent) {
m.mu.Lock()
defer m.mu.Unlock()
m.cache[event.Hash] = MailServerRequestSent
}
func (m *MailRequestMonitor) handleEventMailServerRequestCompleted(event types.EnvelopeEvent) {
m.mu.Lock()
defer m.mu.Unlock()
m.requestsRegistry.Unregister(event.Hash)
state, ok := m.cache[event.Hash]
if !ok || state != MailServerRequestSent {
return
}
log.Debug("mailserver response received", "hash", event.Hash)
delete(m.cache, event.Hash)
if m.handler != nil {
if resp, ok := event.Data.(*types.MailServerResponse); ok {
m.handler.MailServerRequestCompleted(event.Hash, resp.LastEnvelopeHash, resp.Cursor, resp.Error)
}
}
}
func (m *MailRequestMonitor) handleEventMailServerRequestExpired(event types.EnvelopeEvent) {
m.mu.Lock()
defer m.mu.Unlock()
m.requestsRegistry.Unregister(event.Hash)
state, ok := m.cache[event.Hash]
if !ok || state != MailServerRequestSent {
return
}
log.Debug("mailserver response expired", "hash", event.Hash)
delete(m.cache, event.Hash)
if m.handler != nil {
m.handler.MailServerRequestExpired(event.Hash)
}
}

View File

@@ -0,0 +1,145 @@
package mailservers
import (
"encoding/json"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/util"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/db"
"github.com/status-im/status-go/eth-node/types"
)
// NewPeerRecord returns instance of the peer record.
func NewPeerRecord(node *enode.Node) PeerRecord {
return PeerRecord{node: node}
}
// PeerRecord is set data associated with each peer that is stored on disk.
// PeerRecord stored with a enode as a key in leveldb, and body marshalled as json.
type PeerRecord struct {
node *enode.Node
// last time it was used.
LastUsed time.Time
}
// Encode encodes PeerRecords to bytes.
func (r PeerRecord) Encode() ([]byte, error) {
return json.Marshal(r)
}
// ID returns enode identity of the node.
func (r PeerRecord) ID() enode.ID {
return r.node.ID()
}
// Node returs pointer to original object.
// enode.Node doensn't allow modification on the object.
func (r PeerRecord) Node() *enode.Node {
return r.node
}
// EncodeKey returns bytes that will should be used as a key in persistent storage.
func (r PeerRecord) EncodeKey() ([]byte, error) {
return r.Node().MarshalText()
}
// NewCache returns pointer to a Cache instance.
func NewCache(db *leveldb.DB) *Cache {
return &Cache{db: db}
}
// Cache is wrapper for operations on disk with leveldb.
type Cache struct {
db *leveldb.DB
}
// Replace deletes old and adds new records in the persistent cache.
func (c *Cache) Replace(nodes []*enode.Node) error {
batch := new(leveldb.Batch)
iter := createPeersIterator(c.db)
defer iter.Release()
newNodes := nodesToMap(nodes)
for iter.Next() {
record, err := unmarshalKeyValue(keyWithoutPrefix(iter.Key()), iter.Value())
if err != nil {
return err
}
if _, exist := newNodes[types.EnodeID(record.ID())]; exist {
delete(newNodes, types.EnodeID(record.ID()))
} else {
batch.Delete(iter.Key())
}
}
for _, n := range newNodes {
enodeKey, err := n.MarshalText()
if err != nil {
return err
}
// we put nil as default value doesn't have any state associated with them.
batch.Put(db.Key(db.MailserversCache, enodeKey), nil)
}
return c.db.Write(batch, nil)
}
// LoadAll loads all records from persistent database.
func (c *Cache) LoadAll() (rst []PeerRecord, err error) {
iter := createPeersIterator(c.db)
for iter.Next() {
record, err := unmarshalKeyValue(keyWithoutPrefix(iter.Key()), iter.Value())
if err != nil {
return nil, err
}
rst = append(rst, record)
}
return rst, nil
}
// UpdateRecord updates single record.
func (c *Cache) UpdateRecord(record PeerRecord) error {
enodeKey, err := record.EncodeKey()
if err != nil {
return err
}
value, err := record.Encode()
if err != nil {
return err
}
return c.db.Put(db.Key(db.MailserversCache, enodeKey), value, nil)
}
func unmarshalKeyValue(key, value []byte) (record PeerRecord, err error) {
enodeKey := key
node := new(enode.Node)
err = node.UnmarshalText(enodeKey)
if err != nil {
return record, err
}
record = PeerRecord{node: node}
if len(value) != 0 {
err = json.Unmarshal(value, &record)
}
return record, err
}
func nodesToMap(nodes []*enode.Node) map[types.EnodeID]*enode.Node {
rst := map[types.EnodeID]*enode.Node{}
for _, n := range nodes {
rst[types.EnodeID(n.ID())] = n
}
return rst
}
func createPeersIterator(level *leveldb.DB) iterator.Iterator {
return level.NewIterator(util.BytesPrefix([]byte{byte(db.MailserversCache)}), nil)
}
// keyWithoutPrefix removes first byte from key.
func keyWithoutPrefix(key []byte) []byte {
return key[1:]
}

View File

@@ -0,0 +1,271 @@
package mailservers
import (
"sync"
"time"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/eth-node/types"
)
const (
peerEventsBuffer = 10 // sufficient buffer to avoid blocking a p2p feed.
whisperEventsBuffer = 20 // sufficient buffer to avod blocking a eventSub envelopes feed.
)
// PeerAdderRemover is an interface for adding or removing peers.
type PeerAdderRemover interface {
AddPeer(node *enode.Node)
RemovePeer(node *enode.Node)
}
// PeerEventsSubscriber interface to subscribe for p2p.PeerEvent's.
type PeerEventsSubscriber interface {
SubscribeEvents(chan *p2p.PeerEvent) event.Subscription
}
// EnvelopeEventSubscriber interface to subscribe for types.EnvelopeEvent's.
type EnvelopeEventSubscriber interface {
SubscribeEnvelopeEvents(chan<- types.EnvelopeEvent) types.Subscription
}
type p2pServer interface {
PeerAdderRemover
PeerEventsSubscriber
}
// NewConnectionManager creates an instance of ConnectionManager.
func NewConnectionManager(server p2pServer, eventSub EnvelopeEventSubscriber, target, maxFailures int, timeout time.Duration) *ConnectionManager {
return &ConnectionManager{
server: server,
eventSub: eventSub,
connectedTarget: target,
maxFailures: maxFailures,
notifications: make(chan []*enode.Node),
timeoutWaitAdded: timeout,
}
}
// ConnectionManager manages keeps target of peers connected.
type ConnectionManager struct {
wg sync.WaitGroup
quit chan struct{}
server p2pServer
eventSub EnvelopeEventSubscriber
notifications chan []*enode.Node
connectedTarget int
timeoutWaitAdded time.Duration
maxFailures int
}
// Notify sends a non-blocking notification about new nodes.
func (ps *ConnectionManager) Notify(nodes []*enode.Node) {
ps.wg.Add(1)
go func() {
select {
case ps.notifications <- nodes:
case <-ps.quit:
}
ps.wg.Done()
}()
}
// Start subscribes to a p2p server and handles new peers and state updates for those peers.
func (ps *ConnectionManager) Start() {
ps.quit = make(chan struct{})
ps.wg.Add(1)
go func() {
state := newInternalState(ps.server, ps.connectedTarget, ps.timeoutWaitAdded)
events := make(chan *p2p.PeerEvent, peerEventsBuffer)
sub := ps.server.SubscribeEvents(events)
whisperEvents := make(chan types.EnvelopeEvent, whisperEventsBuffer)
whisperSub := ps.eventSub.SubscribeEnvelopeEvents(whisperEvents)
requests := map[types.Hash]struct{}{}
failuresPerServer := map[types.EnodeID]int{}
defer sub.Unsubscribe()
defer whisperSub.Unsubscribe()
defer ps.wg.Done()
for {
select {
case <-ps.quit:
return
case err := <-sub.Err():
log.Error("retry after error subscribing to p2p events", "error", err)
return
case err := <-whisperSub.Err():
log.Error("retry after error suscribing to eventSub events", "error", err)
return
case newNodes := <-ps.notifications:
state.processReplacement(newNodes, events)
case ev := <-events:
processPeerEvent(state, ev)
case ev := <-whisperEvents:
// TODO treat failed requests the same way as expired
switch ev.Event {
case types.EventMailServerRequestSent:
requests[ev.Hash] = struct{}{}
case types.EventMailServerRequestCompleted:
// reset failures count on first success
failuresPerServer[ev.Peer] = 0
delete(requests, ev.Hash)
case types.EventMailServerRequestExpired:
_, exist := requests[ev.Hash]
if !exist {
continue
}
failuresPerServer[ev.Peer]++
log.Debug("request to a mail server expired, disconnect a peer", "address", ev.Peer)
if failuresPerServer[ev.Peer] >= ps.maxFailures {
state.nodeDisconnected(ev.Peer)
}
}
}
}
}()
}
// Stop gracefully closes all background goroutines and waits until they finish.
func (ps *ConnectionManager) Stop() {
if ps.quit == nil {
return
}
select {
case <-ps.quit:
return
default:
}
close(ps.quit)
ps.wg.Wait()
ps.quit = nil
}
func (state *internalState) processReplacement(newNodes []*enode.Node, events <-chan *p2p.PeerEvent) {
replacement := map[types.EnodeID]*enode.Node{}
for _, n := range newNodes {
replacement[types.EnodeID(n.ID())] = n
}
state.replaceNodes(replacement)
if state.ReachedTarget() {
log.Debug("already connected with required target", "target", state.target)
return
}
if state.timeout != 0 {
log.Debug("waiting defined timeout to establish connections",
"timeout", state.timeout, "target", state.target)
timer := time.NewTimer(state.timeout)
waitForConnections(state, timer.C, events)
timer.Stop()
}
}
func newInternalState(srv PeerAdderRemover, target int, timeout time.Duration) *internalState {
return &internalState{
options: options{target: target, timeout: timeout},
srv: srv,
connected: map[types.EnodeID]struct{}{},
currentNodes: map[types.EnodeID]*enode.Node{},
}
}
type options struct {
target int
timeout time.Duration
}
type internalState struct {
options
srv PeerAdderRemover
connected map[types.EnodeID]struct{}
currentNodes map[types.EnodeID]*enode.Node
}
func (state *internalState) ReachedTarget() bool {
return len(state.connected) >= state.target
}
func (state *internalState) replaceNodes(new map[types.EnodeID]*enode.Node) {
for nid, n := range state.currentNodes {
if _, exist := new[nid]; !exist {
delete(state.connected, nid)
state.srv.RemovePeer(n)
}
}
if !state.ReachedTarget() {
for _, n := range new {
state.srv.AddPeer(n)
}
}
state.currentNodes = new
}
func (state *internalState) nodeAdded(peer types.EnodeID) {
n, exist := state.currentNodes[peer]
if !exist {
return
}
if state.ReachedTarget() {
state.srv.RemovePeer(n)
} else {
state.connected[types.EnodeID(n.ID())] = struct{}{}
}
}
func (state *internalState) nodeDisconnected(peer types.EnodeID) {
n, exist := state.currentNodes[peer] // unrelated event
if !exist {
return
}
_, exist = state.connected[peer] // check if already disconnected
if !exist {
return
}
if len(state.currentNodes) == 1 { // keep node connected if we don't have another choice
return
}
state.srv.RemovePeer(n) // remove peer permanently, otherwise p2p.Server will try to reconnect
delete(state.connected, peer)
if !state.ReachedTarget() { // try to connect with any other selected (but not connected) node
for nid, n := range state.currentNodes {
_, exist := state.connected[nid]
if exist || peer == nid {
continue
}
state.srv.AddPeer(n)
}
}
}
func processPeerEvent(state *internalState, ev *p2p.PeerEvent) {
switch ev.Type {
case p2p.PeerEventTypeAdd:
log.Debug("connected to a mailserver", "address", ev.Peer)
state.nodeAdded(types.EnodeID(ev.Peer))
case p2p.PeerEventTypeDrop:
log.Debug("mailserver disconnected", "address", ev.Peer)
state.nodeDisconnected(types.EnodeID(ev.Peer))
}
}
func waitForConnections(state *internalState, timeout <-chan time.Time, events <-chan *p2p.PeerEvent) {
for {
select {
case ev := <-events:
processPeerEvent(state, ev)
if state.ReachedTarget() {
return
}
case <-timeout:
return
}
}
}

View File

@@ -0,0 +1,85 @@
package mailservers
import (
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/eth-node/types"
)
// NewLastUsedConnectionMonitor returns pointer to the instance of LastUsedConnectionMonitor.
func NewLastUsedConnectionMonitor(ps *PeerStore, cache *Cache, eventSub EnvelopeEventSubscriber) *LastUsedConnectionMonitor {
return &LastUsedConnectionMonitor{
ps: ps,
cache: cache,
eventSub: eventSub,
}
}
// LastUsedConnectionMonitor watches relevant events and reflects it in cache.
type LastUsedConnectionMonitor struct {
ps *PeerStore
cache *Cache
eventSub EnvelopeEventSubscriber
quit chan struct{}
wg sync.WaitGroup
}
// Start spins a separate goroutine to watch connections.
func (mon *LastUsedConnectionMonitor) Start() {
mon.quit = make(chan struct{})
mon.wg.Add(1)
go func() {
events := make(chan types.EnvelopeEvent, whisperEventsBuffer)
sub := mon.eventSub.SubscribeEnvelopeEvents(events)
defer sub.Unsubscribe()
defer mon.wg.Done()
for {
select {
case <-mon.quit:
return
case err := <-sub.Err():
log.Error("retry after error suscribing to eventSub events", "error", err)
return
case ev := <-events:
node := mon.ps.Get(ev.Peer)
if node == nil {
continue
}
if ev.Event == types.EventMailServerRequestCompleted {
err := mon.updateRecord(ev.Peer)
if err != nil {
log.Error("unable to update storage", "peer", ev.Peer, "error", err)
}
}
}
}
}()
}
func (mon *LastUsedConnectionMonitor) updateRecord(nodeID types.EnodeID) error {
node := mon.ps.Get(nodeID)
if node == nil {
return nil
}
return mon.cache.UpdateRecord(PeerRecord{node: node, LastUsed: time.Now()})
}
// Stop closes channel to signal a quit and waits until all goroutines are stoppped.
func (mon *LastUsedConnectionMonitor) Stop() {
if mon.quit == nil {
return
}
select {
case <-mon.quit:
return
default:
}
close(mon.quit)
mon.wg.Wait()
mon.quit = nil
}

View File

@@ -0,0 +1,63 @@
package mailservers
import (
"errors"
"sync"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/eth-node/types"
)
var (
// ErrNoConnected returned when mail servers are not connected.
ErrNoConnected = errors.New("no connected mail servers")
)
// PeersProvider is an interface for requesting list of peers.
type PeersProvider interface {
Peers() []*p2p.Peer
}
// NewPeerStore returns an instance of PeerStore.
func NewPeerStore(cache *Cache) *PeerStore {
return &PeerStore{
nodes: map[types.EnodeID]*enode.Node{},
cache: cache,
}
}
// PeerStore stores list of selected mail servers and keeps N of them connected.
type PeerStore struct {
mu sync.RWMutex
nodes map[types.EnodeID]*enode.Node
cache *Cache
}
// Exist confirms that peers was added to a store.
func (ps *PeerStore) Exist(nodeID types.EnodeID) bool {
ps.mu.RLock()
defer ps.mu.RUnlock()
_, exist := ps.nodes[nodeID]
return exist
}
// Get returns instance of the node with requested ID or nil if ID is not found.
func (ps *PeerStore) Get(nodeID types.EnodeID) *enode.Node {
ps.mu.RLock()
defer ps.mu.RUnlock()
return ps.nodes[nodeID]
}
// Update updates peers locally.
func (ps *PeerStore) Update(nodes []*enode.Node) error {
ps.mu.Lock()
ps.nodes = map[types.EnodeID]*enode.Node{}
for _, n := range nodes {
ps.nodes[types.EnodeID(n.ID())] = n
}
ps.mu.Unlock()
return ps.cache.Replace(nodes)
}

View File

@@ -0,0 +1,54 @@
package mailservers
import (
"sort"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/eth-node/types"
)
// GetFirstConnected returns first connected peer that is also added to a peer store.
// Raises ErrNoConnected if no peers are added to a peer store.
func GetFirstConnected(provider PeersProvider, store *PeerStore) (*enode.Node, error) {
peers := provider.Peers()
for _, p := range peers {
if store.Exist(types.EnodeID(p.ID())) {
return p.Node(), nil
}
}
return nil, ErrNoConnected
}
// NodesNotifee interface to be notified when new nodes are received.
type NodesNotifee interface {
Notify([]*enode.Node)
}
// EnsureUsedRecordsAddedFirst checks if any nodes were marked as connected before app went offline.
func EnsureUsedRecordsAddedFirst(ps *PeerStore, conn NodesNotifee) error {
records, err := ps.cache.LoadAll()
if err != nil {
return err
}
if len(records) == 0 {
return nil
}
sort.Slice(records, func(i, j int) bool {
return records[i].LastUsed.After(records[j].LastUsed)
})
all := recordsToNodes(records)
if !records[0].LastUsed.IsZero() {
conn.Notify(all[:1])
}
conn.Notify(all)
return nil
}
func recordsToNodes(records []PeerRecord) []*enode.Node {
nodes := make([]*enode.Node, len(records))
for i := range records {
nodes[i] = records[i].Node()
}
return nodes
}

View File

@@ -0,0 +1,45 @@
package ext
import (
"go.uber.org/zap"
"github.com/status-im/status-go/eth-node/types"
enstypes "github.com/status-im/status-go/eth-node/types/ens"
)
type TestNodeWrapper struct {
whisper types.Whisper
waku types.Waku
}
func NewTestNodeWrapper(whisper types.Whisper, waku types.Waku) *TestNodeWrapper {
return &TestNodeWrapper{whisper: whisper, waku: waku}
}
func (w *TestNodeWrapper) NewENSVerifier(_ *zap.Logger) enstypes.ENSVerifier {
panic("not implemented")
}
func (w *TestNodeWrapper) GetWhisper(_ interface{}) (types.Whisper, error) {
return w.whisper, nil
}
func (w *TestNodeWrapper) GetWaku(_ interface{}) (types.Waku, error) {
return w.waku, nil
}
func (w *TestNodeWrapper) GetWakuV2(_ interface{}) (types.Waku, error) {
return w.waku, nil
}
func (w *TestNodeWrapper) PeersCount() int {
return 1
}
func (w *TestNodeWrapper) AddPeer(url string) error {
panic("not implemented")
}
func (w *TestNodeWrapper) RemovePeer(url string) error {
panic("not implemented")
}

View File

@@ -0,0 +1,98 @@
package ext
import (
"fmt"
"hash/fnv"
"sync"
"time"
"github.com/status-im/status-go/eth-node/types"
)
const (
// DefaultRequestsDelay will be used in RequestsRegistry if no other was provided.
DefaultRequestsDelay = 3 * time.Second
)
type requestMeta struct {
timestamp time.Time
lastUID types.Hash
}
// NewRequestsRegistry creates instance of the RequestsRegistry and returns pointer to it.
func NewRequestsRegistry(delay time.Duration) *RequestsRegistry {
r := &RequestsRegistry{
delay: delay,
}
r.Clear()
return r
}
// RequestsRegistry keeps map for all requests with timestamp when they were made.
type RequestsRegistry struct {
mu sync.Mutex
delay time.Duration
uidToTopics map[types.Hash]types.Hash
byTopicsHash map[types.Hash]requestMeta
}
// Register request with given topics. If request with same topics was made in less then configured delay then error
// will be returned.
func (r *RequestsRegistry) Register(uid types.Hash, topics []types.TopicType) error {
r.mu.Lock()
defer r.mu.Unlock()
topicsHash := topicsToHash(topics)
if meta, exist := r.byTopicsHash[topicsHash]; exist {
if time.Since(meta.timestamp) < r.delay {
return fmt.Errorf("another request with the same topics was sent less than %s ago. Please wait for a bit longer, or set `force` to true in request parameters", r.delay)
}
}
newMeta := requestMeta{
timestamp: time.Now(),
lastUID: uid,
}
r.uidToTopics[uid] = topicsHash
r.byTopicsHash[topicsHash] = newMeta
return nil
}
// Has returns true if given uid is stored in registry.
func (r *RequestsRegistry) Has(uid types.Hash) bool {
r.mu.Lock()
defer r.mu.Unlock()
_, exist := r.uidToTopics[uid]
return exist
}
// Unregister removes request with given UID from registry.
func (r *RequestsRegistry) Unregister(uid types.Hash) {
r.mu.Lock()
defer r.mu.Unlock()
topicsHash, exist := r.uidToTopics[uid]
if !exist {
return
}
delete(r.uidToTopics, uid)
meta := r.byTopicsHash[topicsHash]
// remove topicsHash only if we are trying to unregister last request with this topic.
if meta.lastUID == uid {
delete(r.byTopicsHash, topicsHash)
}
}
// Clear recreates all structures used for caching requests.
func (r *RequestsRegistry) Clear() {
r.mu.Lock()
defer r.mu.Unlock()
r.uidToTopics = map[types.Hash]types.Hash{}
r.byTopicsHash = map[types.Hash]requestMeta{}
}
// topicsToHash returns non-cryptographic hash of the topics.
func topicsToHash(topics []types.TopicType) types.Hash {
hash := fnv.New32()
for i := range topics {
_, _ = hash.Write(topics[i][:]) // never returns error per documentation
}
return types.BytesToHash(hash.Sum(nil))
}

View File

@@ -0,0 +1,67 @@
// TODO: These types should be defined using protobuf, but protoc can only emit []byte instead of types.HexBytes,
// which causes issues when marshaling to JSON on the react side. Let's do that once the chat protocol is moved to the go repo.
package ext
import (
"crypto/ecdsa"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
)
// SendPublicMessageRPC represents the RPC payload for the SendPublicMessage RPC method
type SendPublicMessageRPC struct {
Sig string // TODO: remove
Chat string
Payload types.HexBytes
}
// TODO: implement with accordance to https://github.com/status-im/status-go/protocol/issues/28.
func (m SendPublicMessageRPC) ID() string { return m.Chat }
func (m SendPublicMessageRPC) PublicName() string { return m.Chat }
func (m SendPublicMessageRPC) PublicKey() *ecdsa.PublicKey { return nil }
// SendDirectMessageRPC represents the RPC payload for the SendDirectMessage RPC method
type SendDirectMessageRPC struct {
Sig string // TODO: remove
Chat string
Payload types.HexBytes
PubKey types.HexBytes
DH bool // TODO: make sure to remove safely
}
// TODO: implement with accordance to https://github.com/status-im/status-go/protocol/issues/28.
func (m SendDirectMessageRPC) ID() string { return "" }
func (m SendDirectMessageRPC) PublicName() string { return "" }
func (m SendDirectMessageRPC) PublicKey() *ecdsa.PublicKey {
publicKey, _ := crypto.UnmarshalPubkey(m.PubKey)
return publicKey
}
type JoinRPC struct {
Chat string
PubKey types.HexBytes
Payload types.HexBytes
}
func (m JoinRPC) ID() string { return m.Chat }
func (m JoinRPC) PublicName() string {
if len(m.PubKey) > 0 {
return ""
}
return m.Chat
}
func (m JoinRPC) PublicKey() *ecdsa.PublicKey {
if len(m.PubKey) > 0 {
return nil
}
publicKey, _ := crypto.UnmarshalPubkey(m.PubKey)
return publicKey
}

View File

@@ -0,0 +1,828 @@
package ext
import (
"context"
"crypto/ecdsa"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"github.com/syndtr/goleveldb/leveldb"
"go.uber.org/zap"
commongethtypes "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/connection"
"github.com/status-im/status-go/db"
coretypes "github.com/status-im/status-go/eth-node/core/types"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/anonmetrics"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/common/shard"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/pushnotificationclient"
"github.com/status-im/status-go/protocol/pushnotificationserver"
"github.com/status-im/status-go/protocol/transport"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/communitytokens"
"github.com/status-im/status-go/services/ext/mailservers"
mailserversDB "github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/services/wallet"
w_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/wakuv2"
)
const infinityString = "∞"
const providerID = "community"
// EnvelopeEventsHandler used for two different event types.
type EnvelopeEventsHandler interface {
EnvelopeSent([][]byte)
EnvelopeExpired([][]byte, error)
MailServerRequestCompleted(types.Hash, types.Hash, []byte, error)
MailServerRequestExpired(types.Hash)
}
// Service is a service that provides some additional API to whisper-based protocols like Whisper or Waku.
type Service struct {
messenger *protocol.Messenger
identity *ecdsa.PrivateKey
cancelMessenger chan struct{}
storage db.TransactionalStorage
n types.Node
rpcClient *rpc.Client
config params.NodeConfig
mailMonitor *MailRequestMonitor
server *p2p.Server
peerStore *mailservers.PeerStore
accountsDB *accounts.Database
multiAccountsDB *multiaccounts.Database
account *multiaccounts.Account
}
// Make sure that Service implements node.Service interface.
var _ node.Lifecycle = (*Service)(nil)
func New(
config params.NodeConfig,
n types.Node,
rpcClient *rpc.Client,
ldb *leveldb.DB,
mailMonitor *MailRequestMonitor,
eventSub mailservers.EnvelopeEventSubscriber,
) *Service {
cache := mailservers.NewCache(ldb)
peerStore := mailservers.NewPeerStore(cache)
return &Service{
storage: db.NewLevelDBStorage(ldb),
n: n,
rpcClient: rpcClient,
config: config,
mailMonitor: mailMonitor,
peerStore: peerStore,
}
}
func (s *Service) NodeID() *ecdsa.PrivateKey {
if s.server == nil {
return nil
}
return s.server.PrivateKey
}
func (s *Service) GetPeer(rawURL string) (*enode.Node, error) {
if len(rawURL) == 0 {
return mailservers.GetFirstConnected(s.server, s.peerStore)
}
return enode.ParseV4(rawURL)
}
func (s *Service) InitProtocol(nodeName string, identity *ecdsa.PrivateKey, appDb, walletDb *sql.DB, httpServer *server.MediaServer, multiAccountDb *multiaccounts.Database, acc *multiaccounts.Account, accountManager *account.GethManager, rpcClient *rpc.Client, walletService *wallet.Service, communityTokensService *communitytokens.Service, wakuService *wakuv2.Waku, logger *zap.Logger) error {
var err error
if !s.config.ShhextConfig.PFSEnabled {
return nil
}
// If Messenger has been already set up, we need to shut it down
// before we init it again. Otherwise, it will lead to goroutines leakage
// due to not stopped filters.
if s.messenger != nil {
if err := s.messenger.Shutdown(); err != nil {
return err
}
}
s.identity = identity
dataDir := filepath.Clean(s.config.ShhextConfig.BackupDisabledDataDir)
if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
return err
}
envelopesMonitorConfig := &transport.EnvelopesMonitorConfig{
MaxAttempts: s.config.ShhextConfig.MaxMessageDeliveryAttempts,
AwaitOnlyMailServerConfirmations: s.config.ShhextConfig.MailServerConfirmations,
IsMailserver: func(peer types.EnodeID) bool {
return s.peerStore.Exist(peer)
},
EnvelopeEventsHandler: EnvelopeSignalHandler{},
Logger: logger,
}
s.accountsDB, err = accounts.NewDB(appDb)
if err != nil {
return err
}
s.multiAccountsDB = multiAccountDb
s.account = acc
options, err := buildMessengerOptions(s.config, identity, appDb, walletDb, httpServer, s.rpcClient, s.multiAccountsDB, acc, envelopesMonitorConfig, s.accountsDB, walletService, communityTokensService, wakuService, logger, &MessengerSignalsHandler{}, accountManager)
if err != nil {
return err
}
messenger, err := protocol.NewMessenger(
nodeName,
identity,
s.n,
s.config.ShhextConfig.InstallationID,
s.peerStore,
options...,
)
if err != nil {
return err
}
s.messenger = messenger
s.messenger.SetP2PServer(s.server)
if s.config.ProcessBackedupMessages {
s.messenger.EnableBackedupMessagesProcessing()
}
return messenger.Init()
}
func (s *Service) StartMessenger() (*protocol.MessengerResponse, error) {
// Start a loop that retrieves all messages and propagates them to status-mobile.
s.cancelMessenger = make(chan struct{})
response, err := s.messenger.Start()
if err != nil {
return nil, err
}
s.messenger.StartRetrieveMessagesLoop(time.Second, s.cancelMessenger)
go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger)
if s.config.ShhextConfig.BandwidthStatsEnabled {
go s.retrieveStats(5*time.Second, s.cancelMessenger)
}
return response, nil
}
func (s *Service) retrieveStats(tick time.Duration, cancel <-chan struct{}) {
ticker := time.NewTicker(tick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
response := s.messenger.GetStats()
PublisherSignalHandler{}.Stats(response)
case <-cancel:
return
}
}
}
type verifyTransactionClient struct {
chainID *big.Int
url string
}
func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) {
signer := gethtypes.NewLondonSigner(c.chainID)
client, err := ethclient.Dial(c.url)
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes()))
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
message, err := transaction.AsMessage(signer, nil)
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
from := types.BytesToAddress(message.From().Bytes())
to := types.BytesToAddress(message.To().Bytes())
if pending {
return coretypes.NewMessage(
from,
&to,
message.Nonce(),
message.Value(),
message.Gas(),
message.GasPrice(),
message.Data(),
message.CheckNonce(),
), coretypes.TransactionStatusPending, nil
}
receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes()))
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
coremessage := coretypes.NewMessage(
from,
&to,
message.Nonce(),
message.Value(),
message.Gas(),
message.GasPrice(),
message.Data(),
message.CheckNonce(),
)
// Token transfer, check the logs
if len(coremessage.Data()) != 0 {
if w_common.IsTokenTransfer(receipt.Logs) {
return coremessage, coretypes.TransactionStatus(receipt.Status), nil
}
return coremessage, coretypes.TransactionStatusFailed, nil
}
return coremessage, coretypes.TransactionStatus(receipt.Status), nil
}
func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) {
if s.config.ShhextConfig.VerifyTransactionURL == "" {
log.Warn("not starting transaction loop")
return
}
ticker := time.NewTicker(tick)
defer ticker.Stop()
ctx, cancelVerifyTransaction := context.WithCancel(context.Background())
for {
select {
case <-ticker.C:
accounts, err := s.accountsDB.GetActiveAccounts()
if err != nil {
log.Error("failed to retrieve accounts", "err", err)
}
var wallets []types.Address
for _, account := range accounts {
if account.IsWalletNonWatchOnlyAccount() {
wallets = append(wallets, types.BytesToAddress(account.Address.Bytes()))
}
}
response, err := s.messenger.ValidateTransactions(ctx, wallets)
if err != nil {
log.Error("failed to validate transactions", "err", err)
continue
}
s.messenger.PublishMessengerResponse(response)
case <-cancel:
cancelVerifyTransaction()
return
}
}
}
func (s *Service) EnableInstallation(installationID string) error {
return s.messenger.EnableInstallation(installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (s *Service) DisableInstallation(installationID string) error {
return s.messenger.DisableInstallation(installationID)
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []gethrpc.API {
panic("this is abstract service, use shhext or wakuext implementation")
}
func (s *Service) SetP2PServer(server *p2p.Server) {
s.server = server
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
log.Info("Stopping shhext service")
if s.cancelMessenger != nil {
select {
case <-s.cancelMessenger:
// channel already closed
default:
close(s.cancelMessenger)
s.cancelMessenger = nil
}
}
if s.messenger != nil {
if err := s.messenger.Shutdown(); err != nil {
log.Error("failed to stop messenger", "err", err)
return err
}
s.messenger = nil
}
return nil
}
func buildMessengerOptions(
config params.NodeConfig,
identity *ecdsa.PrivateKey,
appDb *sql.DB,
walletDb *sql.DB,
httpServer *server.MediaServer,
rpcClient *rpc.Client,
multiAccounts *multiaccounts.Database,
account *multiaccounts.Account,
envelopesMonitorConfig *transport.EnvelopesMonitorConfig,
accountsDB *accounts.Database,
walletService *wallet.Service,
communityTokensService *communitytokens.Service,
wakuService *wakuv2.Waku,
logger *zap.Logger,
messengerSignalsHandler protocol.MessengerSignalsHandler,
accountManager account.Manager,
) ([]protocol.Option, error) {
options := []protocol.Option{
protocol.WithCustomLogger(logger),
protocol.WithPushNotifications(),
protocol.WithDatabase(appDb),
protocol.WithWalletDatabase(walletDb),
protocol.WithMultiAccounts(multiAccounts),
protocol.WithMailserversDatabase(mailserversDB.NewDB(appDb)),
protocol.WithAccount(account),
protocol.WithBrowserDatabase(browsers.NewDB(appDb)),
protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig),
protocol.WithSignalsHandler(messengerSignalsHandler),
protocol.WithENSVerificationConfig(config.ShhextConfig.VerifyENSURL, config.ShhextConfig.VerifyENSContractAddress),
protocol.WithClusterConfig(config.ClusterConfig),
protocol.WithTorrentConfig(&config.TorrentConfig),
protocol.WithHTTPServer(httpServer),
protocol.WithRPCClient(rpcClient),
protocol.WithMessageCSV(config.OutputMessageCSVEnabled),
protocol.WithWalletConfig(&config.WalletConfig),
protocol.WithWalletService(walletService),
protocol.WithCommunityTokensService(communityTokensService),
protocol.WithWakuService(wakuService),
protocol.WithAccountManager(accountManager),
}
if config.ShhextConfig.DataSyncEnabled {
options = append(options, protocol.WithDatasync())
}
settings, err := accountsDB.GetSettings()
if err != sql.ErrNoRows && err != nil {
return nil, err
}
// Generate anon metrics client config
if settings.AnonMetricsShouldSend {
keyBytes, err := hex.DecodeString(config.ShhextConfig.AnonMetricsSendID)
if err != nil {
return nil, err
}
key, err := crypto.UnmarshalPubkey(keyBytes)
if err != nil {
return nil, err
}
amcc := &anonmetrics.ClientConfig{
ShouldSend: true,
SendAddress: key,
}
options = append(options, protocol.WithAnonMetricsClientConfig(amcc))
}
// Generate anon metrics server config
if config.ShhextConfig.AnonMetricsServerEnabled {
if len(config.ShhextConfig.AnonMetricsServerPostgresURI) == 0 {
return nil, errors.New("AnonMetricsServerPostgresURI must be set")
}
amsc := &anonmetrics.ServerConfig{
Enabled: true,
PostgresURI: config.ShhextConfig.AnonMetricsServerPostgresURI,
}
options = append(options, protocol.WithAnonMetricsServerConfig(amsc))
}
if settings.TelemetryServerURL != "" {
options = append(options, protocol.WithTelemetry(settings.TelemetryServerURL))
}
if settings.PushNotificationsServerEnabled {
config := &pushnotificationserver.Config{
Enabled: true,
Logger: logger,
}
options = append(options, protocol.WithPushNotificationServerConfig(config))
}
var pushNotifServKey []*ecdsa.PublicKey
for _, d := range config.ShhextConfig.DefaultPushNotificationsServers {
pushNotifServKey = append(pushNotifServKey, d.PublicKey)
}
options = append(options, protocol.WithPushNotificationClientConfig(&pushnotificationclient.Config{
DefaultServers: pushNotifServKey,
BlockMentions: settings.PushNotificationsBlockMentions,
SendEnabled: settings.SendPushNotifications,
AllowFromContactsOnly: settings.PushNotificationsFromContactsOnly,
RemoteNotificationsEnabled: settings.RemotePushNotificationsEnabled,
}))
if config.ShhextConfig.VerifyTransactionURL != "" {
client := &verifyTransactionClient{
url: config.ShhextConfig.VerifyTransactionURL,
chainID: big.NewInt(config.ShhextConfig.VerifyTransactionChainID),
}
options = append(options, protocol.WithVerifyTransactionClient(client))
}
return options, nil
}
func (s *Service) ConnectionChanged(state connection.State) {
if s.messenger != nil {
s.messenger.ConnectionChanged(state)
}
}
func (s *Service) Messenger() *protocol.Messenger {
return s.messenger
}
func tokenURIToCommunityID(tokenURI string) string {
tmpStr := strings.Split(tokenURI, "/")
// Community NFTs have a tokenURI of the form "compressedCommunityID/tokenID"
if len(tmpStr) != 2 {
return ""
}
compressedCommunityID := tmpStr[0]
hexCommunityID, err := multiformat.DeserializeCompressedKey(compressedCommunityID)
if err != nil {
return ""
}
pubKey, err := common.HexToPubkey(hexCommunityID)
if err != nil {
return ""
}
communityID := types.EncodeHex(crypto.CompressPubkey(pubKey))
return communityID
}
func (s *Service) GetCommunityID(tokenURI string) string {
if tokenURI != "" {
return tokenURIToCommunityID(tokenURI)
}
return ""
}
func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibleData) error {
if s.messenger == nil {
return fmt.Errorf("messenger not ready")
}
if collectible == nil {
return fmt.Errorf("empty collectible")
}
id := collectible.CollectibleData.ID
communityID := collectible.CollectibleData.CommunityID
if communityID == "" {
return fmt.Errorf("invalid communityID")
}
// FetchCommunityInfo should have been previously called once to ensure
// that the latest version of the CommunityDescription is available in the DB
community, err := s.fetchCommunity(communityID, false)
if err != nil {
return err
}
if community == nil {
return nil
}
tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, id.ContractID)
if err != nil {
return err
}
if tokenMetadata == nil {
return nil
}
communityToken, err := s.fetchCommunityToken(communityID, id.ContractID)
if err != nil {
return err
}
permission := fetchCommunityCollectiblePermission(community, id)
privilegesLevel := token.CommunityLevel
if permission != nil {
privilegesLevel = permissionTypeToPrivilegesLevel(permission.GetType())
}
imagePayload, _ := images.GetPayloadFromURI(tokenMetadata.GetImage())
collectible.CollectibleData.ContractType = w_common.ContractTypeERC721
collectible.CollectibleData.Provider = providerID
collectible.CollectibleData.Name = tokenMetadata.GetName()
collectible.CollectibleData.Description = tokenMetadata.GetDescription()
collectible.CollectibleData.ImagePayload = imagePayload
collectible.CollectibleData.Traits = getCollectibleCommunityTraits(communityToken)
if collectible.CollectionData == nil {
collectible.CollectionData = &thirdparty.CollectionData{
ID: id.ContractID,
CommunityID: communityID,
}
}
collectible.CollectionData.ContractType = w_common.ContractTypeERC721
collectible.CollectionData.Provider = providerID
collectible.CollectionData.Name = tokenMetadata.GetName()
collectible.CollectionData.ImagePayload = imagePayload
collectible.CommunityInfo = communityToInfo(community)
collectible.CollectibleCommunityInfo = &thirdparty.CollectibleCommunityInfo{
PrivilegesLevel: privilegesLevel,
}
return nil
}
func permissionTypeToPrivilegesLevel(permissionType protobuf.CommunityTokenPermission_Type) token.PrivilegesLevel {
switch permissionType {
case protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER:
return token.OwnerLevel
case protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER:
return token.MasterLevel
default:
return token.CommunityLevel
}
}
func communityToInfo(community *communities.Community) *thirdparty.CommunityInfo {
if community == nil {
return nil
}
return &thirdparty.CommunityInfo{
CommunityName: community.Name(),
CommunityColor: community.Color(),
CommunityImagePayload: fetchCommunityImage(community),
}
}
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
community, err := s.fetchCommunity(communityID, true)
if err != nil {
return nil, err
}
return communityToInfo(community), nil
}
func (s *Service) fetchCommunity(communityID string, fetchLatest bool) (*communities.Community, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
// Try to fetch metadata from Messenger communities
// TODO: we need the shard information in the collectible to be able to retrieve info for
// communities that have specific shards
if fetchLatest {
// Try to fetch the latest version of the Community
var shard *shard.Shard = nil // TODO: build this with info from token
// NOTE: The community returned by this function will be nil if
// the version we have in the DB is the latest available.
_, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
CommunityKey: communityID,
Shard: shard,
TryDatabase: false,
WaitForResponse: true,
})
if err != nil {
return nil, err
}
}
// Get the latest successfully fetched version of the Community
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
if err != nil {
return nil, err
}
return community, nil
}
func (s *Service) fetchCommunityToken(communityID string, contractID thirdparty.ContractID) (*token.CommunityToken, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
return s.messenger.GetCommunityToken(communityID, int(contractID.ChainID), contractID.Address.String())
}
func (s *Service) fetchCommunityCollectibleMetadata(community *communities.Community, contractID thirdparty.ContractID) (*protobuf.CommunityTokenMetadata, error) {
tokensMetadata := community.CommunityTokensMetadata()
for _, tokenMetadata := range tokensMetadata {
contractAddresses := tokenMetadata.GetContractAddresses()
if contractAddresses[uint64(contractID.ChainID)] == contractID.Address.Hex() {
return tokenMetadata, nil
}
}
return nil, nil
}
func tokenCriterionContainsCollectible(tokenCriterion *protobuf.TokenCriteria, id thirdparty.CollectibleUniqueID) bool {
// Check if token type matches
if tokenCriterion.Type != protobuf.CommunityTokenType_ERC721 {
return false
}
for chainID, contractAddressStr := range tokenCriterion.ContractAddresses {
if chainID != uint64(id.ContractID.ChainID) {
continue
}
contractAddress := commongethtypes.HexToAddress(contractAddressStr)
if contractAddress != id.ContractID.Address {
continue
}
if len(tokenCriterion.TokenIds) == 0 {
return true
}
for _, tokenID := range tokenCriterion.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
if id.TokenID.Cmp(tokenIDBigInt) == 0 {
return true
}
}
}
return false
}
func permissionContainsCollectible(permission *communities.CommunityTokenPermission, id thirdparty.CollectibleUniqueID) bool {
// See if any token criterion contains the collectible we're looking for
for _, tokenCriterion := range permission.TokenCriteria {
if tokenCriterionContainsCollectible(tokenCriterion, id) {
return true
}
}
return false
}
func fetchCommunityCollectiblePermission(community *communities.Community, id thirdparty.CollectibleUniqueID) *communities.CommunityTokenPermission {
// Permnission types of interest
permissionTypes := []protobuf.CommunityTokenPermission_Type{
protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
}
for _, permissionType := range permissionTypes {
permissions := community.TokenPermissionsByType(permissionType)
// See if any community permission matches the type we're looking for
for _, permission := range permissions {
if permissionContainsCollectible(permission, id) {
return permission
}
}
}
return nil
}
func fetchCommunityImage(community *communities.Community) []byte {
imageTypes := []string{
images.LargeDimName,
images.SmallDimName,
}
communityImages := community.Images()
for _, imageType := range imageTypes {
if pbImage, ok := communityImages[imageType]; ok {
return pbImage.Payload
}
}
return nil
}
func boolToString(value bool) string {
if value {
return "Yes"
}
return "No"
}
func getCollectibleCommunityTraits(token *token.CommunityToken) []thirdparty.CollectibleTrait {
if token == nil {
return make([]thirdparty.CollectibleTrait, 0)
}
totalStr := infinityString
availableStr := infinityString
if !token.InfiniteSupply {
totalStr = token.Supply.String()
// TODO: calculate available supply. See services/communitytokens/api.go
availableStr = totalStr
}
transferableStr := boolToString(token.Transferable)
destructibleStr := boolToString(token.RemoteSelfDestruct)
return []thirdparty.CollectibleTrait{
{
TraitType: "Symbol",
Value: token.Symbol,
},
{
TraitType: "Total",
Value: totalStr,
},
{
TraitType: "Available",
Value: availableStr,
},
{
TraitType: "Transferable",
Value: transferableStr,
},
{
TraitType: "Destructible",
Value: destructibleStr,
},
}
}

View File

@@ -0,0 +1,186 @@
package ext
import (
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/discord"
"github.com/status-im/status-go/protocol/wakusync"
"github.com/status-im/status-go/signal"
)
// EnvelopeSignalHandler sends signals when envelope is sent or expired.
type EnvelopeSignalHandler struct{}
// EnvelopeSent triggered when envelope delivered atleast to 1 peer.
func (h EnvelopeSignalHandler) EnvelopeSent(identifiers [][]byte) {
signal.SendEnvelopeSent(identifiers)
}
// EnvelopeExpired triggered when envelope is expired but wasn't delivered to any peer.
func (h EnvelopeSignalHandler) EnvelopeExpired(identifiers [][]byte, err error) {
signal.SendEnvelopeExpired(identifiers, err)
}
// MailServerRequestCompleted triggered when the mailserver sends a message to notify that the request has been completed
func (h EnvelopeSignalHandler) MailServerRequestCompleted(requestID types.Hash, lastEnvelopeHash types.Hash, cursor []byte, err error) {
signal.SendMailServerRequestCompleted(requestID, lastEnvelopeHash, cursor, err)
}
// MailServerRequestExpired triggered when the mailserver request expires
func (h EnvelopeSignalHandler) MailServerRequestExpired(hash types.Hash) {
signal.SendMailServerRequestExpired(hash)
}
// PublisherSignalHandler sends signals on protocol events
type PublisherSignalHandler struct{}
func (h PublisherSignalHandler) DecryptMessageFailed(pubKey string) {
signal.SendDecryptMessageFailed(pubKey)
}
func (h PublisherSignalHandler) BundleAdded(identity string, installationID string) {
signal.SendBundleAdded(identity, installationID)
}
func (h PublisherSignalHandler) NewMessages(response *protocol.MessengerResponse) {
signal.SendNewMessages(response)
}
func (h PublisherSignalHandler) Stats(stats types.StatsSummary) {
signal.SendStats(stats)
}
// MessengerSignalHandler sends signals on messenger events
type MessengerSignalsHandler struct{}
// MessageDelivered passes information that message was delivered
func (m MessengerSignalsHandler) MessageDelivered(chatID string, messageID string) {
signal.SendMessageDelivered(chatID, messageID)
}
// BackupPerformed passes information that a backup was performed
func (m MessengerSignalsHandler) BackupPerformed(lastBackup uint64) {
signal.SendBackupPerformed(lastBackup)
}
// MessageDelivered passes info about community that was requested before
func (m MessengerSignalsHandler) CommunityInfoFound(community *communities.Community) {
signal.SendCommunityInfoFound(community)
}
func (m *MessengerSignalsHandler) MessengerResponse(response *protocol.MessengerResponse) {
PublisherSignalHandler{}.NewMessages(response)
}
func (m *MessengerSignalsHandler) HistoryRequestStarted(numBatches int) {
signal.SendHistoricMessagesRequestStarted(numBatches)
}
func (m *MessengerSignalsHandler) HistoryRequestCompleted() {
signal.SendHistoricMessagesRequestCompleted()
}
func (m *MessengerSignalsHandler) HistoryArchivesProtocolEnabled() {
signal.SendHistoryArchivesProtocolEnabled()
}
func (m *MessengerSignalsHandler) HistoryArchivesProtocolDisabled() {
signal.SendHistoryArchivesProtocolDisabled()
}
func (m *MessengerSignalsHandler) CreatingHistoryArchives(communityID string) {
signal.SendCreatingHistoryArchives(communityID)
}
func (m *MessengerSignalsHandler) NoHistoryArchivesCreated(communityID string, from int, to int) {
signal.SendNoHistoryArchivesCreated(communityID, from, to)
}
func (m *MessengerSignalsHandler) HistoryArchivesCreated(communityID string, from int, to int) {
signal.SendHistoryArchivesCreated(communityID, from, to)
}
func (m *MessengerSignalsHandler) HistoryArchivesSeeding(communityID string) {
signal.SendHistoryArchivesSeeding(communityID)
}
func (m *MessengerSignalsHandler) HistoryArchivesUnseeded(communityID string) {
signal.SendHistoryArchivesUnseeded(communityID)
}
func (m *MessengerSignalsHandler) HistoryArchiveDownloaded(communityID string, from int, to int) {
signal.SendHistoryArchiveDownloaded(communityID, from, to)
}
func (m *MessengerSignalsHandler) DownloadingHistoryArchivesStarted(communityID string) {
signal.SendDownloadingHistoryArchivesStarted(communityID)
}
func (m *MessengerSignalsHandler) ImportingHistoryArchiveMessages(communityID string) {
signal.SendImportingHistoryArchiveMessages(communityID)
}
func (m *MessengerSignalsHandler) DownloadingHistoryArchivesFinished(communityID string) {
signal.SendDownloadingHistoryArchivesFinished(communityID)
}
func (m *MessengerSignalsHandler) StatusUpdatesTimedOut(statusUpdates *[]protocol.UserStatus) {
signal.SendStatusUpdatesTimedOut(statusUpdates)
}
func (m *MessengerSignalsHandler) DiscordCategoriesAndChannelsExtracted(categories []*discord.Category, channels []*discord.Channel, oldestMessageTimestamp int64, errors map[string]*discord.ImportError) {
signal.SendDiscordCategoriesAndChannelsExtracted(categories, channels, oldestMessageTimestamp, errors)
}
func (m *MessengerSignalsHandler) DiscordCommunityImportProgress(importProgress *discord.ImportProgress) {
signal.SendDiscordCommunityImportProgress(importProgress)
}
func (m *MessengerSignalsHandler) DiscordChannelImportProgress(importProgress *discord.ImportProgress) {
signal.SendDiscordChannelImportProgress(importProgress)
}
func (m *MessengerSignalsHandler) DiscordCommunityImportFinished(id string) {
signal.SendDiscordCommunityImportFinished(id)
}
func (m *MessengerSignalsHandler) DiscordChannelImportFinished(communityID string, channelID string) {
signal.SendDiscordChannelImportFinished(communityID, channelID)
}
func (m *MessengerSignalsHandler) DiscordCommunityImportCancelled(id string) {
signal.SendDiscordCommunityImportCancelled(id)
}
func (m *MessengerSignalsHandler) DiscordCommunityImportCleanedUp(id string) {
signal.SendDiscordCommunityImportCleanedUp(id)
}
func (m *MessengerSignalsHandler) DiscordChannelImportCancelled(id string) {
signal.SendDiscordChannelImportCancelled(id)
}
func (m *MessengerSignalsHandler) SendWakuFetchingBackupProgress(response *wakusync.WakuBackedUpDataResponse) {
signal.SendWakuFetchingBackupProgress(response)
}
func (m *MessengerSignalsHandler) SendWakuBackedUpProfile(response *wakusync.WakuBackedUpDataResponse) {
signal.SendWakuBackedUpProfile(response)
}
func (m *MessengerSignalsHandler) SendWakuBackedUpSettings(response *wakusync.WakuBackedUpDataResponse) {
signal.SendWakuBackedUpSettings(response)
}
func (m *MessengerSignalsHandler) SendWakuBackedUpKeypair(response *wakusync.WakuBackedUpDataResponse) {
signal.SendWakuBackedUpKeypair(response)
}
func (m *MessengerSignalsHandler) SendWakuBackedUpWatchOnlyAccount(response *wakusync.WakuBackedUpDataResponse) {
signal.SendWakuBackedUpWatchOnlyAccount(response)
}
func (m *MessengerSignalsHandler) SendCuratedCommunitiesUpdate(response *communities.KnownCommunitiesResponse) {
signal.SendCuratedCommunitiesUpdate(response)
}

View File

@@ -0,0 +1,162 @@
package gif
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
)
type Gif struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
TinyURL string `json:"tinyUrl"`
Height int `json:"height"`
IsFavorite bool `json:"isFavorite"`
}
type Container struct {
Items []Gif `json:"items"`
}
var tenorAPIKey = ""
var defaultParams = "&media_filter=minimal&limit=50&key="
const maxRetry = 3
const baseURL = "https://g.tenor.com/v1/"
func NewGifAPI(db *accounts.Database) *API {
return &API{db}
}
// API is class with methods available over RPC.
type API struct {
db *accounts.Database
}
func (api *API) SetTenorAPIKey(key string) (err error) {
log.Info("[GifAPI::SetTenorAPIKey]")
err = api.db.SaveSettingField(settings.GifAPIKey, key)
if err != nil {
return err
}
tenorAPIKey = key
return nil
}
func (api *API) GetContentWithRetry(path string) (value string, err error) {
var currentRetry = 0
var response *http.Response
for currentRetry < maxRetry {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
ResponseHeaderTimeout: time.Second * 1,
}
client := http.Client{
Timeout: 1 * time.Second,
Transport: transport,
}
response, err = client.Get(baseURL + path + defaultParams + tenorAPIKey)
if err != nil {
log.Error("can't get content from path %s with %s", path, err.Error())
currentRetry++
time.Sleep(100 * time.Millisecond)
} else {
break
}
}
if response == nil {
return "", fmt.Errorf("Could not reach Tenor API")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("Status error: %v", response.StatusCode)
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("Read body: %v", err)
}
return string(data), nil
}
func (api *API) FetchGifs(path string) (value string, err error) {
log.Info("[GifAPI::fetchGifs]")
return api.GetContentWithRetry(path)
}
func (api *API) UpdateRecentGifs(updatedGifs json.RawMessage) (err error) {
log.Info("[GifAPI::updateRecentGifs]")
recentGifsContainer := Container{}
err = json.Unmarshal(updatedGifs, &recentGifsContainer)
if err != nil {
return err
}
err = api.db.SaveSettingField(settings.GifRecents, recentGifsContainer.Items)
if err != nil {
return err
}
return nil
}
func (api *API) UpdateFavoriteGifs(updatedGifs json.RawMessage) (err error) {
log.Info("[GifAPI::updateFavoriteGifs]", updatedGifs)
favsGifsContainer := Container{}
err = json.Unmarshal(updatedGifs, &favsGifsContainer)
if err != nil {
return err
}
err = api.db.SaveSettingField(settings.GifFavourites, favsGifsContainer.Items)
if err != nil {
return err
}
return nil
}
func (api *API) GetRecentGifs() (recentGifs []Gif, err error) {
log.Info("[GifAPI::getRecentGifs]")
gifs, err := api.db.GifRecents()
if err != nil {
return nil, err
}
recentGifs = make([]Gif, 0)
savedRecentGifs := []Gif{}
if len(gifs) > 0 {
err = json.Unmarshal(gifs, &savedRecentGifs)
if err != nil {
return nil, err
}
recentGifs = savedRecentGifs
}
return recentGifs, nil
}
func (api *API) GetFavoriteGifs() (favoriteGifs []Gif, err error) {
log.Info("[GifAPI::getFavoriteGifs]")
gifs, err := api.db.GifFavorites()
if err != nil {
return nil, err
}
favoriteGifs = make([]Gif, 0)
savedFavGifs := []Gif{}
if len(gifs) > 0 {
err = json.Unmarshal(gifs, &savedFavGifs)
if err != nil {
return nil, err
}
favoriteGifs = savedFavGifs
}
return favoriteGifs, nil
}

View File

@@ -0,0 +1,45 @@
package gif
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/multiaccounts/accounts"
)
// Service represents out own implementation of personal sign operations.
type Service struct {
accountsDB *accounts.Database
}
// New returns a new Service.
func NewService(db *accounts.Database) *Service {
return &Service{accountsDB: db}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "gif",
Version: "0.1.0",
Service: NewGifAPI(s.accountsDB),
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return nil
}

View File

@@ -0,0 +1,31 @@
package localnotifications
import (
"context"
"github.com/ethereum/go-ethereum/log"
)
func NewAPI(s *Service) *API {
return &API{s}
}
type API struct {
s *Service
}
func (api *API) NotificationPreferences(ctx context.Context) ([]NotificationPreference, error) {
return api.s.db.GetPreferences()
}
func (api *API) SwitchWalletNotifications(ctx context.Context, preference bool) error {
log.Debug("Switch Transaction Notification")
err := api.s.db.ChangeWalletPreference(preference)
if err != nil {
return err
}
api.s.WatchingEnabled = preference
return nil
}

View File

@@ -0,0 +1,233 @@
package localnotifications
import (
"database/sql"
"encoding/json"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/signal"
)
type PushCategory string
type NotificationType string
type NotificationBody interface {
json.Marshaler
}
type Notification struct {
ID common.Hash
Platform float32
Body NotificationBody
BodyType NotificationType
Title string
Message string
Category PushCategory
Deeplink string
Image string
IsScheduled bool
ScheduledTime string
IsConversation bool
IsGroupConversation bool
ConversationID string
Timestamp uint64
Author NotificationAuthor
Deleted bool
}
type NotificationAuthor struct {
ID string `json:"id"`
Icon string `json:"icon"`
Name string `json:"name"`
}
// notificationAlias is an interim struct used for json un/marshalling
type notificationAlias struct {
ID common.Hash `json:"id"`
Platform float32 `json:"platform,omitempty"`
Body json.RawMessage `json:"body"`
BodyType NotificationType `json:"bodyType"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Category PushCategory `json:"category,omitempty"`
Deeplink string `json:"deepLink,omitempty"`
Image string `json:"imageUrl,omitempty"`
IsScheduled bool `json:"isScheduled,omitempty"`
ScheduledTime string `json:"scheduleTime,omitempty"`
IsConversation bool `json:"isConversation,omitempty"`
IsGroupConversation bool `json:"isGroupConversation,omitempty"`
ConversationID string `json:"conversationId,omitempty"`
Timestamp uint64 `json:"timestamp,omitempty"`
Author NotificationAuthor `json:"notificationAuthor,omitempty"`
Deleted bool `json:"deleted,omitempty"`
}
// MessageEvent - structure used to pass messages from chat to bus
type MessageEvent struct{}
// CustomEvent - structure used to pass custom user set messages to bus
type CustomEvent struct{}
type transmitter struct {
publisher *event.Feed
wg sync.WaitGroup
quit chan struct{}
}
// Service keeps the state of message bus
type Service struct {
started bool
WatchingEnabled bool
chainID uint64
transmitter *transmitter
walletTransmitter *transmitter
db *Database
walletDB *transfer.Database
accountsDB *accounts.Database
}
func NewService(appDB *sql.DB, walletDB *transfer.Database, chainID uint64) (*Service, error) {
db := NewDB(appDB, chainID)
accountsDB, err := accounts.NewDB(appDB)
if err != nil {
return nil, err
}
trans := &transmitter{}
walletTrans := &transmitter{}
return &Service{
db: db,
chainID: chainID,
walletDB: walletDB,
accountsDB: accountsDB,
transmitter: trans,
walletTransmitter: walletTrans,
}, nil
}
func (n *Notification) MarshalJSON() ([]byte, error) {
var body json.RawMessage
if n.Body != nil {
encodedBody, err := n.Body.MarshalJSON()
if err != nil {
return nil, err
}
body = encodedBody
}
alias := notificationAlias{
ID: n.ID,
Platform: n.Platform,
Body: body,
BodyType: n.BodyType,
Category: n.Category,
Title: n.Title,
Message: n.Message,
Deeplink: n.Deeplink,
Image: n.Image,
IsScheduled: n.IsScheduled,
ScheduledTime: n.ScheduledTime,
IsConversation: n.IsConversation,
IsGroupConversation: n.IsGroupConversation,
ConversationID: n.ConversationID,
Timestamp: n.Timestamp,
Author: n.Author,
Deleted: n.Deleted,
}
return json.Marshal(alias)
}
func PushMessages(ns []*Notification) {
for _, n := range ns {
pushMessage(n)
}
}
func pushMessage(notification *Notification) {
log.Debug("Pushing a new push notification")
signal.SendLocalNotifications(notification)
}
// Start Worker which processes all incoming messages
func (s *Service) Start() error {
s.started = true
s.transmitter.quit = make(chan struct{})
s.transmitter.publisher = &event.Feed{}
events := make(chan TransactionEvent, 10)
sub := s.transmitter.publisher.Subscribe(events)
s.transmitter.wg.Add(1)
go func() {
defer s.transmitter.wg.Done()
for {
select {
case <-s.transmitter.quit:
sub.Unsubscribe()
return
case err := <-sub.Err():
if err != nil {
log.Error("Local notifications transmitter failed with", "error", err)
}
return
case event := <-events:
s.transactionsHandler(event)
}
}
}()
log.Info("Successful start")
return nil
}
// Stop worker
func (s *Service) Stop() error {
s.started = false
if s.transmitter.quit != nil {
close(s.transmitter.quit)
s.transmitter.wg.Wait()
s.transmitter.quit = nil
}
if s.walletTransmitter.quit != nil {
close(s.walletTransmitter.quit)
s.walletTransmitter.wg.Wait()
s.walletTransmitter.quit = nil
}
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "localnotifications",
Version: "0.1.0",
Service: NewAPI(s),
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}
func (s *Service) IsStarted() bool {
return s.started
}

View File

@@ -0,0 +1,53 @@
package localnotifications
import (
"database/sql"
)
type Database struct {
db *sql.DB
network uint64
}
type NotificationPreference struct {
Enabled bool `json:"enabled"`
Service string `json:"service"`
Event string `json:"event,omitempty"`
Identifier string `json:"identifier,omitempty"`
}
func NewDB(db *sql.DB, network uint64) *Database {
return &Database{db: db, network: network}
}
func (db *Database) GetPreferences() (rst []NotificationPreference, err error) {
rows, err := db.db.Query("SELECT service, event, identifier, enabled FROM local_notifications_preferences")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
pref := NotificationPreference{}
err = rows.Scan(&pref.Service, &pref.Event, &pref.Identifier, &pref.Enabled)
if err != nil {
return nil, err
}
rst = append(rst, pref)
}
return rst, nil
}
func (db *Database) GetWalletPreference() (rst NotificationPreference, err error) {
pref := db.db.QueryRow("SELECT service, event, identifier, enabled FROM local_notifications_preferences WHERE service = 'wallet' AND event = 'transaction' AND identifier = 'all'")
err = pref.Scan(&rst.Service, &rst.Event, &rst.Identifier, &rst.Enabled)
if err == sql.ErrNoRows {
return rst, nil
}
return
}
func (db *Database) ChangeWalletPreference(preference bool) error {
_, err := db.db.Exec("INSERT OR REPLACE INTO local_notifications_preferences (service, event, identifier, enabled) VALUES ('wallet', 'transaction', 'all', ?)", preference)
return err
}

View File

@@ -0,0 +1,230 @@
package localnotifications
import (
"encoding/json"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletevent"
)
type transactionState string
const (
walletDeeplinkPrefix = "status-app://wallet/"
failed transactionState = "failed"
inbound transactionState = "inbound"
outbound transactionState = "outbound"
)
// TransactionEvent - structure used to pass messages from wallet to bus
type TransactionEvent struct {
Type string `json:"type"`
BlockNumber *big.Int `json:"block-number"`
Accounts []common.Address `json:"accounts"`
MaxKnownBlocks map[common.Address]*big.Int `json:"max-known-blocks"`
}
type transactionBody struct {
State transactionState `json:"state"`
From common.Address `json:"from"`
To common.Address `json:"to"`
FromAccount *accounts.Account `json:"fromAccount,omitempty"`
ToAccount *accounts.Account `json:"toAccount,omitempty"`
Value *hexutil.Big `json:"value"`
ERC20 bool `json:"erc20"`
Contract common.Address `json:"contract"`
Network uint64 `json:"network"`
}
func (t transactionBody) MarshalJSON() ([]byte, error) {
type Alias transactionBody
item := struct{ *Alias }{Alias: (*Alias)(&t)}
return json.Marshal(item)
}
func (s *Service) buildTransactionNotification(rawTransfer transfer.Transfer) *Notification {
log.Info("Handled a new transfer in buildTransactionNotification", "info", rawTransfer)
var deeplink string
var state transactionState
transfer := transfer.CastToTransferView(rawTransfer)
switch {
case transfer.TxStatus == hexutil.Uint64(0):
state = failed
case transfer.Address == transfer.To:
state = inbound
default:
state = outbound
}
from, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.From))
if err != nil {
log.Debug("Could not select From account by address", "error", err)
}
to, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.To))
if err != nil {
log.Debug("Could not select To account by address", "error", err)
}
if from != nil {
deeplink = walletDeeplinkPrefix + from.Address.String()
} else if to != nil {
deeplink = walletDeeplinkPrefix + to.Address.String()
}
body := transactionBody{
State: state,
From: transfer.From,
To: transfer.Address,
FromAccount: from,
ToAccount: to,
Value: transfer.Value,
ERC20: string(transfer.Type) == "erc20",
Contract: transfer.Contract,
Network: transfer.NetworkID,
}
return &Notification{
BodyType: TypeTransaction,
ID: transfer.ID,
Body: body,
Deeplink: deeplink,
Category: CategoryTransaction,
}
}
func (s *Service) transactionsHandler(payload TransactionEvent) {
log.Info("Handled a new transaction", "info", payload)
limit := 20
if payload.BlockNumber != nil {
for _, address := range payload.Accounts {
if payload.BlockNumber.Cmp(payload.MaxKnownBlocks[address]) >= 0 {
log.Info("Handled transfer for address", "info", address)
transfers, err := s.walletDB.GetTransfersByAddressAndBlock(s.chainID, address, payload.BlockNumber, int64(limit))
if err != nil {
log.Error("Could not fetch transfers", "error", err)
}
for _, transaction := range transfers {
n := s.buildTransactionNotification(transaction)
pushMessage(n)
}
}
}
}
}
// SubscribeWallet - Subscribes to wallet signals
func (s *Service) SubscribeWallet(publisher *event.Feed) error {
s.walletTransmitter.publisher = publisher
preference, err := s.db.GetWalletPreference()
if err != nil {
log.Error("Failed to get wallet preference", "error", err)
s.WatchingEnabled = false
} else {
s.WatchingEnabled = preference.Enabled
}
s.StartWalletWatcher()
return err
}
// StartWalletWatcher - Forward wallet events to notifications
func (s *Service) StartWalletWatcher() {
if s.walletTransmitter.quit != nil {
// already running, nothing to do
return
}
if s.walletTransmitter.publisher == nil {
log.Error("wallet publisher was not initialized")
return
}
s.walletTransmitter.quit = make(chan struct{})
events := make(chan walletevent.Event, 10)
sub := s.walletTransmitter.publisher.Subscribe(events)
s.walletTransmitter.wg.Add(1)
maxKnownBlocks := map[common.Address]*big.Int{}
go func() {
defer s.walletTransmitter.wg.Done()
historyReady := false
for {
select {
case <-s.walletTransmitter.quit:
sub.Unsubscribe()
return
case err := <-sub.Err():
// technically event.Feed cannot send an error to subscription.Err channel.
// the only time we will get an event is when that channel is closed.
if err != nil {
log.Error("wallet signals transmitter failed with", "error", err)
}
return
case event := <-events:
if event.Type == transfer.EventNewTransfers && historyReady && event.BlockNumber != nil {
newBlocks := false
for _, address := range event.Accounts {
if _, ok := maxKnownBlocks[address]; !ok {
newBlocks = true
maxKnownBlocks[address] = event.BlockNumber
} else if event.BlockNumber.Cmp(maxKnownBlocks[address]) == 1 {
maxKnownBlocks[address] = event.BlockNumber
newBlocks = true
}
}
if newBlocks && s.WatchingEnabled {
s.transmitter.publisher.Send(TransactionEvent{
Type: string(event.Type),
BlockNumber: event.BlockNumber,
Accounts: event.Accounts,
MaxKnownBlocks: maxKnownBlocks,
})
}
} else if event.Type == transfer.EventRecentHistoryReady {
historyReady = true
if event.BlockNumber != nil {
for _, address := range event.Accounts {
if _, ok := maxKnownBlocks[address]; !ok {
maxKnownBlocks[address] = event.BlockNumber
}
}
}
}
}
}
}()
}
// StopWalletWatcher - stops watching for new wallet events
func (s *Service) StopWalletWatcher() {
if s.walletTransmitter.quit != nil {
close(s.walletTransmitter.quit)
s.walletTransmitter.wg.Wait()
s.walletTransmitter.quit = nil
}
}
// IsWatchingWallet - check if local-notifications are subscribed to wallet updates
func (s *Service) IsWatchingWallet() bool {
return s.walletTransmitter.quit != nil
}

View File

@@ -0,0 +1,11 @@
package localnotifications
const (
CategoryTransaction PushCategory = "transaction"
CategoryMessage PushCategory = "newMessage"
CategoryGroupInvite PushCategory = "groupInvite"
CategoryCommunityRequestToJoin = "communityRequestToJoin"
TypeTransaction NotificationType = "transaction"
TypeMessage NotificationType = "message"
)

View File

@@ -0,0 +1,118 @@
Mailservers Service
================
Mailservers service provides read/write API for `Mailserver` object
which stores details about user's mailservers.
To enable this service, include `mailservers` in APIModules:
```json
{
"MailserversConfig": {
"Enabled": true
},
"APIModules": "mailservers"
}
```
API
---
Enabling service will expose three additional methods:
#### mailservers_addMailserver
Stores `Mailserver` in the database.
```json
{
"id": "1",
"name": "my mailserver",
"address": "enode://...",
"password": "some-pass",
"fleet": "prod"
}
```
#### mailservers_getMailservers
Reads all saved mailservers.
#### mailservers_deleteMailserver
Deletes a mailserver specified by an ID.
## Mailserver requests gap service
Mailserver request gaps service provides read/write API for `MailserverRequestGap` object
which stores details about the gaps between mailserver requests.
API
---
The service exposes four methods
#### mailserverrequestgaps_addMailserverRequestGaps
Stores `MailserverRequestGap` in the database.
All fields are specified below:
```json
{
"id": "1",
"chatId": "chat-id",
"from": 1,
"to": 2
}
```
#### mailservers_getMailserverRequestGaps
Reads all saved mailserver request gaps by chatID.
#### mailservers_deleteMailserverRequestGaps
Deletes all MailserverRequestGaps specified by IDs.
#### mailservers_deleteMailserverRequestGapsByChatID
Deletes all MailserverRequestGaps specified by chatID.
#### mailservers_addMailserverTopic
Stores `MailserverTopic` in the database.
```json
{
"topic": "topic-as-string",
"chat-ids": ["a", "list", "of", "chatIDs"],
"last-request": 1
}
```
#### mailservers_getMailserverTopics
Reads all saved mailserver topics.
#### mailservers_deleteMailserverTopic
Deletes a mailserver topic using `topic` as an identifier.
#### mailservers_addChatRequestRange
Stores `ChatRequestRange` in the database.
```json
{
"chat-id": "chat-id-001",
"lowest-request-from": 1567693421154,
"highest-request-to": 1567693576779
}
```
#### mailservers_getChatRequestRanges
Reads all saved chat request ranges.
#### mailservers_deleteChatRequestRange
Deletes a chat request range by `chat-id`.

View File

@@ -0,0 +1,72 @@
package mailservers
import "context"
func NewAPI(db *Database) *API {
return &API{db}
}
// API is class with methods available over RPC.
type API struct {
db *Database
}
func (a *API) AddMailserver(ctx context.Context, m Mailserver) error {
return a.db.Add(m)
}
func (a *API) GetMailservers(ctx context.Context) ([]Mailserver, error) {
return a.db.Mailservers()
}
func (a *API) DeleteMailserver(ctx context.Context, id string) error {
return a.db.Delete(id)
}
func (a *API) AddMailserverRequestGaps(ctx context.Context, gaps []MailserverRequestGap) error {
return a.db.AddGaps(gaps)
}
func (a *API) GetMailserverRequestGaps(ctx context.Context, chatID string) ([]MailserverRequestGap, error) {
return a.db.RequestGaps(chatID)
}
func (a *API) DeleteMailserverRequestGaps(ctx context.Context, ids []string) error {
return a.db.DeleteGaps(ids)
}
func (a *API) DeleteMailserverRequestGapsByChatID(ctx context.Context, chatID string) error {
return a.db.DeleteGapsByChatID(chatID)
}
func (a *API) AddMailserverTopic(ctx context.Context, topic MailserverTopic) error {
return a.db.AddTopic(topic)
}
func (a *API) AddMailserverTopics(ctx context.Context, topics []MailserverTopic) error {
return a.db.AddTopics(topics)
}
func (a *API) GetMailserverTopics(ctx context.Context) ([]MailserverTopic, error) {
return a.db.Topics()
}
func (a *API) DeleteMailserverTopic(ctx context.Context, pubsubTopic string, topic string) error {
return a.db.DeleteTopic(pubsubTopic, topic)
}
func (a *API) AddChatRequestRange(ctx context.Context, req ChatRequestRange) error {
return a.db.AddChatRequestRange(req)
}
func (a *API) AddChatRequestRanges(ctx context.Context, reqs []ChatRequestRange) error {
return a.db.AddChatRequestRanges(reqs)
}
func (a *API) GetChatRequestRanges(ctx context.Context) ([]ChatRequestRange, error) {
return a.db.ChatRequestRanges()
}
func (a *API) DeleteChatRequestRange(ctx context.Context, chatID string) error {
return a.db.DeleteChatRequestRange(chatID)
}

View File

@@ -0,0 +1,501 @@
package mailservers
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/protocol/transport"
)
type Mailserver struct {
ID string `json:"id"`
Name string `json:"name"`
Custom bool `json:"custom"`
Address string `json:"address"`
Password string `json:"password,omitempty"`
Fleet string `json:"fleet"`
Version uint `json:"version"`
FailedRequests uint `json:"-"`
}
func (m Mailserver) Enode() (*enode.Node, error) {
return enode.ParseV4(m.Address)
}
func (m Mailserver) IDBytes() ([]byte, error) {
if m.Version == 2 {
id, err := peer.Decode(m.UniqueID())
if err != nil {
return nil, err
}
return []byte(id.Pretty()), err
}
node, err := enode.ParseV4(m.Address)
if err != nil {
return nil, err
}
return node.ID().Bytes(), nil
}
func (m Mailserver) PeerID() (peer.ID, error) {
if m.Version != 2 {
return "", errors.New("not available")
}
pID, err := peer.Decode(m.UniqueID())
if err != nil {
return "", err
}
return pID, nil
}
func (m Mailserver) UniqueID() string {
if m.Version == 2 {
s := strings.Split(m.Address, "/")
return s[len(s)-1]
}
return m.Address
}
func (m Mailserver) nullablePassword() (val sql.NullString) {
if m.Password != "" {
val.String = m.Password
val.Valid = true
}
return
}
type MailserverRequestGap struct {
ID string `json:"id"`
ChatID string `json:"chatId"`
From uint64 `json:"from"`
To uint64 `json:"to"`
}
type MailserverTopic struct {
PubsubTopic string `json:"pubsubTopic"`
ContentTopic string `json:"topic"`
Discovery bool `json:"discovery?"`
Negotiated bool `json:"negotiated?"`
ChatIDs []string `json:"chat-ids"`
LastRequest int `json:"last-request"` // default is 1
}
type ChatRequestRange struct {
ChatID string `json:"chat-id"`
LowestRequestFrom int `json:"lowest-request-from"`
HighestRequestTo int `json:"highest-request-to"`
}
// sqlStringSlice helps to serialize a slice of strings into a single column using JSON serialization.
type sqlStringSlice []string
// Scan implements the Scanner interface.
func (ss *sqlStringSlice) Scan(value interface{}) error {
if value == nil {
*ss = nil
return nil
}
src, ok := value.([]byte)
if !ok {
return errors.New("invalid value type, expected byte slice")
}
return json.Unmarshal(src, ss)
}
// Value implements the driver Valuer interface.
func (ss sqlStringSlice) Value() (driver.Value, error) {
return json.Marshal(ss)
}
// Database sql wrapper for operations with mailserver objects.
type Database struct {
db *sql.DB
}
func NewDB(db *sql.DB) *Database {
return &Database{db: db}
}
func (d *Database) Add(mailserver Mailserver) error {
_, err := d.db.Exec(`INSERT OR REPLACE INTO mailservers(
id,
name,
address,
password,
fleet
) VALUES (?, ?, ?, ?, ?)`,
mailserver.ID,
mailserver.Name,
mailserver.Address,
mailserver.nullablePassword(),
mailserver.Fleet,
)
return err
}
func (d *Database) Mailservers() ([]Mailserver, error) {
var result []Mailserver
rows, err := d.db.Query(`SELECT id, name, address, password, fleet FROM mailservers`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var (
m Mailserver
password sql.NullString
)
if err := rows.Scan(
&m.ID,
&m.Name,
&m.Address,
&password,
&m.Fleet,
); err != nil {
return nil, err
}
m.Custom = true
if password.Valid {
m.Password = password.String
}
result = append(result, m)
}
return result, nil
}
func (d *Database) Delete(id string) error {
_, err := d.db.Exec(`DELETE FROM mailservers WHERE id = ?`, id)
return err
}
func (d *Database) AddGaps(gaps []MailserverRequestGap) error {
tx, err := d.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
for _, gap := range gaps {
_, err := tx.Exec(`INSERT OR REPLACE INTO mailserver_request_gaps(
id,
chat_id,
gap_from,
gap_to
) VALUES (?, ?, ?, ?)`,
gap.ID,
gap.ChatID,
gap.From,
gap.To,
)
if err != nil {
return err
}
}
return nil
}
func (d *Database) RequestGaps(chatID string) ([]MailserverRequestGap, error) {
var result []MailserverRequestGap
rows, err := d.db.Query(`SELECT id, chat_id, gap_from, gap_to FROM mailserver_request_gaps WHERE chat_id = ?`, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var m MailserverRequestGap
if err := rows.Scan(
&m.ID,
&m.ChatID,
&m.From,
&m.To,
); err != nil {
return nil, err
}
result = append(result, m)
}
return result, nil
}
func (d *Database) DeleteGaps(ids []string) error {
if len(ids) == 0 {
return nil
}
inVector := strings.Repeat("?, ", len(ids)-1) + "?"
query := fmt.Sprintf(`DELETE FROM mailserver_request_gaps WHERE id IN (%s)`, inVector) // nolint: gosec
idsArgs := make([]interface{}, 0, len(ids))
for _, id := range ids {
idsArgs = append(idsArgs, id)
}
_, err := d.db.Exec(query, idsArgs...)
return err
}
func (d *Database) DeleteGapsByChatID(chatID string) error {
_, err := d.db.Exec(`DELETE FROM mailserver_request_gaps WHERE chat_id = ?`, chatID)
return err
}
func (d *Database) AddTopic(topic MailserverTopic) error {
chatIDs := sqlStringSlice(topic.ChatIDs)
_, err := d.db.Exec(`INSERT OR REPLACE INTO mailserver_topics(
pubsub_topic,
topic,
chat_ids,
last_request,
discovery,
negotiated
) VALUES (?, ?, ?, ?, ?, ?)`,
topic.PubsubTopic,
topic.ContentTopic,
chatIDs,
topic.LastRequest,
topic.Discovery,
topic.Negotiated,
)
return err
}
func (d *Database) AddTopics(topics []MailserverTopic) (err error) {
var tx *sql.Tx
tx, err = d.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
for _, topic := range topics {
chatIDs := sqlStringSlice(topic.ChatIDs)
_, err = tx.Exec(`INSERT OR REPLACE INTO mailserver_topics(
pubsub_topic,
topic,
chat_ids,
last_request,
discovery,
negotiated
) VALUES (?, ?, ?, ?, ?, ?)`,
topic.PubsubTopic,
topic.ContentTopic,
chatIDs,
topic.LastRequest,
topic.Discovery,
topic.Negotiated,
)
if err != nil {
return
}
}
return
}
func (d *Database) Topics() ([]MailserverTopic, error) {
var result []MailserverTopic
rows, err := d.db.Query(`SELECT pubsub_topic, topic, chat_ids, last_request,discovery,negotiated FROM mailserver_topics`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var (
t MailserverTopic
chatIDs sqlStringSlice
)
if err := rows.Scan(
&t.PubsubTopic,
&t.ContentTopic,
&chatIDs,
&t.LastRequest,
&t.Discovery,
&t.Negotiated,
); err != nil {
return nil, err
}
t.ChatIDs = chatIDs
result = append(result, t)
}
return result, nil
}
func (d *Database) ResetLastRequest(pubsubTopic, contentTopic string) error {
_, err := d.db.Exec("UPDATE mailserver_topics SET last_request = 0 WHERE pubsub_topic = ? AND topic = ?", pubsubTopic, contentTopic)
return err
}
func (d *Database) DeleteTopic(pubsubTopic, contentTopic string) error {
_, err := d.db.Exec(`DELETE FROM mailserver_topics WHERE pubsub_topic = ? AND topic = ?`, pubsubTopic, contentTopic)
return err
}
// SetTopics deletes all topics excepts the one set, or upsert those if
// missing
func (d *Database) SetTopics(filters []*transport.Filter) (err error) {
var tx *sql.Tx
tx, err = d.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
if len(filters) == 0 {
return nil
}
contentTopicsPerPubsubTopic := make(map[string]map[string]struct{})
for _, filter := range filters {
contentTopics, ok := contentTopicsPerPubsubTopic[filter.PubsubTopic]
if !ok {
contentTopics = make(map[string]struct{})
}
contentTopics[filter.ContentTopic.String()] = struct{}{}
contentTopicsPerPubsubTopic[filter.PubsubTopic] = contentTopics
}
for pubsubTopic, contentTopics := range contentTopicsPerPubsubTopic {
topicsArgs := make([]interface{}, 0, len(contentTopics)+1)
topicsArgs = append(topicsArgs, pubsubTopic)
for ct := range contentTopics {
topicsArgs = append(topicsArgs, ct)
}
inVector := strings.Repeat("?, ", len(contentTopics)-1) + "?"
// Delete topics
query := "DELETE FROM mailserver_topics WHERE pubsub_topic = ? AND topic NOT IN (" + inVector + ")" // nolint: gosec
_, err = tx.Exec(query, topicsArgs...)
}
// Default to now - 1.day
lastRequest := (time.Now().Add(-24 * time.Hour)).Unix()
// Insert if not existing
for _, filter := range filters {
// fetch
var topic string
err = tx.QueryRow(`SELECT topic FROM mailserver_topics WHERE topic = ? AND pubsub_topic = ?`, filter.ContentTopic.String(), filter.PubsubTopic).Scan(&topic)
if err != nil && err != sql.ErrNoRows {
return
} else if err == sql.ErrNoRows {
// we insert the topic
_, err = tx.Exec(`INSERT INTO mailserver_topics(topic,pubsub_topic,last_request,discovery,negotiated) VALUES (?,?,?,?,?)`, filter.ContentTopic.String(), filter.PubsubTopic, lastRequest, filter.Discovery, filter.Negotiated)
}
if err != nil {
return
}
}
return
}
func (d *Database) AddChatRequestRange(req ChatRequestRange) error {
_, err := d.db.Exec(`INSERT OR REPLACE INTO mailserver_chat_request_ranges(
chat_id,
lowest_request_from,
highest_request_to
) VALUES (?, ?, ?)`,
req.ChatID,
req.LowestRequestFrom,
req.HighestRequestTo,
)
return err
}
func (d *Database) AddChatRequestRanges(reqs []ChatRequestRange) (err error) {
var tx *sql.Tx
tx, err = d.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
for _, req := range reqs {
_, err = tx.Exec(`INSERT OR REPLACE INTO mailserver_chat_request_ranges(
chat_id,
lowest_request_from,
highest_request_to
) VALUES (?, ?, ?)`,
req.ChatID,
req.LowestRequestFrom,
req.HighestRequestTo,
)
if err != nil {
return
}
}
return
}
func (d *Database) ChatRequestRanges() ([]ChatRequestRange, error) {
var result []ChatRequestRange
rows, err := d.db.Query(`SELECT chat_id, lowest_request_from, highest_request_to FROM mailserver_chat_request_ranges`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var req ChatRequestRange
if err := rows.Scan(
&req.ChatID,
&req.LowestRequestFrom,
&req.HighestRequestTo,
); err != nil {
return nil, err
}
result = append(result, req)
}
return result, nil
}
func (d *Database) DeleteChatRequestRange(chatID string) error {
_, err := d.db.Exec(`DELETE FROM mailserver_chat_request_ranges WHERE chat_id = ?`, chatID)
return err
}

View File

@@ -0,0 +1,235 @@
package mailservers
import "github.com/status-im/status-go/params"
func DefaultMailserversByFleet(fleet string) []Mailserver {
var items []Mailserver
for _, ms := range DefaultMailservers() {
if ms.Fleet == fleet {
items = append(items, ms)
}
}
return items
}
func DefaultMailservers() []Mailserver {
return []Mailserver{
Mailserver{
ID: "mail-01.ac-cn-hongkong-c.eth.prod",
Address: "enode://606ae04a71e5db868a722c77a21c8244ae38f1bd6e81687cc6cfe88a3063fa1c245692232f64f45bd5408fed5133eab8ed78049332b04f9c110eac7f71c1b429@47.75.247.214:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-01.do-ams3.eth.prod",
Address: "enode://c42f368a23fa98ee546fd247220759062323249ef657d26d357a777443aec04db1b29a3a22ef3e7c548e18493ddaf51a31b0aed6079bd6ebe5ae838fcfaf3a49@178.128.142.54:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-01.gc-us-central1-a.eth.prod",
Address: "enode://ee2b53b0ace9692167a410514bca3024695dbf0e1a68e1dff9716da620efb195f04a4b9e873fb9b74ac84de801106c465b8e2b6c4f0d93b8749d1578bfcaf03e@104.197.238.144:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-02.ac-cn-hongkong-c.eth.prod",
Address: "enode://2c8de3cbb27a3d30cbb5b3e003bc722b126f5aef82e2052aaef032ca94e0c7ad219e533ba88c70585ebd802de206693255335b100307645ab5170e88620d2a81@47.244.221.14:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-02.do-ams3.eth.prod",
Address: "enode://7aa648d6e855950b2e3d3bf220c496e0cae4adfddef3e1e6062e6b177aec93bc6cdcf1282cb40d1656932ebfdd565729da440368d7c4da7dbd4d004b1ac02bf8@178.128.142.26:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-02.gc-us-central1-a.eth.prod",
Address: "enode://30211cbd81c25f07b03a0196d56e6ce4604bb13db773ff1c0ea2253547fafd6c06eae6ad3533e2ba39d59564cfbdbb5e2ce7c137a5ebb85e99dcfc7a75f99f55@23.236.58.92:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-03.ac-cn-hongkong-c.eth.prod",
Address: "enode://e85f1d4209f2f99da801af18db8716e584a28ad0bdc47fbdcd8f26af74dbd97fc279144680553ec7cd9092afe683ddea1e0f9fc571ebcb4b1d857c03a088853d@47.244.129.82:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-03.do-ams3.eth.prod",
Address: "enode://8a64b3c349a2e0ef4a32ea49609ed6eb3364be1110253c20adc17a3cebbc39a219e5d3e13b151c0eee5d8e0f9a8ba2cd026014e67b41a4ab7d1d5dd67ca27427@178.128.142.94:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-03.gc-us-central1-a.eth.prod",
Address: "enode://44160e22e8b42bd32a06c1532165fa9e096eebedd7fa6d6e5f8bbef0440bc4a4591fe3651be68193a7ec029021cdb496cfe1d7f9f1dc69eb99226e6f39a7a5d4@35.225.221.245:443",
Fleet: params.FleetProd,
Version: 1,
},
Mailserver{
ID: "mail-01.ac-cn-hongkong-c.eth.staging",
Address: "enode://b74859176c9751d314aeeffc26ec9f866a412752e7ddec91b19018a18e7cca8d637cfe2cedcb972f8eb64d816fbd5b4e89c7e8c7fd7df8a1329fa43db80b0bfe@47.52.90.156:443",
Fleet: params.FleetStaging,
Version: 1,
},
Mailserver{
ID: "mail-01.do-ams3.eth.staging",
Address: "enode://69f72baa7f1722d111a8c9c68c39a31430e9d567695f6108f31ccb6cd8f0adff4991e7fdca8fa770e75bc8a511a87d24690cbc80e008175f40c157d6f6788d48@206.189.240.16:443",
Fleet: params.FleetStaging,
Version: 1,
},
Mailserver{
ID: "mail-01.gc-us-central1-a.eth.staging",
Address: "enode://e4fc10c1f65c8aed83ac26bc1bfb21a45cc1a8550a58077c8d2de2a0e0cd18e40fd40f7e6f7d02dc6cd06982b014ce88d6e468725ffe2c138e958788d0002a7f@35.239.193.41:443",
Fleet: params.FleetStaging,
Version: 1,
},
Mailserver{
ID: "mail-01.ac-cn-hongkong-c.eth.test",
Address: "enode://619dbb5dda12e85bf0eb5db40fb3de625609043242737c0e975f7dfd659d85dc6d9a84f9461a728c5ab68c072fed38ca6a53917ca24b8e93cc27bdef3a1e79ac@47.52.188.196:443",
Fleet: params.FleetTest,
Version: 1,
},
Mailserver{
ID: "mail-01.do-ams3.eth.test",
Address: "enode://e4865fe6c2a9c1a563a6447990d8e9ce672644ae3e08277ce38ec1f1b690eef6320c07a5d60c3b629f5d4494f93d6b86a745a0bf64ab295bbf6579017adc6ed8@206.189.243.161:443",
Fleet: params.FleetTest,
Version: 1,
},
Mailserver{
ID: "mail-01.gc-us-central1-a.eth.test",
Address: "enode://707e57453acd3e488c44b9d0e17975371e2f8fb67525eae5baca9b9c8e06c86cde7c794a6c2e36203bf9f56cae8b0e50f3b33c4c2b694a7baeea1754464ce4e3@35.192.229.172:443",
Fleet: params.FleetTest,
Version: 1,
},
Mailserver{
ID: "node-01.ac-cn-hongkong-c.wakuv2.prod",
Address: "/ip4/8.210.222.231/tcp/30303/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD",
Fleet: params.FleetWakuV2Prod,
Version: 2,
},
Mailserver{
ID: "node-01.do-ams3.wakuv2.prod",
Address: "/ip4/188.166.135.145/tcp/30303/p2p/16Uiu2HAmL5okWopX7NqZWBUKVqW8iUxCEmd5GMHLVPwCgzYzQv3e",
Fleet: params.FleetWakuV2Prod,
Version: 2,
},
Mailserver{
ID: "node-01.gc-us-central1-a.wakuv2.prod",
Address: "/ip4/34.121.100.108/tcp/30303/p2p/16Uiu2HAmVkKntsECaYfefR1V2yCR79CegLATuTPE6B9TxgxBiiiA",
Fleet: params.FleetWakuV2Prod,
Version: 2,
},
Mailserver{
ID: "node-01.ac-cn-hongkong-c.wakuv2.test",
Address: "/ip4/47.242.210.73/tcp/30303/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm",
Fleet: params.FleetWakuV2Test,
Version: 2,
},
Mailserver{
ID: "node-01.do-ams3.wakuv2.test",
Address: "/ip4/134.209.139.210/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ",
Fleet: params.FleetWakuV2Test,
Version: 2,
},
Mailserver{
ID: "node-01.gc-us-central1-a.wakuv2.test",
Address: "/ip4/104.154.239.128/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS",
Fleet: params.FleetWakuV2Test,
Version: 2,
},
Mailserver{
ID: "node-01.ac-cn-hongkong-c.status.prod",
Address: "/dns4/node-01.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkvEZgh3KLwhLwXg95e5ojM8XykJ4Kxi2T7hk22rnA7pJC",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-01.do-ams3.status.prod",
Address: "/dns4/node-01.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAm6HZZr7aToTvEBPpiys4UxajCTU97zj5v7RNR2gbniy1D",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-01.gc-us-central1-a.status.prod",
Address: "/dns4/node-01.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkwBp8T6G77kQXSNMnxgaMky1JeyML5yqoTHRM8dbeCBNb",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-02.ac-cn-hongkong-c.status.prod",
Address: "/dns4/node-02.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmFy8BrJhCEmCYrUfBdSNkrPw6VHExtv4rRp1DSBnCPgx8",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-02.do-ams3.status.prod",
Address: "/dns4/node-02.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmSve7tR5YZugpskMv2dmJAsMUKmfWYEKRXNUxRaTCnsXV",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-02.gc-us-central1-a.status.prod",
Address: "/dns4/node-02.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmDQugwDHM3YeUp86iGjrUvbdw3JPRgikC7YoGBsT2ymMg",
Fleet: params.FleetStatusProd,
Version: 2,
},
Mailserver{
ID: "node-01.ac-cn-hongkong-c.status.test",
Address: "/dns4/node-01.ac-cn-hongkong-c.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAm2BjXxCp1sYFJQKpLLbPbwd5juxbsYofu3TsS3auvT9Yi",
Fleet: params.FleetStatusTest,
Version: 2,
},
Mailserver{
ID: "node-01.do-ams3.status.test",
Address: "/dns4/node-01.do-ams3.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAkukebeXjTQ9QDBeNDWuGfbaSg79wkkhK4vPocLgR6QFDf",
Fleet: params.FleetStatusTest,
Version: 2,
},
Mailserver{
ID: "node-01.gc-us-central1-a.status.test",
Address: "/dns4/node-01.gc-us-central1-a.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAmGDX3iAFox93PupVYaHa88kULGqMpJ7AEHGwj3jbMtt76",
Fleet: params.FleetStatusTest,
Version: 2,
},
Mailserver{
ID: "store-01.do-ams3.shards.test",
Address: "/dns4/store-01.do-ams3.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT",
Fleet: params.FleetShardsTest,
Version: 2,
},
Mailserver{
ID: "store-02.do-ams3.shards.test",
Address: "/dns4/store-02.do-ams3.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAm9aDJPkhGxc2SFcEACTFdZ91Q5TJjp76qZEhq9iF59x7R",
Fleet: params.FleetShardsTest,
Version: 2,
},
Mailserver{
ID: "store-01.gc-us-central1-a.shards.test",
Address: "/dns4/store-01.gc-us-central1-a.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAmMELCo218hncCtTvC2Dwbej3rbyHQcR8erXNnKGei7WPZ",
Fleet: params.FleetShardsTest,
Version: 2,
},
Mailserver{
ID: "store-02.gc-us-central1-a.shards.test",
Address: "/dns4/store-02.gc-us-central1-a.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJnVR7ZzFaYvciPVafUXuYGLHPzSUigqAmeNw9nJUVGeM",
Fleet: params.FleetShardsTest,
Version: 2,
},
Mailserver{
ID: "store-01.ac-cn-hongkong-c.shards.test",
Address: "/dns4/store-01.ac-cn-hongkong-c.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAm2M7xs7cLPc3jamawkEqbr7cUJX11uvY7LxQ6WFUdUKUT",
Fleet: params.FleetShardsTest,
Version: 2,
},
Mailserver{
ID: "store-02.ac-cn-hongkong-c.shards.test",
Address: "/dns4/store-02.ac-cn-hongkong-c.shards.test.statusim.net/tcp/30303/p2p/16Uiu2HAm9CQhsuwPR54q27kNj9iaQVfyRzTGKrhFmr94oD8ujU6P",
Fleet: params.FleetShardsTest,
Version: 2,
},
}
}

View File

@@ -0,0 +1,36 @@
package mailservers
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
func NewService(db *Database) *Service {
return &Service{db: db}
}
type Service struct {
db *Database
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "mailservers",
Version: "0.1.0",
Service: NewAPI(s.db),
},
}
}
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,152 @@
package mailservers
import (
"context"
"fmt"
"net"
"time"
multiaddr "github.com/multiformats/go-multiaddr"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/status-im/status-go/rtt"
)
type PingQuery struct {
Addresses []string `json:"addresses"`
TimeoutMs int `json:"timeoutMs"`
}
type PingResult struct {
Address string `json:"address"`
RTTMs *int `json:"rttMs"`
Err *string `json:"error"`
}
type parseFn func(string) (string, error)
func (pr *PingResult) Update(rttMs int, err error) {
if err != nil {
errStr := err.Error()
pr.Err = &errStr
}
if rttMs >= 0 {
pr.RTTMs = &rttMs
} else {
pr.RTTMs = nil
}
}
func EnodeToAddr(node *enode.Node) (string, error) {
var ip4 enr.IPv4
err := node.Load(&ip4)
if err != nil {
return "", err
}
var tcp enr.TCP
err = node.Load(&tcp)
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%d", net.IP(ip4).String(), tcp), nil
}
func EnodeStringToAddr(enodeAddr string) (string, error) {
node, err := enode.ParseV4(enodeAddr)
if err != nil {
return "", err
}
return EnodeToAddr(node)
}
func parse(addresses []string, fn parseFn) (map[string]*PingResult, []string) {
results := make(map[string]*PingResult, len(addresses))
var toPing []string
for i := range addresses {
addr, err := fn(addresses[i])
if err != nil {
errStr := err.Error()
results[addresses[i]] = &PingResult{Address: addresses[i], Err: &errStr}
continue
}
results[addr] = &PingResult{Address: addresses[i]}
toPing = append(toPing, addr)
}
return results, toPing
}
func mapValues(m map[string]*PingResult) []*PingResult {
rval := make([]*PingResult, 0, len(m))
for _, value := range m {
rval = append(rval, value)
}
return rval
}
func DoPing(ctx context.Context, addresses []string, timeoutMs int, p parseFn) ([]*PingResult, error) {
timeout := time.Duration(timeoutMs) * time.Millisecond
resultsMap, toPing := parse(addresses, p)
// run the checks concurrently
results, err := rtt.CheckHosts(toPing, timeout)
if err != nil {
return nil, err
}
// set ping results
for i := range results {
r := results[i]
pr := resultsMap[r.Addr]
if pr == nil {
continue
}
pr.Update(r.RTTMs, r.Err)
}
return mapValues(resultsMap), nil
}
func (a *API) Ping(ctx context.Context, pq PingQuery) ([]*PingResult, error) {
return DoPing(ctx, pq.Addresses, pq.TimeoutMs, EnodeStringToAddr)
}
func MultiAddressToAddress(multiAddr string) (string, error) {
ma, err := multiaddr.NewMultiaddr(multiAddr)
if err != nil {
return "", err
}
dns4, err := ma.ValueForProtocol(multiaddr.P_DNS4)
if err != nil && err != multiaddr.ErrProtocolNotFound {
return "", err
}
ip4 := ""
if dns4 != "" {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ip4, err = net.DefaultResolver.LookupCNAME(ctx, dns4)
if err != nil {
return "", err
}
} else {
ip4, err = ma.ValueForProtocol(multiaddr.P_IP4)
if err != nil {
return "", err
}
}
tcp, err := ma.ValueForProtocol(multiaddr.P_TCP)
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%s", ip4, tcp), nil
}
func (a *API) MultiAddressPing(ctx context.Context, pq PingQuery) ([]*PingResult, error) {
return DoPing(ctx, pq.Addresses, pq.TimeoutMs, MultiAddressToAddress)
}

View File

@@ -0,0 +1,48 @@
package peer
import (
"context"
"errors"
)
var (
// ErrInvalidTopic error returned when the requested topic is not valid.
ErrInvalidTopic = errors.New("topic not valid")
// ErrInvalidRange error returned when max-min range is not valid.
ErrInvalidRange = errors.New("invalid range, Min should be lower or equal to Max")
// ErrDiscovererNotProvided error when discoverer is not being provided.
ErrDiscovererNotProvided = errors.New("discoverer not provided")
)
// PublicAPI represents a set of APIs from the `web3.peer` namespace.
type PublicAPI struct {
s *Service
}
// NewAPI creates an instance of the peer API.
func NewAPI(s *Service) *PublicAPI {
return &PublicAPI{s: s}
}
// DiscoverRequest json request for peer_discover.
type DiscoverRequest struct {
Topic string `json:"topic"`
Max int `json:"max"`
Min int `json:"min"`
}
// Discover is an implementation of `peer_discover` or `web3.peer.discover` API.
func (api *PublicAPI) Discover(context context.Context, req DiscoverRequest) (err error) {
if api.s.d == nil {
return ErrDiscovererNotProvided
}
if len(req.Topic) == 0 {
return ErrInvalidTopic
}
if req.Max < req.Min {
return ErrInvalidRange
}
return api.s.d.Discover(req.Topic, req.Max, req.Min)
}

View File

@@ -0,0 +1,48 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: services/peer/service.go
// Package peer is a generated GoMock package.
package peer
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockDiscoverer is a mock of Discoverer interface
type MockDiscoverer struct {
ctrl *gomock.Controller
recorder *MockDiscovererMockRecorder
}
// MockDiscovererMockRecorder is the mock recorder for MockDiscoverer
type MockDiscovererMockRecorder struct {
mock *MockDiscoverer
}
// NewMockDiscoverer creates a new mock instance
func NewMockDiscoverer(ctrl *gomock.Controller) *MockDiscoverer {
mock := &MockDiscoverer{ctrl: ctrl}
mock.recorder = &MockDiscovererMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDiscoverer) EXPECT() *MockDiscovererMockRecorder {
return m.recorder
}
// Discover mocks base method
func (m *MockDiscoverer) Discover(topic string, max, min int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discover", topic, max, min)
ret0, _ := ret[0].(error)
return ret0
}
// Discover indicates an expected call of Discover
func (mr *MockDiscovererMockRecorder) Discover(topic, max, min interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discover", reflect.TypeOf((*MockDiscoverer)(nil).Discover), topic, max, min)
}

View File

@@ -0,0 +1,59 @@
package peer
import (
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Make sure that Service implements node.Lifecycle interface.
var _ node.Lifecycle = (*Service)(nil)
// Discoverer manages peer discovery.
type Discoverer interface {
Discover(topic string, max, min int) error
}
// Service it manages all endpoints for peer operations.
type Service struct {
d Discoverer
}
// New returns a new Service.
func New() *Service {
return &Service{}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "peer",
Version: "1.0",
Service: NewAPI(s),
Public: false,
},
}
}
// SetDiscoverer sets discoverer for the API calls.
func (s *Service) SetDiscoverer(d Discoverer) {
s.d = d
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Stop() error {
return nil
}

View File

@@ -0,0 +1,38 @@
Dapps permissions service
=========================
To enable:
```json
{
"PermissionsConfig": {
"Enabled": true,
},
APIModules: "permissions"
}
```
API
---
#### permissions_addDappPermissions
Stores provided permissions for dapp. On update replaces previous version of the object.
```json
{
"dapp": "first",
"permissions": [
"r",
"x"
]
}
```
#### permissions_getDappPermissions
Returns all permissions for dapps. Order is not deterministic.
#### permissions_deleteDappPermissions
Delete dapp by a name.

View File

@@ -0,0 +1,30 @@
package permissions
import (
"context"
)
func NewAPI(db *Database) *API {
return &API{db}
}
// API is class with methods available over RPC.
type API struct {
db *Database
}
func (api *API) AddDappPermissions(ctx context.Context, perms DappPermissions) error {
return api.db.AddPermissions(perms)
}
func (api *API) GetDappPermissions(ctx context.Context) ([]DappPermissions, error) {
return api.db.GetPermissions()
}
func (api *API) DeleteDappPermissions(ctx context.Context, name string) error {
return api.db.DeletePermission(name, "")
}
func (api *API) DeleteDappPermissionsByNameAndAddress(ctx context.Context, name string, address string) error {
return api.db.DeletePermission(name, address)
}

View File

@@ -0,0 +1,168 @@
package permissions
import (
"database/sql"
)
// Database sql wrapper for operations with browser objects.
type Database struct {
db *sql.DB
}
// Close closes database.
func (db Database) Close() error {
return db.db.Close()
}
func NewDB(db *sql.DB) *Database {
return &Database{db: db}
}
type DappPermissions struct {
ID int
Name string `json:"dapp"`
Permissions []string `json:"permissions,omitempty"`
Address string `json:"address,omitempty"`
}
func (db *Database) AddPermissions(perms DappPermissions) (err error) {
tx, err := db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
dRows, err := tx.Query("SELECT id FROM dapps where name = ? AND address = ?", perms.Name, perms.Address)
if err != nil {
return
}
defer dRows.Close()
var id int64
if dRows.Next() {
err = dRows.Scan(&id)
if err != nil {
return
}
} else {
dInsert, err := tx.Prepare("INSERT INTO dapps(name, address) VALUES(?, ?)")
if err != nil {
return err
}
res, err := dInsert.Exec(perms.Name, perms.Address)
dInsert.Close()
if err != nil {
return err
}
id, err = res.LastInsertId()
if err != nil {
return err
}
}
pDelete, err := tx.Prepare("DELETE FROM permissions WHERE dapp_id = ?")
if err != nil {
return
}
defer pDelete.Close()
_, err = pDelete.Exec(id)
if err != nil {
return
}
if len(perms.Permissions) == 0 {
return
}
pInsert, err := tx.Prepare("INSERT INTO permissions(dapp_id, permission) VALUES(?, ?)")
if err != nil {
return
}
defer pInsert.Close()
for _, perm := range perms.Permissions {
_, err = pInsert.Exec(id, perm)
if err != nil {
return
}
}
return
}
func (db *Database) GetPermissions() (rst []DappPermissions, err error) {
tx, err := db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
// FULL and RIGHT joins are not supported
dRows, err := tx.Query("SELECT id, name, address FROM dapps")
if err != nil {
return
}
defer dRows.Close()
dapps := map[int]*DappPermissions{}
for dRows.Next() {
perms := DappPermissions{}
err = dRows.Scan(&perms.ID, &perms.Name, &perms.Address)
if err != nil {
return nil, err
}
dapps[perms.ID] = &perms
}
pRows, err := tx.Query("SELECT dapp_id, permission from permissions")
if err != nil {
return
}
defer pRows.Close()
var (
id int
permission string
)
for pRows.Next() {
err = pRows.Scan(&id, &permission)
if err != nil {
return
}
dapps[id].Permissions = append(dapps[id].Permissions, permission)
}
rst = make([]DappPermissions, 0, len(dapps))
for key := range dapps {
rst = append(rst, *dapps[key])
}
return rst, nil
}
func (db *Database) DeletePermission(name string, address string) error {
_, err := db.db.Exec("DELETE FROM dapps WHERE name = ? AND address = ?", name, address)
return err
}
func (db *Database) HasPermission(dappName string, address string, permission string) (bool, error) {
var id int64
err := db.db.QueryRow("SELECT id FROM dapps where name = ? AND address = ?", dappName, address).Scan(&id)
if err != nil {
return false, nil
}
var count uint64
err = db.db.QueryRow(
`SELECT COUNT(1) FROM permissions WHERE dapp_id = ? AND permission = ?`,
id, permission,
).Scan(&count)
return count > 0, err
}

View File

@@ -0,0 +1,41 @@
package permissions
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// NewService initializes service instance.
func NewService(db *Database) *Service {
return &Service{db: db}
}
type Service struct {
db *Database
}
// Start a service.
func (s *Service) Start() error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "permissions",
Version: "0.1.0",
Service: NewAPI(s.db),
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,7 @@
# personal
This package contains Status integraton with `personal_*` RPC APIs more
information on these APIs can be found on the Ethereum Wiki:
https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal
In `web3.js` these methods are located in `web3.personal` namespace.

View File

@@ -0,0 +1,91 @@
package personal
import (
"context"
"errors"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
)
var (
// ErrInvalidPersonalSignAccount is returned when the account passed to
// personal_sign isn't equal to the currently selected account.
ErrInvalidPersonalSignAccount = errors.New("invalid account as only the selected one can generate a signature")
)
// SignParams required to sign messages
type SignParams struct {
Data interface{} `json:"data"`
Address string `json:"account"`
Password string `json:"password"`
}
// RecoverParams are for calling `personal_ecRecover`
type RecoverParams struct {
Message string `json:"message"`
Signature string `json:"signature"`
}
// PublicAPI represents a set of APIs from the `web3.personal` namespace.
type PublicAPI struct {
rpcClient *rpc.Client
rpcTimeout time.Duration
}
// NewAPI creates an instance of the personal API.
func NewAPI() *PublicAPI {
return &PublicAPI{
rpcTimeout: 300 * time.Second,
}
}
// SetRPC sets RPC params (client and timeout) for the API calls.
func (api *PublicAPI) SetRPC(rpcClient *rpc.Client, timeout time.Duration) {
api.rpcClient = rpcClient
api.rpcTimeout = timeout
}
// Recover is an implementation of `personal_ecRecover` or `web3.personal.ecRecover` API
func (api *PublicAPI) Recover(rpcParams RecoverParams) (addr types.Address, err error) {
ctx, cancel := context.WithTimeout(context.Background(), api.rpcTimeout)
defer cancel()
var gethAddr common.Address
err = api.rpcClient.CallContextIgnoringLocalHandlers(
ctx,
&gethAddr,
api.rpcClient.UpstreamChainID,
params.PersonalRecoverMethodName,
rpcParams.Message, rpcParams.Signature)
addr = types.Address(gethAddr)
return
}
// Sign is an implementation of `personal_sign` or `web3.personal.sign` API
func (api *PublicAPI) Sign(rpcParams SignParams, verifiedAccount *account.SelectedExtKey) (result types.HexBytes, err error) {
if !strings.EqualFold(rpcParams.Address, verifiedAccount.Address.Hex()) {
err = ErrInvalidPersonalSignAccount
return
}
ctx, cancel := context.WithTimeout(context.Background(), api.rpcTimeout)
defer cancel()
var gethResult hexutil.Bytes
err = api.rpcClient.CallContextIgnoringLocalHandlers(
ctx,
&gethResult,
api.rpcClient.UpstreamChainID,
params.PersonalSignMethodName,
rpcParams.Data, rpcParams.Address, rpcParams.Password)
result = types.HexBytes(gethResult)
return
}

View File

@@ -0,0 +1,51 @@
package personal
import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/ethapi"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Make sure that Service implements node.Service interface.
var _ node.Lifecycle = (*Service)(nil)
// Service represents out own implementation of personal sign operations.
type Service struct {
am *accounts.Manager
}
// New returns a new Service.
func New(am *accounts.Manager) *Service {
return &Service{am}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "personal",
Version: "1.0",
Service: ethapi.NewLimitedPersonalAPI(s.am),
Public: false,
},
}
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Stop() error {
return nil
}

View File

@@ -0,0 +1,257 @@
package rpcfilters
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/pborman/uuid"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/log"
getrpc "github.com/ethereum/go-ethereum/rpc"
)
const (
defaultFilterLivenessPeriod = 5 * time.Minute
defaultLogsPeriod = 3 * time.Second
defaultLogsQueryTimeout = 10 * time.Second
)
var (
errFilterNotFound = errors.New("filter not found")
)
type filter interface {
add(interface{}) error
pop() interface{}
stop()
deadline() *time.Timer
}
type ChainEvent interface {
Start() error
Stop()
Subscribe() (id int, ch interface{})
Unsubscribe(id int)
}
// PublicAPI represents filter API that is exported to `eth` namespace
type PublicAPI struct {
filtersMu sync.Mutex
filters map[getrpc.ID]filter
// filterLivenessLoop defines how often timeout loop is executed
filterLivenessLoop time.Duration
// filter liveness increased by this period when changes are requested
filterLivenessPeriod time.Duration
client func() ContextCaller
chainID func() uint64
latestBlockChangedEvent *latestBlockChangedEvent
transactionSentToUpstreamEvent *transactionSentToUpstreamEvent
}
// NewPublicAPI returns a reference to the PublicAPI object
func NewPublicAPI(s *Service) *PublicAPI {
api := &PublicAPI{
filters: make(map[getrpc.ID]filter),
latestBlockChangedEvent: s.latestBlockChangedEvent,
transactionSentToUpstreamEvent: s.transactionSentToUpstreamEvent,
client: func() ContextCaller { return s.rpc.RPCClient() },
chainID: func() uint64 { return s.rpc.RPCClient().UpstreamChainID },
filterLivenessLoop: defaultFilterLivenessPeriod,
filterLivenessPeriod: defaultFilterLivenessPeriod + 10*time.Second,
}
go api.timeoutLoop(s.quit)
return api
}
func (api *PublicAPI) timeoutLoop(quit chan struct{}) {
for {
select {
case <-quit:
return
case <-time.After(api.filterLivenessLoop):
api.filtersMu.Lock()
for id, f := range api.filters {
deadline := f.deadline()
if deadline == nil {
continue
}
select {
case <-deadline.C:
delete(api.filters, id)
f.stop()
default:
continue
}
}
api.filtersMu.Unlock()
}
}
}
func (api *PublicAPI) NewFilter(crit filters.FilterCriteria) (getrpc.ID, error) {
id := getrpc.ID(uuid.New())
ctx, cancel := context.WithCancel(context.Background())
f := &logsFilter{
id: id,
crit: ethereum.FilterQuery(crit),
originalCrit: ethereum.FilterQuery(crit),
done: make(chan struct{}),
timer: time.NewTimer(api.filterLivenessPeriod),
ctx: ctx,
cancel: cancel,
logsCache: newCache(defaultCacheSize),
}
api.filtersMu.Lock()
api.filters[id] = f
api.filtersMu.Unlock()
go pollLogs(api.client(), api.chainID(), f, defaultLogsQueryTimeout, defaultLogsPeriod)
return id, nil
}
// NewBlockFilter is an implemenation of `eth_newBlockFilter` API
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter
func (api *PublicAPI) NewBlockFilter() getrpc.ID {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
f := newHashFilter()
id := getrpc.ID(uuid.New())
api.filters[id] = f
go func() {
id, si := api.latestBlockChangedEvent.Subscribe()
s, ok := si.(chan common.Hash)
if !ok {
panic("latestBlockChangedEvent returned wrong type")
}
defer api.latestBlockChangedEvent.Unsubscribe(id)
for {
select {
case hash := <-s:
if err := f.add(hash); err != nil {
log.Error("error adding value to filter", "hash", hash, "error", err)
}
case <-f.done:
return
}
}
}()
return id
}
// NewPendingTransactionFilter is an implementation of `eth_newPendingTransactionFilter` API
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newpendingtransactionfilter
func (api *PublicAPI) NewPendingTransactionFilter() getrpc.ID {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
f := newHashFilter()
id := getrpc.ID(uuid.New())
api.filters[id] = f
go func() {
id, si := api.transactionSentToUpstreamEvent.Subscribe()
s, ok := si.(chan *PendingTxInfo)
if !ok {
panic("transactionSentToUpstreamEvent returned wrong type")
}
defer api.transactionSentToUpstreamEvent.Unsubscribe(id)
for {
select {
case hash := <-s:
if err := f.add(hash); err != nil {
log.Error("error adding value to filter", "hash", hash, "error", err)
}
case <-f.done:
return
}
}
}()
return id
}
// UninstallFilter is an implemenation of `eth_uninstallFilter` API
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter
func (api *PublicAPI) UninstallFilter(id getrpc.ID) bool {
api.filtersMu.Lock()
f, found := api.filters[id]
if found {
delete(api.filters, id)
}
api.filtersMu.Unlock()
if found {
f.stop()
}
return found
}
// GetFilterLogs returns the logs for the filter with the given id.
// If the filter could not be found an empty array of logs is returned.
//
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs
func (api *PublicAPI) GetFilterLogs(ctx context.Context, id getrpc.ID) ([]types.Log, error) {
api.filtersMu.Lock()
f, exist := api.filters[id]
api.filtersMu.Unlock()
if !exist {
return []types.Log{}, errFilterNotFound
}
logs, ok := f.(*logsFilter)
if !ok {
return []types.Log{}, fmt.Errorf("filter with ID %v is not of logs type", id)
}
ctx, cancel := context.WithTimeout(ctx, defaultLogsQueryTimeout)
defer cancel()
rst, err := getLogs(ctx, api.client(), api.chainID(), logs.originalCrit)
return rst, err
}
// GetFilterChanges returns the hashes for the filter with the given id since
// last time it was called. This can be used for polling.
//
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges
func (api *PublicAPI) GetFilterChanges(id getrpc.ID) (interface{}, error) {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
if f, found := api.filters[id]; found {
deadline := f.deadline()
if deadline != nil {
if !deadline.Stop() {
// timer expired but filter is not yet removed in timeout loop
// receive timer value and reset timer
// see https://golang.org/pkg/time/#Timer.Reset
<-deadline.C
}
deadline.Reset(api.filterLivenessPeriod)
}
rst := f.pop()
if rst == nil {
return []interface{}{}, nil
}
return rst, nil
}
return []interface{}{}, errFilterNotFound
}

View File

@@ -0,0 +1,56 @@
package rpcfilters
import (
"errors"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
)
type hashFilter struct {
hashes []common.Hash
mu sync.Mutex
done chan struct{}
timer *time.Timer
}
// add adds a hash to the hashFilter
func (f *hashFilter) add(data interface{}) error {
hash, ok := data.(common.Hash)
if !ok {
return errors.New("provided data is not a common.Hash")
}
f.mu.Lock()
defer f.mu.Unlock()
f.hashes = append(f.hashes, hash)
return nil
}
// pop returns all the hashes stored in the hashFilter and clears the hashFilter contents
func (f *hashFilter) pop() interface{} {
f.mu.Lock()
defer f.mu.Unlock()
hashes := f.hashes
f.hashes = nil
return hashes
}
func (f *hashFilter) stop() {
select {
case <-f.done:
return
default:
close(f.done)
}
}
func (f *hashFilter) deadline() *time.Timer {
return f.timer
}
func newHashFilter() *hashFilter {
return &hashFilter{
done: make(chan struct{}),
}
}

View File

@@ -0,0 +1,164 @@
package rpcfilters
import (
"errors"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
const (
defaultTickerPeriod = 3 * time.Second
defaultReportHistorySize = 20
)
// ringArray represents a thread-safe capped collection of hashes.
type ringArray struct {
mu sync.Mutex
maxCount int
currentIndex int
blocks []common.Hash
}
func newRingArray(maxCount int) *ringArray {
return &ringArray{
maxCount: maxCount,
blocks: make([]common.Hash, maxCount),
}
}
// TryAddUnique adds a hash to the array if the array doesn't have it.
// Returns true if the element was added.
func (r *ringArray) TryAddUnique(hash common.Hash) bool {
r.mu.Lock()
defer r.mu.Unlock()
if r.has(hash) {
return false
}
r.blocks[r.currentIndex] = hash
r.currentIndex++
if r.currentIndex >= len(r.blocks) {
r.currentIndex = 0
}
return true
}
// has returns `true` if the hash is in the array.
// It has linear complexity but on short arrays it isn't worth optimizing.
func (r *ringArray) has(hash common.Hash) bool {
for _, h := range r.blocks {
if h == hash {
return true
}
}
return false
}
// latestBlockChangedEvent represents an event that one can subscribe to
type latestBlockChangedEvent struct {
sxMu sync.Mutex
sx map[int]chan common.Hash
reportedBlocks *ringArray
provider latestBlockProvider
quit chan struct{}
tickerPeriod time.Duration
}
func (e *latestBlockChangedEvent) Start() error {
if e.quit != nil {
return errors.New("latest block changed event is already started")
}
e.quit = make(chan struct{})
go func() {
ticker := time.NewTicker(e.tickerPeriod)
for {
select {
case <-ticker.C:
if e.numberOfSubscriptions() == 0 {
continue
}
latestBlock, err := e.provider.GetLatestBlock()
if err != nil {
log.Error("error while receiving latest block", "error", err)
continue
}
e.processLatestBlock(latestBlock)
case <-e.quit:
return
}
}
}()
return nil
}
func (e *latestBlockChangedEvent) numberOfSubscriptions() int {
e.sxMu.Lock()
defer e.sxMu.Unlock()
return len(e.sx)
}
func (e *latestBlockChangedEvent) processLatestBlock(latestBlock blockInfo) {
// if we received the hash we already received before, don't add it
if !e.reportedBlocks.TryAddUnique(latestBlock.Hash) {
return
}
e.sxMu.Lock()
defer e.sxMu.Unlock()
for _, channel := range e.sx {
channel <- latestBlock.Hash
}
}
func (e *latestBlockChangedEvent) Stop() {
if e.quit == nil {
return
}
select {
case <-e.quit:
e.quit = nil
return
default:
close(e.quit)
}
e.quit = nil
}
func (e *latestBlockChangedEvent) Subscribe() (int, interface{}) {
e.sxMu.Lock()
defer e.sxMu.Unlock()
channel := make(chan common.Hash)
id := len(e.sx)
e.sx[id] = channel
return id, channel
}
func (e *latestBlockChangedEvent) Unsubscribe(id int) {
e.sxMu.Lock()
defer e.sxMu.Unlock()
delete(e.sx, id)
}
func newLatestBlockChangedEvent(provider latestBlockProvider) *latestBlockChangedEvent {
return &latestBlockChangedEvent{
sx: make(map[int]chan common.Hash),
provider: provider,
reportedBlocks: newRingArray(defaultReportHistorySize),
tickerPeriod: defaultTickerPeriod,
}
}

View File

@@ -0,0 +1,58 @@
package rpcfilters
import (
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/rpc"
)
type rpcProvider interface {
RPCClient() *rpc.Client
}
// blockInfo contains the hash and the number of the latest block
type blockInfo struct {
Hash common.Hash `json:"hash"`
NumberBytes hexutil.Bytes `json:"number"`
}
// Number returns a big.Int representation of the encoded block number.
func (i blockInfo) Number() *big.Int {
number := big.NewInt(0)
number.SetBytes(i.NumberBytes)
return number
}
// latestBlockProvider provides the latest block info from the blockchain
type latestBlockProvider interface {
GetLatestBlock() (blockInfo, error)
}
// latestBlockProviderRPC is an implementation of latestBlockProvider interface
// that requests a block using an RPC client provided
type latestBlockProviderRPC struct {
rpc rpcProvider
}
// GetLatestBlock returns the block info
func (p *latestBlockProviderRPC) GetLatestBlock() (blockInfo, error) {
rpcClient := p.rpc.RPCClient()
if rpcClient == nil {
return blockInfo{}, errors.New("no active RPC client: is the node running?")
}
var result blockInfo
err := rpcClient.Call(&result, rpcClient.UpstreamChainID, "eth_getBlockByNumber", "latest", false)
if err != nil {
return blockInfo{}, err
}
return result, nil
}

View File

@@ -0,0 +1,70 @@
package rpcfilters
import (
"context"
"math/big"
"time"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
getRpc "github.com/ethereum/go-ethereum/rpc"
)
// ContextCaller provides CallContext method as ethereums rpc.Client.
type ContextCaller interface {
CallContext(ctx context.Context, result interface{}, chainID uint64, method string, args ...interface{}) error
}
func pollLogs(client ContextCaller, chainID uint64, f *logsFilter, timeout, period time.Duration) {
query := func() {
ctx, cancel := context.WithTimeout(f.ctx, timeout)
defer cancel()
logs, err := getLogs(ctx, client, chainID, f.criteria())
if err != nil {
log.Error("Error fetch logs", "criteria", f.crit, "error", err)
return
}
if err := f.add(logs); err != nil {
log.Error("Error adding logs", "logs", logs, "error", err)
}
}
query()
latest := time.NewTicker(period)
defer latest.Stop()
for {
select {
case <-latest.C:
query()
case <-f.done:
log.Debug("Filter was stopped", "ID", f.id, "crit", f.crit)
return
}
}
}
func getLogs(ctx context.Context, client ContextCaller, chainID uint64, crit ethereum.FilterQuery) (rst []types.Log, err error) {
return rst, client.CallContext(ctx, &rst, chainID, "eth_getLogs", toFilterArg(crit))
}
func toFilterArg(q ethereum.FilterQuery) interface{} {
arg := map[string]interface{}{
"fromBlock": toBlockNumArg(q.FromBlock),
"toBlock": toBlockNumArg(q.ToBlock),
"address": q.Addresses,
"topics": q.Topics,
}
if q.FromBlock == nil {
arg["fromBlock"] = "0x0"
}
return arg
}
func toBlockNumArg(number *big.Int) string {
if number == nil || number.Int64() == getRpc.LatestBlockNumber.Int64() {
return "latest"
} else if number.Int64() == getRpc.PendingBlockNumber.Int64() {
return "pending"
}
return hexutil.EncodeBig(number)
}

View File

@@ -0,0 +1,148 @@
package rpcfilters
import (
"fmt"
"sort"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
const (
defaultCacheSize = 20
)
type cacheRecord struct {
block uint64
hash common.Hash
logs []types.Log
}
func newCache(size int) *cache {
return &cache{
records: make([]cacheRecord, 0, size),
size: size,
}
}
type cache struct {
mu sync.RWMutex
size int // length of the records
records []cacheRecord
}
// add inserts logs into cache and returns added and replaced logs.
// replaced logs with will be returned with Removed=true.
func (c *cache) add(logs []types.Log) (added, replaced []types.Log, err error) {
if len(logs) == 0 {
return nil, nil, nil
}
aggregated := aggregateLogs(logs, c.size) // size doesn't change
if len(aggregated) == 0 {
return nil, nil, nil
}
if err := checkLogsAreInOrder(aggregated); err != nil {
return nil, nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
// find common block. e.g. [3,4] and [1,2,3,4] = 3
last := 0
if len(c.records) > 0 {
last = len(c.records) - 1
for aggregated[0].block < c.records[last].block && last > 0 {
last--
}
}
c.records, added, replaced = merge(last, c.records, aggregated)
if lth := len(c.records); lth > c.size {
copy(c.records, c.records[lth-c.size:])
}
return added, replaced, nil
}
func (c *cache) earliestBlockNum() uint64 {
if len(c.records) == 0 {
return 0
}
return c.records[0].block
}
func checkLogsAreInOrder(records []cacheRecord) error {
for prev, i := 0, 1; i < len(records); i++ {
if records[prev].block == records[i].block-1 {
prev = i
} else {
return fmt.Errorf(
"logs must be delivered straight in order. gaps between blocks '%d' and '%d'",
records[prev].block, records[i].block,
)
}
}
return nil
}
// merge merges received records into old slice starting at provided position, example:
// [1, 2, 3]
//
// [2, 3, 4]
//
// [1, 2, 3, 4]
// if hash doesn't match previously received hash - such block was removed due to reorg
// logs that were a part of that block will be returned with Removed set to true
func merge(last int, old, received []cacheRecord) ([]cacheRecord, []types.Log, []types.Log) {
var (
added, replaced []types.Log
block uint64
hash common.Hash
)
for i := range received {
record := received[i]
if last < len(old) {
block = old[last].block
hash = old[last].hash
}
if record.block > block {
// simply add new records
added = append(added, record.logs...)
old = append(old, record)
} else if record.hash != hash && record.block == block {
// record hash is not equal to previous record hash at the same height
// replace record in hash and add logs as replaced
replaced = append(replaced, old[last].logs...)
added = append(added, record.logs...)
old[last] = record
}
last++
}
return old, added, replaced
}
// aggregateLogs creates at most requested amount of cacheRecords from provided logs.
// cacheRecords will be sorted in ascending order, starting from lowest block to highest.
func aggregateLogs(logs []types.Log, limit int) []cacheRecord {
// sort in reverse order, so that iteration will start from latest blocks
sort.Slice(logs, func(i, j int) bool {
return logs[i].BlockNumber > logs[j].BlockNumber
})
rst := make([]cacheRecord, limit)
pos, start := len(rst)-1, 0
var hash common.Hash
for i := range logs {
log := logs[i]
if (hash != common.Hash{}) && hash != log.BlockHash {
rst[pos].logs = logs[start:i]
start = i
if pos-1 < 0 {
break
}
pos--
}
rst[pos].logs = logs[start:]
rst[pos].block = log.BlockNumber
rst[pos].hash = log.BlockHash
hash = log.BlockHash
}
return rst[pos:]
}

View File

@@ -0,0 +1,161 @@
package rpcfilters
import (
"context"
"fmt"
"math/big"
"sync"
"time"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
)
type logsFilter struct {
mu sync.RWMutex
logs []types.Log
crit ethereum.FilterQuery // will be modified and different from original
originalCrit ethereum.FilterQuery // not modified version of the criteria
logsCache *cache
id rpc.ID
timer *time.Timer
ctx context.Context
cancel context.CancelFunc
done chan struct{}
}
func (f *logsFilter) criteria() ethereum.FilterQuery {
f.mu.RLock()
defer f.mu.RUnlock()
return f.crit
}
func (f *logsFilter) add(data interface{}) error {
logs, ok := data.([]types.Log)
if !ok {
return fmt.Errorf("can't cast %v to types.Log", data)
}
filtered := filterLogs(logs, f.crit)
if len(filtered) > 0 {
f.mu.Lock()
defer f.mu.Unlock()
added, replaced, err := f.logsCache.add(filtered)
if err != nil {
return err
}
for _, log := range replaced {
log.Removed = true
f.logs = append(f.logs, log)
}
if len(added) > 0 {
f.logs = append(f.logs, added...)
}
// if there was no replaced logs - keep polling only latest logs
if len(replaced) == 0 {
adjustFromBlock(&f.crit)
} else {
// otherwise poll earliest known block in cache
earliest := f.logsCache.earliestBlockNum()
if earliest != 0 {
f.crit.FromBlock = new(big.Int).SetUint64(earliest)
}
}
}
return nil
}
func (f *logsFilter) pop() interface{} {
f.mu.Lock()
defer f.mu.Unlock()
rst := f.logs
f.logs = nil
return rst
}
func (f *logsFilter) stop() {
select {
case <-f.done:
return
default:
close(f.done)
if f.cancel != nil {
f.cancel()
}
}
}
func (f *logsFilter) deadline() *time.Timer {
return f.timer
}
// adjustFromBlock adjusts crit.FromBlock to latest to avoid querying same logs.
func adjustFromBlock(crit *ethereum.FilterQuery) {
latest := big.NewInt(rpc.LatestBlockNumber.Int64())
// don't adjust if filter is not interested in newer blocks
if crit.ToBlock != nil && crit.ToBlock.Cmp(latest) == 1 {
return
}
// don't adjust if from block is already pending
if crit.FromBlock != nil && crit.FromBlock.Cmp(latest) == -1 {
return
}
crit.FromBlock = latest
}
func includes(addresses []common.Address, a common.Address) bool {
for _, addr := range addresses {
if addr == a {
return true
}
}
return false
}
// filterLogs creates a slice of logs matching the given criteria.
func filterLogs(logs []types.Log, crit ethereum.FilterQuery) (
ret []types.Log) {
for _, log := range logs {
if matchLog(log, crit) {
ret = append(ret, log)
}
}
return
}
func matchLog(log types.Log, crit ethereum.FilterQuery) bool {
if crit.FromBlock != nil && crit.FromBlock.Int64() >= 0 && crit.FromBlock.Uint64() > log.BlockNumber {
return false
}
if crit.ToBlock != nil && crit.ToBlock.Int64() >= 0 && crit.ToBlock.Uint64() < log.BlockNumber {
return false
}
if len(crit.Addresses) > 0 && !includes(crit.Addresses, log.Address) {
return false
}
if len(crit.Topics) > len(log.Topics) {
return false
}
return matchTopics(log, crit.Topics)
}
func matchTopics(log types.Log, topics [][]common.Hash) bool {
for i, sub := range topics {
match := len(sub) == 0 // empty rule set == wildcard
for _, topic := range sub {
if log.Topics[i] == topic {
match = true
break
}
}
if !match {
return false
}
}
return true
}

View File

@@ -0,0 +1,75 @@
package rpcfilters
import (
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Make sure that Service implements node.Lifecycle interface.
var _ node.Lifecycle = (*Service)(nil)
// Service represents out own implementation of personal sign operations.
type Service struct {
latestBlockChangedEvent *latestBlockChangedEvent
transactionSentToUpstreamEvent *transactionSentToUpstreamEvent
rpc rpcProvider
quit chan struct{}
}
// New returns a new Service.
func New(rpc rpcProvider) *Service {
provider := &latestBlockProviderRPC{rpc}
latestBlockChangedEvent := newLatestBlockChangedEvent(provider)
transactionSentToUpstreamEvent := newTransactionSentToUpstreamEvent()
return &Service{
latestBlockChangedEvent: latestBlockChangedEvent,
transactionSentToUpstreamEvent: transactionSentToUpstreamEvent,
rpc: rpc,
}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "eth",
Version: "1.0",
Service: NewPublicAPI(s),
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
s.quit = make(chan struct{})
err := s.transactionSentToUpstreamEvent.Start()
if err != nil {
return err
}
return s.latestBlockChangedEvent.Start()
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
close(s.quit)
s.transactionSentToUpstreamEvent.Stop()
s.latestBlockChangedEvent.Stop()
return nil
}
func (s *Service) TransactionSentToUpstreamEvent() ChainEvent {
return s.transactionSentToUpstreamEvent
}
func (s *Service) TriggerTransactionSentToUpstreamEvent(txInfo *PendingTxInfo) {
s.transactionSentToUpstreamEvent.Trigger(txInfo)
}

View File

@@ -0,0 +1,112 @@
package rpcfilters
import (
"errors"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type PendingTxInfo struct {
Hash common.Hash
Type string
From common.Address
ChainID uint64
}
// transactionSentToUpstreamEvent represents an event that one can subscribe to
type transactionSentToUpstreamEvent struct {
sxMu sync.Mutex
sx map[int]chan *PendingTxInfo
listener chan *PendingTxInfo
quit chan struct{}
}
func newTransactionSentToUpstreamEvent() *transactionSentToUpstreamEvent {
return &transactionSentToUpstreamEvent{
sx: make(map[int]chan *PendingTxInfo),
listener: make(chan *PendingTxInfo),
}
}
func (e *transactionSentToUpstreamEvent) Start() error {
if e.quit != nil {
return errors.New("latest transaction sent to upstream event is already started")
}
e.quit = make(chan struct{})
go func() {
for {
select {
case transactionInfo := <-e.listener:
if e.numberOfSubscriptions() == 0 {
continue
}
e.processTransactionSentToUpstream(transactionInfo)
case <-e.quit:
return
}
}
}()
return nil
}
func (e *transactionSentToUpstreamEvent) numberOfSubscriptions() int {
e.sxMu.Lock()
defer e.sxMu.Unlock()
return len(e.sx)
}
func (e *transactionSentToUpstreamEvent) processTransactionSentToUpstream(transactionInfo *PendingTxInfo) {
e.sxMu.Lock()
defer e.sxMu.Unlock()
for id, channel := range e.sx {
select {
case channel <- transactionInfo:
default:
log.Error("dropping messages %s for subscriotion %d because the channel is full", transactionInfo, id)
}
}
}
func (e *transactionSentToUpstreamEvent) Stop() {
if e.quit == nil {
return
}
select {
case <-e.quit:
return
default:
close(e.quit)
}
e.quit = nil
}
func (e *transactionSentToUpstreamEvent) Subscribe() (int, interface{}) {
e.sxMu.Lock()
defer e.sxMu.Unlock()
channel := make(chan *PendingTxInfo, 512)
id := len(e.sx)
e.sx[id] = channel
return id, channel
}
func (e *transactionSentToUpstreamEvent) Unsubscribe(id int) {
e.sxMu.Lock()
defer e.sxMu.Unlock()
delete(e.sx, id)
}
// Trigger gets called in order to trigger the event
func (e *transactionSentToUpstreamEvent) Trigger(transactionInfo *PendingTxInfo) {
e.listener <- transactionInfo
}

View File

@@ -0,0 +1,34 @@
package rpcstats
import (
"context"
)
// PublicAPI represents a set of APIs from the namespace.
type PublicAPI struct {
s *Service
}
// NewAPI creates an instance of the API.
func NewAPI(s *Service) *PublicAPI {
return &PublicAPI{s: s}
}
// Reset resets RPC usage stats
func (api *PublicAPI) Reset(context context.Context) {
resetStats()
}
type RPCStats struct {
Total uint `json:"total"`
CounterPerMethod map[string]uint `json:"methods"`
}
// GetStats retrun RPC usage stats
func (api *PublicAPI) GetStats(context context.Context) (RPCStats, error) {
total, perMethod := getStats()
return RPCStats{
Total: total,
CounterPerMethod: perMethod,
}, nil
}

View File

@@ -0,0 +1,44 @@
package rpcstats
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Service represents our own implementation of status status operations.
type Service struct{}
// New returns a new Service.
func New() *Service {
return &Service{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "rpcstats",
Version: "1.0",
Service: NewAPI(s),
Public: true,
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start() error {
resetStats()
return nil
}
// Stop is run when a service is stopped.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Stop() error {
return nil
}

View File

@@ -0,0 +1,48 @@
package rpcstats
import (
"sync"
)
type RPCUsageStats struct {
total uint
counterPerMethod map[string]uint
rw sync.RWMutex
}
var stats *RPCUsageStats
func getInstance() *RPCUsageStats {
if stats == nil {
stats = &RPCUsageStats{
total: 0,
counterPerMethod: map[string]uint{},
}
}
return stats
}
func getStats() (uint, map[string]uint) {
stats := getInstance()
stats.rw.RLock()
defer stats.rw.RUnlock()
return stats.total, stats.counterPerMethod
}
func resetStats() {
stats := getInstance()
stats.rw.Lock()
defer stats.rw.Unlock()
stats.total = 0
stats.counterPerMethod = map[string]uint{}
}
func CountCall(method string) {
stats := getInstance()
stats.rw.Lock()
defer stats.rw.Unlock()
stats.total++
stats.counterPerMethod[method]++
}

View File

@@ -0,0 +1,89 @@
package status
import (
"encoding/json"
"errors"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/common/shard"
)
// Make sure that Service implements node.Lifecycle interface.
var _ node.Lifecycle = (*Service)(nil)
var ErrNotInitialized = errors.New("status public api not initialized")
// Service represents out own implementation of personal sign operations.
type Service struct {
messenger *protocol.Messenger
}
// New returns a new Service.
func New() *Service {
return &Service{}
}
func (s *Service) Init(messenger *protocol.Messenger) {
s.messenger = messenger
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "status",
Version: "1.0",
Service: NewPublicAPI(s),
Public: true,
},
}
}
// NewPublicAPI returns a reference to the PublicAPI object
func NewPublicAPI(s *Service) *PublicAPI {
api := &PublicAPI{
service: s,
}
return api
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return nil
}
type PublicAPI struct {
service *Service
}
func (p *PublicAPI) CommunityInfo(communityID types.HexBytes, shard *shard.Shard) (json.RawMessage, error) {
if p.service.messenger == nil {
return nil, ErrNotInitialized
}
community, err := p.service.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
CommunityKey: communityID.String(),
Shard: shard,
TryDatabase: true,
WaitForResponse: true,
})
if err != nil {
return nil, err
}
return community.MarshalPublicAPIJSON()
}

View File

@@ -0,0 +1,481 @@
package stickers
import (
"context"
"math/big"
"time"
"github.com/zenthangplus/goccm"
"olympos.io/encoding/edn"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/stickers"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/ipfs"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/transactions"
)
const maxConcurrentRequests = 3
const requestTimeout = time.Duration(5) * time.Second
// ConnectionType constants
type stickerStatus int
const (
statusAvailable stickerStatus = iota
statusInstalled
statusPending
statusPurchased
)
type API struct {
contractMaker *contracts.ContractMaker
accountsManager *account.GethManager
accountsDB *accounts.Database
pendingTracker *transactions.PendingTxTracker
keyStoreDir string
downloader *ipfs.Downloader
httpServer *server.MediaServer
ctx context.Context
}
type Sticker struct {
PackID *bigint.BigInt `json:"packID,omitempty"`
URL string `json:"url,omitempty"`
Hash string `json:"hash,omitempty"`
}
type StickerPack struct {
ID *bigint.BigInt `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
Owner common.Address `json:"owner,omitempty"`
Price *bigint.BigInt `json:"price"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Stickers []Sticker `json:"stickers"`
Status stickerStatus `json:"status"`
}
type StickerPackCollection map[uint]StickerPack
type ednSticker struct {
Hash string
}
type ednStickerPack struct {
Name string
Author string
Thumbnail string
Preview string
Stickers []ednSticker
}
type ednStickerPackInfo struct {
Meta ednStickerPack
}
func NewAPI(ctx context.Context, acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, keyStoreDir string, downloader *ipfs.Downloader, httpServer *server.MediaServer) *API {
result := &API{
contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient,
},
accountsManager: accountsManager,
accountsDB: acc,
pendingTracker: pendingTracker,
keyStoreDir: keyStoreDir,
downloader: downloader,
ctx: ctx,
httpServer: httpServer,
}
return result
}
func (api *API) Market(chainID uint64) ([]StickerPack, error) {
// TODO: eventually this should be changed to include pagination
accs, err := api.accountsDB.GetActiveAccounts()
if err != nil {
return nil, err
}
allStickerPacks, err := api.getContractPacks(chainID)
if err != nil {
return nil, err
}
purchasedPacks := make(map[uint]struct{})
purchasedPackChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan)
for {
select {
case err := <-errChan:
if err != nil {
return nil, err
}
case packID := <-purchasedPackChan:
if packID != nil {
purchasedPacks[uint(packID.Uint64())] = struct{}{}
}
case <-doneChan:
var result []StickerPack
for _, pack := range allStickerPacks {
packID := uint(pack.ID.Uint64())
_, isPurchased := purchasedPacks[packID]
if isPurchased {
pack.Status = statusPurchased
} else {
pack.Status = statusAvailable
}
result = append(result, pack)
}
return result, nil
}
}
}
func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
errChan <- err
return
}
if len(tokenIDs) == 0 {
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
for _, tokenID := range tokenIDs {
c.Wait()
go func(tokenID *big.Int) {
defer c.Done()
packID, err := stickerPack.TokenPackId(callOpts, tokenID)
if err != nil {
errChan <- err
return
}
resultChan <- packID
}(tokenID)
}
c.WaitAllDone()
}
func (api *API) getTokenPackIDs(chainID uint64, tokenIDs []*big.Int) ([]*big.Int, error) {
tokenPackIDChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.execTokenPackID(chainID, tokenIDs, tokenPackIDChan, errChan, doneChan)
var tokenPackIDs []*big.Int
for {
select {
case <-doneChan:
return tokenPackIDs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case t := <-tokenPackIDChan:
if t != nil {
tokenPackIDs = append(tokenPackIDs, t)
}
}
}
}
func (api *API) getPurchasedPackIDs(chainID uint64, account types.Address) ([]*big.Int, error) {
// TODO: this should be replaced in the future by something like TheGraph to reduce the number of requests to infura
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
balance, err := stickerPack.BalanceOf(callOpts, common.Address(account))
if err != nil {
return nil, err
}
tokenIDs, err := api.getTokenOwnerOfIndex(chainID, account, balance)
if err != nil {
return nil, err
}
return api.getTokenPackIDs(chainID, tokenIDs)
}
func (api *API) fetchStickerPacks(chainID uint64, resultChan chan<- *StickerPack, errChan chan<- error, doneChan chan<- struct{}) {
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
installedPacks, err := api.Installed()
if err != nil {
errChan <- err
return
}
pendingPacks, err := api.pendingStickerPacks()
if err != nil {
errChan <- err
return
}
stickerType, err := api.contractMaker.NewStickerType(chainID)
if err != nil {
errChan <- err
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
numPacks, err := stickerType.PackCount(callOpts)
if err != nil {
errChan <- err
return
}
if numPacks.Uint64() == 0 {
return
}
c := goccm.New(maxConcurrentRequests)
for i := uint64(0); i < numPacks.Uint64(); i++ {
c.Wait()
go func(i uint64) {
defer c.Done()
packID := new(big.Int).SetUint64(i)
_, exists := installedPacks[uint(i)]
if exists {
return // We already have the sticker pack data, no need to query it
}
_, exists = pendingPacks[uint(i)]
if exists {
return // We already have the sticker pack data, no need to query it
}
stickerPack, err := api.fetchPackData(stickerType, packID, true)
if err != nil {
log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err)
errChan <- err
return
}
resultChan <- stickerPack
}(i)
}
c.WaitAllDone()
}
func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) {
timeoutContext, timeoutCancel := context.WithTimeout(api.ctx, requestTimeout)
defer timeoutCancel()
callOpts := &bind.CallOpts{Context: timeoutContext, Pending: false}
packData, err := stickerType.GetPackData(callOpts, packID)
if err != nil {
return nil, err
}
stickerPack := &StickerPack{
ID: &bigint.BigInt{Int: packID},
Owner: packData.Owner,
Price: &bigint.BigInt{Int: packData.Price},
}
err = api.downloadPackData(stickerPack, packData.Contenthash, translateHashes)
if err != nil {
return nil, err
}
return stickerPack, nil
}
func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, translateHashes bool) error {
fileContent, err := api.downloader.Get(hexutil.Encode(contentHash)[2:], true)
if err != nil {
return err
}
return api.populateStickerPackAttributes(stickerPack, fileContent, translateHashes)
}
func (api *API) hashToURL(hash string) string {
return api.httpServer.MakeStickerURL(hash)
}
func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {
var stickerpackIPFSInfo ednStickerPackInfo
err := edn.Unmarshal(ednSource, &stickerpackIPFSInfo)
if err != nil {
return err
}
stickerPack.Author = stickerpackIPFSInfo.Meta.Author
stickerPack.Name = stickerpackIPFSInfo.Meta.Name
if translateHashes {
stickerPack.Preview = api.hashToURL(stickerpackIPFSInfo.Meta.Preview)
stickerPack.Thumbnail = api.hashToURL(stickerpackIPFSInfo.Meta.Thumbnail)
} else {
stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview
stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail
}
for _, s := range stickerpackIPFSInfo.Meta.Stickers {
url := ""
if translateHashes {
url = api.hashToURL(s.Hash)
}
stickerPack.Stickers = append(stickerPack.Stickers, Sticker{
PackID: stickerPack.ID,
URL: url,
Hash: s.Hash,
})
}
return nil
}
func (api *API) getContractPacks(chainID uint64) ([]StickerPack, error) {
stickerPackChan := make(chan *StickerPack)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.fetchStickerPacks(chainID, stickerPackChan, errChan, doneChan)
var packs []StickerPack
for {
select {
case <-doneChan:
return packs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case pack := <-stickerPackChan:
if pack != nil {
packs = append(packs, *pack)
}
}
}
}
func (api *API) getAccountsPurchasedPack(chainID uint64, accs []*accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
if len(accs) == 0 {
return
}
c := goccm.New(maxConcurrentRequests)
for _, account := range accs {
c.Wait()
go func(acc *accounts.Account) {
defer c.Done()
packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
if err != nil {
errChan <- err
return
}
for _, p := range packs {
resultChan <- p
}
}(account)
}
c.WaitAllDone()
}
func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
errChan <- err
return
}
if balance.Int64() == 0 {
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
for i := uint64(0); i < balance.Uint64(); i++ {
c.Wait()
go func(i uint64) {
defer c.Done()
tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
if err != nil {
errChan <- err
return
}
resultChan <- tokenID
}(i)
}
c.WaitAllDone()
}
func (api *API) getTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int) ([]*big.Int, error) {
tokenIDChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.execTokenOwnerOfIndex(chainID, account, balance, tokenIDChan, errChan, doneChan)
var tokenIDs []*big.Int
for {
select {
case <-doneChan:
return tokenIDs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case tokenID := <-tokenIDChan:
if tokenID != nil {
tokenIDs = append(tokenIDs, tokenID)
}
}
}
}

View File

@@ -0,0 +1,128 @@
package stickers
import (
"encoding/json"
"errors"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/services/wallet/bigint"
)
func (api *API) Install(chainID uint64, packID *bigint.BigInt) error {
installedPacks, err := api.installedStickerPacks()
if err != nil {
return err
}
if _, exists := installedPacks[uint(packID.Uint64())]; exists {
return errors.New("sticker pack is already installed")
}
// TODO: this does not validate if the pack is purchased. Should it?
stickerType, err := api.contractMaker.NewStickerType(chainID)
if err != nil {
return err
}
stickerPack, err := api.fetchPackData(stickerType, packID.Int, false)
if err != nil {
return err
}
installedPacks[uint(packID.Uint64())] = *stickerPack
err = api.accountsDB.SaveSettingField(settings.StickersPacksInstalled, installedPacks)
if err != nil {
return err
}
return nil
}
func (api *API) installedStickerPacks() (StickerPackCollection, error) {
stickerPacks := make(StickerPackCollection)
installedStickersJSON, err := api.accountsDB.GetInstalledStickerPacks()
if err != nil {
return nil, err
}
if installedStickersJSON == nil {
return stickerPacks, nil
}
err = json.Unmarshal(*installedStickersJSON, &stickerPacks)
if err != nil {
return nil, err
}
return stickerPacks, nil
}
func (api *API) Installed() (StickerPackCollection, error) {
stickerPacks, err := api.installedStickerPacks()
if err != nil {
return nil, err
}
for packID, stickerPack := range stickerPacks {
stickerPack.Status = statusInstalled
stickerPack.Preview = api.hashToURL(stickerPack.Preview)
stickerPack.Thumbnail = api.hashToURL(stickerPack.Thumbnail)
for i, sticker := range stickerPack.Stickers {
sticker.URL = api.hashToURL(sticker.Hash)
if err != nil {
return nil, err
}
stickerPack.Stickers[i] = sticker
}
stickerPacks[packID] = stickerPack
}
return stickerPacks, nil
}
func (api *API) Uninstall(packID *bigint.BigInt) error {
installedPacks, err := api.installedStickerPacks()
if err != nil {
return err
}
if _, exists := installedPacks[uint(packID.Uint64())]; !exists {
return errors.New("sticker pack is not installed")
}
delete(installedPacks, uint(packID.Uint64()))
err = api.accountsDB.SaveSettingField(settings.StickersPacksInstalled, installedPacks)
if err != nil {
return err
}
// Removing uninstalled pack from recent stickers
recentStickers, err := api.recentStickers()
if err != nil {
return err
}
idx := -1
for i, r := range recentStickers {
if r.PackID.Cmp(packID.Int) == 0 {
idx = i
break
}
}
if idx > -1 {
var newRecentStickers []Sticker
newRecentStickers = append(newRecentStickers, recentStickers[:idx]...)
if idx != len(recentStickers)-1 {
newRecentStickers = append(newRecentStickers, recentStickers[idx+1:]...)
}
return api.accountsDB.SaveSettingField(settings.StickersRecentStickers, newRecentStickers)
}
return nil
}

View File

@@ -0,0 +1,133 @@
package stickers
import (
"encoding/json"
"errors"
"math/big"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/services/wallet/bigint"
)
func (api *API) AddPending(chainID uint64, packID *bigint.BigInt) error {
pendingPacks, err := api.pendingStickerPacks()
if err != nil {
return err
}
if _, exists := pendingPacks[uint(packID.Uint64())]; exists {
return errors.New("sticker pack is already pending")
}
stickerType, err := api.contractMaker.NewStickerType(chainID)
if err != nil {
return err
}
stickerPack, err := api.fetchPackData(stickerType, packID.Int, false)
if err != nil {
return err
}
pendingPacks[uint(packID.Uint64())] = *stickerPack
return api.accountsDB.SaveSettingField(settings.StickersPacksPending, pendingPacks)
}
func (api *API) pendingStickerPacks() (StickerPackCollection, error) {
stickerPacks := make(StickerPackCollection)
pendingStickersJSON, err := api.accountsDB.GetPendingStickerPacks()
if err != nil {
return nil, err
}
if pendingStickersJSON == nil {
return stickerPacks, nil
}
err = json.Unmarshal(*pendingStickersJSON, &stickerPacks)
if err != nil {
return nil, err
}
return stickerPacks, nil
}
func (api *API) Pending() (StickerPackCollection, error) {
stickerPacks, err := api.pendingStickerPacks()
if err != nil {
return nil, err
}
for packID, stickerPack := range stickerPacks {
stickerPack.Status = statusPending
stickerPack.Preview = api.hashToURL(stickerPack.Preview)
stickerPack.Thumbnail = api.hashToURL(stickerPack.Thumbnail)
for i, sticker := range stickerPack.Stickers {
sticker.URL = api.hashToURL(sticker.Hash)
stickerPack.Stickers[i] = sticker
}
stickerPacks[packID] = stickerPack
}
return stickerPacks, nil
}
func (api *API) ProcessPending(chainID uint64) (pendingChanged StickerPackCollection, err error) {
pendingStickerPacks, err := api.pendingStickerPacks()
if err != nil {
return nil, err
}
accs, err := api.accountsDB.GetActiveAccounts()
if err != nil {
return nil, err
}
purchasedPacks := make(map[uint]struct{})
purchasedPackChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan)
for {
select {
case err := <-errChan:
if err != nil {
return nil, err
}
case packID := <-purchasedPackChan:
if packID != nil {
purchasedPacks[uint(packID.Uint64())] = struct{}{}
}
case <-doneChan:
result := make(StickerPackCollection)
for _, stickerPack := range pendingStickerPacks {
packID := uint(stickerPack.ID.Uint64())
if _, exists := purchasedPacks[packID]; !exists {
continue
}
delete(pendingStickerPacks, packID)
stickerPack.Status = statusPurchased
result[packID] = stickerPack
}
err = api.accountsDB.SaveSettingField(settings.StickersPacksPending, pendingStickerPacks)
return result, err
}
}
}
func (api *API) RemovePending(packID *bigint.BigInt) error {
pendingPacks, err := api.pendingStickerPacks()
if err != nil {
return err
}
if _, exists := pendingPacks[uint(packID.Uint64())]; !exists {
return nil
}
delete(pendingPacks, uint(packID.Uint64()))
return api.accountsDB.SaveSettingField(settings.StickersPacksPending, pendingPacks)
}

View File

@@ -0,0 +1,102 @@
package stickers
import (
"encoding/json"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/services/wallet/bigint"
)
const maxNumberRecentStickers = 24
func (api *API) recentStickers() ([]Sticker, error) {
installedStickersPacksJSON, err := api.accountsDB.GetInstalledStickerPacks()
if err != nil || installedStickersPacksJSON == nil {
return []Sticker{}, nil
}
recentStickersJSON, err := api.accountsDB.GetRecentStickers()
if err != nil || recentStickersJSON == nil {
return []Sticker{}, nil
}
recentStickersList := make([]Sticker, 0)
if err := json.Unmarshal(*recentStickersJSON, &recentStickersList); err != nil {
return []Sticker{}, err
}
var installedStickersPacks map[string]StickerPack
if err := json.Unmarshal(*installedStickersPacksJSON, &installedStickersPacks); err != nil {
return []Sticker{}, err
}
recentStickersListInExistingPacks := make([]Sticker, 0)
existingPackIDs := make(map[string]bool)
for k := range installedStickersPacks {
existingPackIDs[k] = true
}
for _, s := range recentStickersList {
packIDStr := s.PackID.String()
if _, exists := existingPackIDs[packIDStr]; exists {
recentStickersListInExistingPacks = append(recentStickersListInExistingPacks, s)
}
}
return recentStickersListInExistingPacks, nil
}
func (api *API) ClearRecent() error {
var recentStickersList []Sticker
return api.accountsDB.SaveSettingField(settings.StickersRecentStickers, recentStickersList)
}
func (api *API) Recent() ([]Sticker, error) {
recentStickersList, err := api.recentStickers()
if err != nil {
return nil, err
}
for i, sticker := range recentStickersList {
sticker.URL = api.hashToURL(sticker.Hash)
recentStickersList[i] = sticker
}
return recentStickersList, nil
}
func (api *API) AddRecent(packID *bigint.BigInt, hash string) error {
sticker := Sticker{
PackID: packID,
Hash: hash,
}
recentStickersList, err := api.recentStickers()
if err != nil {
return err
}
// Remove duplicated
idx := -1
for i, currSticker := range recentStickersList {
if currSticker.PackID.Cmp(sticker.PackID.Int) == 0 && currSticker.Hash == sticker.Hash {
idx = i
}
}
if idx > -1 {
recentStickersList = append(recentStickersList[:idx], recentStickersList[idx+1:]...)
}
sticker.URL = ""
if len(recentStickersList) >= maxNumberRecentStickers {
recentStickersList = append([]Sticker{sticker}, recentStickersList[:maxNumberRecentStickers-1]...)
} else {
recentStickersList = append([]Sticker{sticker}, recentStickersList...)
}
return api.accountsDB.SaveSettingField(settings.StickersRecentStickers, recentStickersList)
}

View File

@@ -0,0 +1,76 @@
package stickers
import (
"context"
"github.com/ethereum/go-ethereum/p2p"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/ipfs"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/transactions"
)
// NewService initializes service instance.
func NewService(acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, config *params.NodeConfig, downloader *ipfs.Downloader, httpServer *server.MediaServer, pendingTracker *transactions.PendingTxTracker) *Service {
ctx, cancel := context.WithCancel(context.Background())
return &Service{
accountsDB: acc,
rpcClient: rpcClient,
accountsManager: accountsManager,
keyStoreDir: config.KeyStoreDir,
downloader: downloader,
httpServer: httpServer,
ctx: ctx,
cancel: cancel,
api: NewAPI(ctx, acc, rpcClient, accountsManager, pendingTracker, config.KeyStoreDir, downloader, httpServer),
}
}
// Service is a browsers service.
type Service struct {
accountsDB *accounts.Database
rpcClient *rpc.Client
accountsManager *account.GethManager
downloader *ipfs.Downloader
keyStoreDir string
httpServer *server.MediaServer
ctx context.Context
cancel context.CancelFunc
api *API
}
// Start a service.
func (s *Service) Start() error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
s.cancel()
return nil
}
func (s *Service) API() *API {
return s.api
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []ethRpc.API {
return []ethRpc.API{
{
Namespace: "stickers",
Version: "0.1.0",
Service: s.api,
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@@ -0,0 +1,115 @@
package stickers
import (
"context"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/contracts/snt"
"github.com/status-im/status-go/contracts/stickers"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/wallet/bigint"
)
func (api *API) BuyPrepareTxCallMsg(chainID uint64, from types.Address, packID *bigint.BigInt) (ethereum.CallMsg, error) {
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
stickerType, err := api.contractMaker.NewStickerType(chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
packInfo, err := stickerType.GetPackData(callOpts, packID.Int)
if err != nil {
return ethereum.CallMsg{}, err
}
stickerMarketABI, err := abi.JSON(strings.NewReader(stickers.StickerMarketABI))
if err != nil {
return ethereum.CallMsg{}, err
}
extraData, err := stickerMarketABI.Pack("buyToken", packID.Int, from, packInfo.Price)
if err != nil {
return ethereum.CallMsg{}, err
}
sntABI, err := abi.JSON(strings.NewReader(snt.SNTABI))
if err != nil {
return ethereum.CallMsg{}, err
}
stickerMarketAddress, err := stickers.StickerMarketContractAddress(chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
data, err := sntABI.Pack("approveAndCall", stickerMarketAddress, packInfo.Price, extraData)
if err != nil {
return ethereum.CallMsg{}, err
}
sntAddress, err := snt.ContractAddress(chainID)
if err != nil {
return ethereum.CallMsg{}, err
}
return ethereum.CallMsg{
From: common.Address(from),
To: &sntAddress,
Value: big.NewInt(0),
Data: data,
}, nil
}
func (api *API) BuyPrepareTx(ctx context.Context, chainID uint64, from types.Address, packID *bigint.BigInt) (interface{}, error) {
callMsg, err := api.BuyPrepareTxCallMsg(chainID, from, packID)
if err != nil {
return nil, err
}
return toCallArg(callMsg), nil
}
func (api *API) BuyEstimate(ctx context.Context, chainID uint64, from types.Address, packID *bigint.BigInt) (uint64, error) {
callMsg, err := api.BuyPrepareTxCallMsg(chainID, from, packID)
if err != nil {
return 0, err
}
ethClient, err := api.contractMaker.RPCClient.EthClient(chainID)
if err != nil {
return 0, err
}
return ethClient.EstimateGas(ctx, callMsg)
}
func (api *API) StickerMarketAddress(ctx context.Context, chainID uint64) (common.Address, error) {
return stickers.StickerMarketContractAddress(chainID)
}
func toCallArg(msg ethereum.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
"to": msg.To,
}
if len(msg.Data) > 0 {
arg["data"] = hexutil.Bytes(msg.Data)
}
if msg.Value != nil {
arg["value"] = (*hexutil.Big)(msg.Value)
}
if msg.Gas != 0 {
arg["gas"] = hexutil.Uint64(msg.Gas)
}
if msg.GasPrice != nil {
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
}
return arg
}

View File

@@ -0,0 +1,79 @@
# Signal Subscriptions
This package implements subscriptions mechanics using [`signal`](../../signal) package.
It defines 3 new RPC methods in the `eth` namespace and 2 signals.
## Methods
###`eth_subscribeSignal`
Creates a new filter and subscribes to its changes via signals.
Parameters: receives the method name and parameters for the filter that is created.
Example 1:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribeSignal",
"params": ["eth_newPendingTransactionFilter", []]
}
```
Example 2:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_subscribeSignal",
"params": [
"shh_newMessageFilter",
[{ "symKeyID":"abcabcabcabc", "topics": ["0x12341234"] }]
]
}
```
Supported filters: `shh_newMessageFilter`, `eth_newFilter`, `eth_newBlockFilter`, `eth_newPendingTransactionFilter`
(see [Ethereum documentation](https://github.com/ethereum/wiki/wiki/JSON-RPC) for respective parameters).
Returns: error or `subscriptionID`.
###`eth_unsubscribeSignal`
Unsubscribes and removes one filter by its ID.
NOTE: Unsubscribing from a filter removes it.
Parameters: `subscriptionID` obtained from `eth_subscribeSignal`
Returns: error if something went wrong while unsubscribing.
## Signals
1. Subscription data received
```json
{
"type": "subscriptions.data",
"event": {
"subscription_id": "shh_0x01",
"data": {
<whisper envelope 01>,
<whisper envelope 02>,
...
}
}
```
2. Subscription error received
```json
{
"type": "subscriptions.error",
"event": {
"subscription_id": "shh_0x01",
"error_message": "can not find filter with id: 0x01"
}
}
```

View File

@@ -0,0 +1,47 @@
package subscriptions
import (
"fmt"
"time"
"github.com/status-im/status-go/rpc"
)
type API struct {
rpcPrivateClientFunc func() *rpc.Client
activeSubscriptions *Subscriptions
}
func NewPublicAPI(rpcPrivateClientFunc func() *rpc.Client) *API {
return &API{
rpcPrivateClientFunc: rpcPrivateClientFunc,
activeSubscriptions: NewSubscriptions(100 * time.Millisecond),
}
}
func (api *API) SubscribeSignal(method string, args []interface{}) (SubscriptionID, error) {
var (
filter filter
err error
namespace = method[:3]
)
switch namespace {
case "shh":
filter, err = installShhFilter(api.rpcPrivateClientFunc(), method, args)
case "eth":
filter, err = installEthFilter(api.rpcPrivateClientFunc(), method, args)
default:
err = fmt.Errorf("unexpected namespace: %s", namespace)
}
if err != nil {
return "", fmt.Errorf("[SubscribeSignal] could not subscribe, failed to call %s: %v", method, err)
}
return api.activeSubscriptions.Create(namespace, filter)
}
func (api *API) UnsubscribeSignal(id string) error {
return api.activeSubscriptions.Remove(SubscriptionID(id))
}

View File

@@ -0,0 +1,7 @@
package subscriptions
type filter interface {
getID() string
getChanges() ([]interface{}, error)
uninstall() error
}

View File

@@ -0,0 +1,65 @@
package subscriptions
import (
"fmt"
"github.com/status-im/status-go/rpc"
)
type ethFilter struct {
id string
rpcClient *rpc.Client
}
func installEthFilter(rpcClient *rpc.Client, method string, args []interface{}) (*ethFilter, error) {
if err := validateEthMethod(method); err != nil {
return nil, err
}
var result string
err := rpcClient.Call(&result, rpcClient.UpstreamChainID, method, args...)
if err != nil {
return nil, err
}
filter := &ethFilter{
id: result,
rpcClient: rpcClient,
}
return filter, nil
}
func (ef *ethFilter) getID() string {
return ef.id
}
func (ef *ethFilter) getChanges() ([]interface{}, error) {
var result []interface{}
err := ef.rpcClient.Call(&result, ef.rpcClient.UpstreamChainID, "eth_getFilterChanges", ef.getID())
return result, err
}
func (ef *ethFilter) uninstall() error {
return ef.rpcClient.Call(nil, ef.rpcClient.UpstreamChainID, "eth_uninstallFilter", ef.getID())
}
func validateEthMethod(method string) error {
for _, allowedMethod := range []string{
"eth_newFilter",
"eth_newBlockFilter",
"eth_newPendingTransactionFilter",
} {
if method == allowedMethod {
return nil
}
}
return fmt.Errorf("unexpected filter method: %s", method)
}

View File

@@ -0,0 +1,57 @@
package subscriptions
import (
"fmt"
"github.com/status-im/status-go/rpc"
)
type whisperFilter struct {
id string
rpcClient *rpc.Client
}
func installShhFilter(rpcClient *rpc.Client, method string, args []interface{}) (*whisperFilter, error) {
if err := validateShhMethod(method); err != nil {
return nil, err
}
var result string
err := rpcClient.Call(&result, rpcClient.UpstreamChainID, method, args...)
if err != nil {
return nil, err
}
filter := &whisperFilter{
id: result,
rpcClient: rpcClient,
}
return filter, nil
}
func (wf *whisperFilter) getChanges() ([]interface{}, error) {
var result []interface{}
err := wf.rpcClient.Call(&result, wf.rpcClient.UpstreamChainID, "shh_getFilterMessages", wf.getID())
return result, err
}
func (wf *whisperFilter) getID() string {
return wf.id
}
func (wf *whisperFilter) uninstall() error {
return wf.rpcClient.Call(nil, wf.rpcClient.UpstreamChainID, "shh_deleteMessageFilter", wf.getID())
}
func validateShhMethod(method string) error {
if method != "shh_newMessageFilter" {
return fmt.Errorf("unexpected filter method: %s", method)
}
return nil
}

View File

@@ -0,0 +1,51 @@
package subscriptions
import (
gethnode "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/rpc"
)
// Make sure that Service implements gethnode.Lifecycle interface.
var _ gethnode.Lifecycle = (*Service)(nil)
// Service represents our own implementation of personal sign operations.
type Service struct {
api *API
}
// New returns a new Service.
func New(rpcPrivateClientFunc func() *rpc.Client) *Service {
return &Service{
api: NewPublicAPI(rpcPrivateClientFunc),
}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []gethrpc.API {
return []gethrpc.API{
{
Namespace: "eth",
Version: "1.0",
Service: s.api,
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return s.api.activeSubscriptions.removeAll()
}

View File

@@ -0,0 +1,19 @@
package subscriptions
import "github.com/status-im/status-go/signal"
type filterSignal struct {
filterID string
}
func newFilterSignal(filterID string) *filterSignal {
return &filterSignal{filterID}
}
func (s *filterSignal) SendError(err error) {
signal.SendSubscriptionErrorEvent(s.filterID, err)
}
func (s *filterSignal) SendData(data []interface{}) {
signal.SendSubscriptionDataEvent(s.filterID, data)
}

View File

@@ -0,0 +1,82 @@
package subscriptions
import (
"errors"
"fmt"
"sync"
"time"
)
type SubscriptionID string
type Subscription struct {
mu sync.RWMutex
id SubscriptionID
signal *filterSignal
quit chan struct{}
filter filter
started bool
}
func NewSubscription(namespace string, filter filter) *Subscription {
subscriptionID := NewSubscriptionID(namespace, filter.getID())
return &Subscription{
id: subscriptionID,
signal: newFilterSignal(string(subscriptionID)),
filter: filter,
}
}
func (s *Subscription) Start(checkPeriod time.Duration) error {
s.mu.Lock()
if s.started {
s.mu.Unlock()
return errors.New("subscription already started or used")
}
s.started = true
s.quit = make(chan struct{})
quit := s.quit
s.mu.Unlock()
ticker := time.NewTicker(checkPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
filterData, err := s.filter.getChanges()
if err != nil {
s.signal.SendError(err)
} else if len(filterData) > 0 {
s.signal.SendData(filterData)
}
case <-quit:
return nil
}
}
}
func (s *Subscription) Stop(uninstall bool) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.started {
return nil
}
select {
case _, ok := <-s.quit:
// handle a case of a closed channel
if !ok {
return nil
}
default:
close(s.quit)
}
if !uninstall {
return nil
}
return s.filter.uninstall()
}
func NewSubscriptionID(namespace, filterID string) SubscriptionID {
return SubscriptionID(fmt.Sprintf("%s-%s", namespace, filterID))
}

View File

@@ -0,0 +1,84 @@
package subscriptions
import (
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
)
type Subscriptions struct {
mu sync.Mutex
subs map[SubscriptionID]*Subscription
checkPeriod time.Duration
log log.Logger
}
func NewSubscriptions(period time.Duration) *Subscriptions {
return &Subscriptions{
subs: make(map[SubscriptionID]*Subscription),
checkPeriod: period,
log: log.New("package", "status-go/services/subsriptions.Subscriptions"),
}
}
func (s *Subscriptions) Create(namespace string, filter filter) (SubscriptionID, error) {
s.mu.Lock()
defer s.mu.Unlock()
newSub := NewSubscription(namespace, filter)
go func() {
err := newSub.Start(s.checkPeriod)
if err != nil {
s.log.Error("error while starting subscription", "err", err)
}
}()
s.subs[newSub.id] = newSub
return newSub.id, nil
}
func (s *Subscriptions) Remove(id SubscriptionID) error {
s.mu.Lock()
defer s.mu.Unlock()
found, err := s.stopSubscription(id, true)
if found {
delete(s.subs, id)
}
return err
}
func (s *Subscriptions) removeAll() error {
s.mu.Lock()
defer s.mu.Unlock()
unsubscribeErrors := make(map[SubscriptionID]error)
for id := range s.subs {
_, err := s.stopSubscription(id, false)
if err != nil {
unsubscribeErrors[id] = err
}
}
s.subs = make(map[SubscriptionID]*Subscription)
if len(unsubscribeErrors) > 0 {
return fmt.Errorf("errors while cleaning up subscriptions: %+v", unsubscribeErrors)
}
return nil
}
func (s *Subscriptions) stopSubscription(id SubscriptionID, uninstall bool) (bool, error) {
sub, found := s.subs[id]
if !found {
return false, nil
}
return true, sub.Stop(uninstall)
}

View File

@@ -0,0 +1,201 @@
package typeddata
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/big"
"sort"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
var (
bytes32Type, _ = abi.NewType("bytes32", "", nil)
int256Type, _ = abi.NewType("int256", "", nil)
errNotInteger = errors.New("not an integer")
)
// ValidateAndHash generates a hash of TypedData and verifies that chainId in the typed data matches currently selected chain.
func ValidateAndHash(typed TypedData, chain *big.Int) (common.Hash, error) {
if err := typed.ValidateChainID(chain); err != nil {
return common.Hash{}, err
}
return encodeData(typed)
}
// deps runs breadth-first traversal starting from target and collects all
// found composite dependencies types into result slice. target always will be first
// in the result array. all other dependencies are sorted alphabetically.
// for example: Z{c C, a A} A{c C} and the target is Z.
// result would be Z, A, B, C
func deps(target string, types Types) []string {
unique := map[string]struct{}{}
unique[target] = struct{}{}
visited := []string{target}
deps := []string{}
for len(visited) > 0 {
current := visited[0]
fields := types[current]
for i := range fields {
f := fields[i]
if _, defined := types[f.Type]; defined {
if _, exist := unique[f.Type]; !exist {
visited = append(visited, f.Type)
unique[f.Type] = struct{}{}
}
}
}
visited = visited[1:]
deps = append(deps, current)
}
sort.Slice(deps[1:], func(i, j int) bool {
return deps[1:][i] < deps[1:][j]
})
return deps
}
func typeString(target string, types Types) string {
b := new(bytes.Buffer)
for _, dep := range deps(target, types) {
b.WriteString(dep)
b.WriteString("(")
fields := types[dep]
first := true
for i := range fields {
if !first {
b.WriteString(",")
} else {
first = false
}
f := fields[i]
b.WriteString(f.Type)
b.WriteString(" ")
b.WriteString(f.Name)
}
b.WriteString(")")
}
return b.String()
}
func typeHash(target string, types Types) (rst common.Hash) {
return crypto.Keccak256Hash([]byte(typeString(target, types)))
}
func hashStruct(target string, data map[string]json.RawMessage, types Types) (rst common.Hash, err error) {
fields := types[target]
typeh := typeHash(target, types)
args := abi.Arguments{{Type: bytes32Type}}
vals := []interface{}{typeh}
for i := range fields {
f := fields[i]
val, typ, err := toABITypeAndValue(f, data, types)
if err != nil {
return rst, err
}
vals = append(vals, val)
args = append(args, abi.Argument{Name: f.Name, Type: typ})
}
packed, err := args.Pack(vals...)
if err != nil {
return rst, err
}
return crypto.Keccak256Hash(packed), nil
}
func toABITypeAndValue(f Field, data map[string]json.RawMessage, types Types) (val interface{}, typ abi.Type, err error) {
if f.Type == "string" {
var str string
if err = json.Unmarshal(data[f.Name], &str); err != nil {
return
}
return crypto.Keccak256Hash([]byte(str)), bytes32Type, nil
} else if f.Type == "bytes" {
var bytes hexutil.Bytes
if err = json.Unmarshal(data[f.Name], &bytes); err != nil {
return
}
return crypto.Keccak256Hash(bytes), bytes32Type, nil
} else if _, exist := types[f.Type]; exist {
var obj map[string]json.RawMessage
if err = json.Unmarshal(data[f.Name], &obj); err != nil {
return
}
val, err = hashStruct(f.Type, obj, types)
if err != nil {
return
}
return val, bytes32Type, nil
}
return atomicType(f, data)
}
func atomicType(f Field, data map[string]json.RawMessage) (val interface{}, typ abi.Type, err error) {
typ, err = abi.NewType(f.Type, "", nil)
if err != nil {
return
}
if typ.T == abi.SliceTy || typ.T == abi.ArrayTy || typ.T == abi.FunctionTy {
return val, typ, errors.New("arrays, slices and functions are not supported")
} else if typ.T == abi.FixedBytesTy {
return toFixedBytes(f, data[f.Name])
} else if typ.T == abi.AddressTy {
val, err = toAddress(f, data[f.Name])
} else if typ.T == abi.IntTy || typ.T == abi.UintTy {
return toInt(f, data[f.Name])
} else if typ.T == abi.BoolTy {
val, err = toBool(f, data[f.Name])
} else {
err = fmt.Errorf("type %s is not supported", f.Type)
}
return
}
func toFixedBytes(f Field, data json.RawMessage) (rst [32]byte, typ abi.Type, err error) {
var bytes hexutil.Bytes
if err = json.Unmarshal(data, &bytes); err != nil {
return
}
typ = bytes32Type
rst = [32]byte{}
// reduce the length to the advertised size
if len(bytes) > typ.Size {
bytes = bytes[:typ.Size]
}
copy(rst[:], bytes)
return rst, typ, nil
}
func toInt(f Field, data json.RawMessage) (val *big.Int, typ abi.Type, err error) {
val = new(big.Int)
if err = json.Unmarshal(data, &val); err != nil {
var buf string
err = json.Unmarshal(data, &buf)
if err != nil {
return
}
var ok bool
val, ok = val.SetString(buf, 0)
if !ok {
err = errNotInteger
return
}
}
return val, int256Type, nil
}
func toAddress(f Field, data json.RawMessage) (rst common.Address, err error) {
err = json.Unmarshal(data, &rst)
return
}
func toBool(f Field, data json.RawMessage) (rst bool, err error) {
err = json.Unmarshal(data, &rst)
return
}

View File

@@ -0,0 +1,79 @@
package typeddata
import (
"crypto/ecdsa"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
)
var (
// x19 to avoid collision with rlp encode. x01 version byte defined in EIP-191
messagePadding = []byte{0x19, 0x01}
)
func encodeData(typed TypedData) (rst common.Hash, err error) {
domainSeparator, err := hashStruct(eip712Domain, typed.Domain, typed.Types)
if err != nil {
return rst, err
}
primary, err := hashStruct(typed.PrimaryType, typed.Message, typed.Types)
if err != nil {
return rst, err
}
return crypto.Keccak256Hash(messagePadding, domainSeparator[:], primary[:]), nil
}
func encodeDataV4(typedData signercore.TypedData, chain *big.Int) ([]byte, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, err
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
sighash := crypto.Keccak256(rawData)
return sighash, nil
}
func HashTypedDataV4(typedData signercore.TypedData, chain *big.Int) (common.Hash, error) {
hashBytes, err := encodeDataV4(typedData, chain)
if err != nil {
return common.Hash{}, err
}
return common.BytesToHash(hashBytes), nil
}
func SignTypedDataV4(typedData signercore.TypedData, prv *ecdsa.PrivateKey, chain *big.Int) (hexutil.Bytes, error) {
sighash, err := HashTypedDataV4(typedData, chain)
if err != nil {
return nil, err
}
sig, err := crypto.Sign(sighash[:], prv)
if err != nil {
return nil, err
}
sig[64] += 27
return sig, nil
}
// Sign TypedData with a given private key. Verify that chainId in the typed data matches currently selected chain.
func Sign(typed TypedData, prv *ecdsa.PrivateKey, chain *big.Int) ([]byte, error) {
hash, err := ValidateAndHash(typed, chain)
if err != nil {
return nil, err
}
sig, err := crypto.Sign(hash[:], prv)
if err != nil {
return nil, err
}
sig[64] += 27
return sig, nil
}

View File

@@ -0,0 +1,93 @@
package typeddata
import (
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
)
const (
eip712Domain = "EIP712Domain"
chainIDKey = "chainId"
)
// Types define fields for each composite type.
type Types map[string][]Field
// Field stores name and solidity type of the field.
type Field struct {
Name string `json:"name"`
Type string `json:"type"`
}
// Validate checks that both name and type are not empty.
func (f Field) Validate() error {
if len(f.Name) == 0 {
return errors.New("`name` is required")
}
if len(f.Type) == 0 {
return errors.New("`type` is required")
}
return nil
}
// TypedData defines typed data according to eip-712.
type TypedData struct {
Types Types `json:"types"`
PrimaryType string `json:"primaryType"`
Domain map[string]json.RawMessage `json:"domain"`
Message map[string]json.RawMessage `json:"message"`
}
// Validate that required fields are defined.
// This method doesn't check if dependencies of the main type are defined, it will be validated
// when type string is computed.
func (t TypedData) Validate() error {
if _, exist := t.Types[eip712Domain]; !exist {
return fmt.Errorf("`%s` must be in `types`", eip712Domain)
}
if t.PrimaryType == "" {
return errors.New("`primaryType` is required")
}
if _, exist := t.Types[t.PrimaryType]; !exist {
return fmt.Errorf("primary type `%s` not defined in types", t.PrimaryType)
}
if t.Domain == nil {
return errors.New("`domain` is required")
}
if t.Message == nil {
return errors.New("`message` is required")
}
for typ := range t.Types {
fields := t.Types[typ]
for i := range fields {
if err := fields[i].Validate(); err != nil {
return fmt.Errorf("field %d from type `%s` is invalid: %v", i, typ, err)
}
}
}
return nil
}
// ValidateChainID accept chain as big integer and verifies if typed data belongs to the same chain.
func (t TypedData) ValidateChainID(chain *big.Int) error {
if _, exist := t.Domain[chainIDKey]; !exist {
return fmt.Errorf("domain misses chain key %s", chainIDKey)
}
var chainID int64
if err := json.Unmarshal(t.Domain[chainIDKey], &chainID); err != nil {
var chainIDString string
if err = json.Unmarshal(t.Domain[chainIDKey], &chainIDString); err != nil {
return err
}
if chainID, err = strconv.ParseInt(chainIDString, 0, 64); err != nil {
return err
}
}
if chainID != chain.Int64() {
return fmt.Errorf("chainId %d doesn't match selected chain %s", chainID, chain)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More