1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-07 21:37:04 +09:00

Go 2450 subscriptions (#1041)

* GO-2450 Add Payments commands

* GO-2450 Payment methods: remove IDs from requests

* GO-2450 Use GetAccountEthAddress()

* GO-2450 Use any-sync instead of pp repo

* GO-2450 Add RequestedAnyName field

* GO-2450 Basic nameservice methods

* GO-2450 New methods for subscriptions/nameservice

* GO-2450 Refactor: protos for payments

* GO-2450 Downgrade go to 1.20

* GO-2450 Fix build

* GO-2450 Refactoring: renames

* GO-2450 GetPortalURL implemented; Test tiers

* GO-2450 Update any-sync

* GO-2450 Fix: bootstrap

* GO-2450 Fix pp encryption: peer key -> sign key

* GO-2450 Bug fix: Ethereum wallet address

* GO-2450 Update tier names

* GO-2450 Email verification methods

* GO-2450 Return email if was verified before

* GO-2450 Update any-sync

* GO-2450 Update any-sync

* GO-2395 WiP: cache for PP node

* GO-2395: cache for PP node

* GO-2395 Change logics: return 0 tier when no response from the pp

* GO-2395 fix: cache logics

* GO-2450 Use any-sync from feature-payments branch for now

* GO-2395 any-sync update

* GO-2395 Fixes after review

* GO-2395 Refactoring after review

* GO-2395 Review fixes

* GO-2395 Build fix

* GO-2450 Refactoring: payments interface; tier -> int32

* GO-2450 Add FinalizeSubscription method

* GO-2450 Cache fix

* GO-2450 GetSubscriptionStatus: add NoCache

* GO-2734 Add global name to cache WIP

* GO-2450 go mod tidy

* GO-2450 Update any-sync

* GO-2450 Linter fix

* GO-2450 PaymentsTiersGet

* GO-2450 Refactoring: PaymentsGetTiers

* GO-2450 NS: implement NameServiceResolveAnyId

* GO-2734 Use AnyId to retrieve Global name

* GO-2734 Add GlobalName to identity

* GO-2734 Implement DetailsSettable in participant

* GO-2734 Get GlobalName from NN only on app start

* GO-2450 Upgrade any-sync to v0.3.33: TODOs in the required methods

* GO-2734 Fix tests WIP

* GO-2734 Use batch method

* GO-2734 Fix unittest

* GO-3061 Refactoring: payments - huge renames

* GO-3061 Add EventMembershipUpdate

* GO-2734 Fix tests 2

* GO-2734 Fixes upon pr comments

* GO-2450 Fix panic with nil cache.data

* GO-2450 Fixes after merge

* GO-2734 Fix unittests WIP

* GO-2734 Move mock expectations to newFixture

* GO-2734 Add return statement in mock

* GO-2734 Add check if name was found

* GO-2450 Add IsNameValid method

* GO-2450 Fix tests: cache

* GO-2734 Resolve names directly from NS

* GO-2450 Cache logics fix

* GO-2450 refactoring: cache logics simplified

* GO-2450 refactoring: IsNameValid - code -> error

* GO-2734 Set globalName in new spaces

* GO-3128 IsNameValid, GetAllTiers rebuilt

* GO-2734 Rename forceUpdate flag

* GO-2734 Save globalName even if is not found

* GO-3128 IsNameValid, GetAllTiers rebuilt

* GO-3128 Fix string len

* GO-2734 Rename UpdateIdentities

---------

Co-authored-by: kirillston <stonozhenko@anytype.io>
Co-authored-by: Kirill Stonozhenko <40611691+KirillSto@users.noreply.github.com>
This commit is contained in:
Anthony Akentiev 2024-03-26 17:40:15 +00:00 committed by GitHub
parent c127cd5426
commit bf46949207
Signed by: github
GPG key ID: B5690EEEBB952194
30 changed files with 20564 additions and 2370 deletions

View file

@ -91,3 +91,6 @@ packages:
github.com/anyproto/anytype-heart/core/files/fileacl:
interfaces:
Service:
github.com/anyproto/anytype-heart/core/payments/cache:
interfaces:
CacheService:

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@ import (
"github.com/anyproto/anytype-heart/core/invitestore"
"github.com/anyproto/anytype-heart/core/kanban"
"github.com/anyproto/anytype-heart/core/notifications"
"github.com/anyproto/anytype-heart/core/payments"
"github.com/anyproto/anytype-heart/core/recordsbatcher"
"github.com/anyproto/anytype-heart/core/subscription"
"github.com/anyproto/anytype-heart/core/syncstatus"
@ -94,6 +95,11 @@ import (
"github.com/anyproto/anytype-heart/util/linkpreview"
"github.com/anyproto/anytype-heart/util/unsplash"
"github.com/anyproto/anytype-heart/util/vcs"
"github.com/anyproto/any-sync/nameservice/nameserviceclient"
"github.com/anyproto/any-sync/paymentservice/paymentserviceclient"
paymentscache "github.com/anyproto/anytype-heart/core/payments/cache"
)
var (
@ -270,7 +276,11 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(profiler.New()).
Register(identity.New(30*time.Second, 10*time.Second)).
Register(templateservice.New()).
Register(notifications.New())
Register(notifications.New()).
Register(paymentserviceclient.New()).
Register(nameserviceclient.New()).
Register(payments.New()).
Register(paymentscache.New())
}
func MiddlewareVersion() string {

View file

@ -8,6 +8,8 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/identityrepo/identityrepoproto"
"github.com/anyproto/any-sync/nameservice/nameserviceclient"
"github.com/anyproto/any-sync/nameservice/nameserviceproto"
"github.com/anyproto/any-sync/util/crypto"
"github.com/dgraph-io/badger/v4"
"github.com/gogo/protobuf/proto"
@ -17,6 +19,7 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/files/fileacl"
"github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/database"
@ -39,6 +42,8 @@ var (
type Service interface {
GetMyProfileDetails() (identity string, metadataKey crypto.SymKey, details *types.Struct)
UpdateGlobalNames()
RegisterIdentity(spaceId string, identity string, encryptionKey crypto.SymKey, observer func(identity string, profile *model.IdentityProfile)) error
// UnregisterIdentity removes the observer for the identity in specified space
@ -73,6 +78,8 @@ type service struct {
spaceIdDeriver spaceIdDeriver
identityRepoClient identityRepoClient
fileAclService fileacl.Service
wallet wallet.Wallet
namingService nameserviceclient.AnyNsClientService
closing chan struct{}
startedCh chan struct{}
techSpaceId string
@ -84,13 +91,15 @@ type service struct {
pushIdentityTimer *time.Timer // timer for batching
pushIdentityBatchTimeout time.Duration
identityObservePeriod time.Duration
identityForceUpdate chan struct{}
lock sync.RWMutex
identityObservePeriod time.Duration
identityForceUpdate chan struct{}
globalNamesForceUpdate chan struct{}
lock sync.RWMutex
// identity => spaceId => observer
identityObservers map[string]map[string]*observer
identityEncryptionKeys map[string]crypto.SymKey
identityProfileCache map[string]*model.IdentityProfile
identityGlobalNames map[string]*nameserviceproto.NameByAddressResponse
}
func New(identityObservePeriod time.Duration, pushIdentityBatchTimeout time.Duration) Service {
@ -98,10 +107,12 @@ func New(identityObservePeriod time.Duration, pushIdentityBatchTimeout time.Dura
startedCh: make(chan struct{}),
closing: make(chan struct{}),
identityForceUpdate: make(chan struct{}),
globalNamesForceUpdate: make(chan struct{}),
identityObservePeriod: identityObservePeriod,
identityObservers: make(map[string]map[string]*observer),
identityEncryptionKeys: make(map[string]crypto.SymKey),
identityProfileCache: make(map[string]*model.IdentityProfile),
identityGlobalNames: make(map[string]*nameserviceproto.NameByAddressResponse),
pushIdentityBatchTimeout: pushIdentityBatchTimeout,
}
}
@ -114,6 +125,8 @@ func (s *service) Init(a *app.App) (err error) {
s.identityRepoClient = app.MustComponent[identityRepoClient](a)
s.fileAclService = app.MustComponent[fileacl.Service](a)
s.dbProvider = app.MustComponent[datastore.Datastore](a)
s.wallet = app.MustComponent[wallet.Wallet](a)
s.namingService = app.MustComponent[nameserviceclient.AnyNsClientService](a)
return
}
@ -218,6 +231,13 @@ func (s *service) GetMyProfileDetails() (identity string, metadataKey crypto.Sym
return s.myIdentity, s.spaceService.AccountMetadataSymKey(), s.currentProfileDetails
}
func (s *service) UpdateGlobalNames() {
select {
case s.globalNamesForceUpdate <- struct{}{}:
default:
}
}
func (s *service) WaitProfile(ctx context.Context, identity string) *model.IdentityProfile {
profile := s.getProfileFromCache(identity)
if profile != nil {
@ -282,6 +302,7 @@ func (s *service) cacheProfileDetails(details *types.Struct) {
Name: pbtypes.GetString(details, bundle.RelationKeyName.String()),
Description: pbtypes.GetString(details, bundle.RelationKeyDescription.String()),
IconCid: pbtypes.GetString(details, bundle.RelationKeyIconImage.String()),
GlobalName: pbtypes.GetString(details, bundle.RelationKeyGlobalName.String()),
}
s.lock.RLock()
@ -318,6 +339,7 @@ func (s *service) pushProfileToIdentityRegistry(ctx context.Context) error {
Description: pbtypes.GetString(s.currentProfileDetails, bundle.RelationKeyDescription.String()),
IconCid: iconCid,
IconEncryptionKeys: iconEncryptionKeys,
GlobalName: pbtypes.GetString(s.currentProfileDetails, bundle.RelationKeyGlobalName.String()),
}
data, err := proto.Marshal(identityProfile)
if err != nil {
@ -352,8 +374,8 @@ func (s *service) observeIdentitiesLoop() {
defer ticker.Stop()
ctx := context.Background()
observe := func() {
err := s.observeIdentities(ctx)
observe := func(globalNamesForceUpdate bool) {
err := s.observeIdentities(ctx, globalNamesForceUpdate)
if err != nil {
log.Error("error observing identities", zap.Error(err))
}
@ -363,9 +385,11 @@ func (s *service) observeIdentitiesLoop() {
case <-s.closing:
return
case <-s.identityForceUpdate:
observe()
observe(false)
case <-s.globalNamesForceUpdate:
observe(true)
case <-ticker.C:
observe()
observe(false)
}
}
}
@ -373,7 +397,7 @@ func (s *service) observeIdentitiesLoop() {
const identityRepoDataKind = "profile"
// TODO Maybe we need to use backoff in case of error from coordinator
func (s *service) observeIdentities(ctx context.Context) error {
func (s *service) observeIdentities(ctx context.Context, globalNamesForceUpdate bool) error {
s.lock.RLock()
defer s.lock.RUnlock()
@ -392,6 +416,10 @@ func (s *service) observeIdentities(ctx context.Context) error {
return fmt.Errorf("failed to pull identity: %w", err)
}
if err = s.fetchGlobalNames(append(identities, s.myIdentity), globalNamesForceUpdate); err != nil {
log.Error("error fetching identities global names from Naming Service", zap.Error(err))
}
for _, identityData := range identitiesData {
err := s.broadcastIdentityProfile(identityData)
if err != nil {
@ -449,6 +477,10 @@ func (s *service) broadcastIdentityProfile(identityData *identityrepoproto.DataW
return fmt.Errorf("find profile: %w", err)
}
if globalName, found := s.identityGlobalNames[identityData.Identity]; found && globalName.Found {
profile.GlobalName = globalName.Name
}
prevProfile, ok := s.identityProfileCache[identityData.Identity]
hasUpdates := !ok || !proto.Equal(prevProfile, profile)
@ -498,6 +530,32 @@ func (s *service) findProfile(identityData *identityrepoproto.DataWithIdentity)
return profile, rawProfile, nil
}
func (s *service) fetchGlobalNames(identities []string, forceUpdate bool) error {
if len(s.identityGlobalNames) == len(identities) && !forceUpdate {
return nil
}
response, err := s.namingService.BatchGetNameByAnyId(context.Background(), &nameserviceproto.BatchNameByAnyIdRequest{AnyAddresses: identities})
if err != nil {
return err
}
if response == nil {
return nil
}
for i, anyID := range identities {
s.identityGlobalNames[anyID] = response.Results[i]
if anyID == s.myIdentity && response.Results[i].Found {
s.currentProfileDetailsLock.RLock()
details := pbtypes.CopyStruct(s.currentProfileDetails)
s.currentProfileDetailsLock.RUnlock()
details.Fields[bundle.RelationKeyGlobalName.String()] = pbtypes.String(response.Results[i].Name)
if err = s.objectStore.UpdateObjectDetails(pbtypes.GetString(details, bundle.RelationKeyId.String()), details); err != nil {
return err
}
}
}
return nil
}
func (s *service) cacheIdentityProfile(rawProfile []byte, profile *model.IdentityProfile) error {
s.identityProfileCache[profile.Identity] = profile
return badgerhelper.SetValue(s.db, makeIdentityProfileKey(profile.Identity), rawProfile)

View file

@ -9,13 +9,18 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/identityrepo/identityrepoproto"
mock_nameserviceclient "github.com/anyproto/any-sync/nameservice/nameserviceclient/mock"
"github.com/anyproto/any-sync/nameservice/nameserviceproto"
"github.com/anyproto/any-sync/util/crypto"
"github.com/gogo/protobuf/proto"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/anytype/account/mock_account"
"github.com/anyproto/anytype-heart/core/files/fileacl/mock_fileacl"
"github.com/anyproto/anytype-heart/core/wallet/mock_wallet"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -29,10 +34,15 @@ type fixture struct {
coordinatorClient *inMemoryIdentityRepo
}
const testObserverPeriod = 1 * time.Millisecond
const (
testObserverPeriod = 1 * time.Millisecond
globalName = "anytypeuser.any"
identity = "identity1"
)
func newFixture(t *testing.T) *fixture {
ctx := context.Background()
ctrl := gomock.NewController(t)
identityRepoClient := newInMemoryIdentityRepo()
objectStore := objectstore.NewStoreFixture(t)
@ -40,6 +50,17 @@ func newFixture(t *testing.T) *fixture {
spaceService := mock_space.NewMockService(t)
fileAclService := mock_fileacl.NewMockService(t)
dataStore := datastore.NewInMemory()
wallet := mock_wallet.NewMockWallet(t)
nsClient := mock_nameserviceclient.NewMockAnyNsClientService(ctrl)
nsClient.EXPECT().BatchGetNameByAnyId(gomock.Any(), &nameserviceproto.BatchNameByAnyIdRequest{AnyAddresses: []string{identity, ""}}).AnyTimes().
Return(&nameserviceproto.BatchNameByAddressResponse{Results: []*nameserviceproto.NameByAddressResponse{{
Found: true,
Name: globalName,
}, {
Found: false,
Name: "",
},
}}, nil)
err := dataStore.Run(ctx)
require.NoError(t, err)
@ -51,6 +72,8 @@ func newFixture(t *testing.T) *fixture {
a.Register(testutil.PrepareMock(ctx, a, accountService))
a.Register(testutil.PrepareMock(ctx, a, spaceService))
a.Register(testutil.PrepareMock(ctx, a, fileAclService))
a.Register(testutil.PrepareMock(ctx, a, wallet))
a.Register(testutil.PrepareMock(ctx, a, nsClient))
svc := New(testObserverPeriod, 1*time.Microsecond)
err = svc.Init(a)
@ -63,6 +86,7 @@ func newFixture(t *testing.T) *fixture {
db, err := dataStore.LocalStorage()
require.NoError(t, err)
svcRef.db = db
svcRef.currentProfileDetails = &types.Struct{Fields: make(map[string]*types.Value)}
fx := &fixture{
service: svcRef,
coordinatorClient: identityRepoClient,
@ -145,8 +169,9 @@ func TestIdentityProfileCache(t *testing.T) {
profileSymKey, err := crypto.NewRandomAES()
require.NoError(t, err)
wantProfile := &model.IdentityProfile{
Identity: identity,
Name: "name1",
Identity: identity,
Name: "name1",
GlobalName: globalName,
}
wantData := marshalProfile(t, wantProfile, profileSymKey)
@ -174,8 +199,9 @@ func TestObservers(t *testing.T) {
profileSymKey, err := crypto.NewRandomAES()
require.NoError(t, err)
wantProfile := &model.IdentityProfile{
Identity: identity,
Name: "name1",
Identity: identity,
Name: "name1",
GlobalName: globalName,
}
wantData := marshalProfile(t, wantProfile, profileSymKey)
@ -203,6 +229,7 @@ func TestObservers(t *testing.T) {
Identity: identity,
Name: "name1 edited",
Description: "my description",
GlobalName: globalName,
}
wantData2 := marshalProfile(t, wantProfile2, profileSymKey)
@ -220,13 +247,15 @@ func TestObservers(t *testing.T) {
wantCalls := []*model.IdentityProfile{
{
Identity: identity,
Name: "name1",
Identity: identity,
Name: "name1",
GlobalName: globalName,
},
{
Identity: identity,
Name: "name1 edited",
Description: "my description",
GlobalName: globalName,
},
}
assert.Equal(t, wantCalls, callbackCalls)

137
core/nameservice.go Normal file
View file

@ -0,0 +1,137 @@
package core
import (
"context"
"github.com/anyproto/any-sync/nameservice/nameserviceclient"
proto "github.com/anyproto/any-sync/nameservice/nameserviceproto"
"github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pb"
)
// NameServiceResolveName does a name lookup: somename.any -> info
func (mw *Middleware) NameServiceResolveName(ctx context.Context, req *pb.RpcNameServiceResolveNameRequest) *pb.RpcNameServiceResolveNameResponse {
ns := getService[nameserviceclient.AnyNsClientService](mw)
var in proto.NameAvailableRequest
in.FullName = req.FullName
nar, err := ns.IsNameAvailable(ctx, &in)
if err != nil {
return &pb.RpcNameServiceResolveNameResponse{
Error: &pb.RpcNameServiceResolveNameResponseError{
// we don't map error codes here
Code: pb.RpcNameServiceResolveNameResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
// Return the response
var out pb.RpcNameServiceResolveNameResponse
out.Available = nar.Available
out.OwnerAnyAddress = nar.OwnerAnyAddress
// EOA is onwer of -> SCW is owner of -> name
out.OwnerEthAddress = nar.OwnerEthAddress
out.OwnerScwEthAddress = nar.OwnerScwEthAddress
out.SpaceId = nar.SpaceId
out.NameExpires = nar.NameExpires
return &out
}
func (mw *Middleware) NameServiceResolveAnyId(ctx context.Context, req *pb.RpcNameServiceResolveAnyIdRequest) *pb.RpcNameServiceResolveAnyIdResponse {
// Get name service object that connects to the remote "namingNode"
// in order for that to work, we need to have a "namingNode" node in the nodes section of the config
ns := getService[nameserviceclient.AnyNsClientService](mw)
var in proto.NameByAnyIdRequest
in.AnyAddress = req.AnyId
nar, err := ns.GetNameByAnyId(ctx, &in)
if err != nil {
return &pb.RpcNameServiceResolveAnyIdResponse{
Error: &pb.RpcNameServiceResolveAnyIdResponseError{
// we don't map error codes here
Code: pb.RpcNameServiceResolveAnyIdResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
// Return the response
var out pb.RpcNameServiceResolveAnyIdResponse
out.Found = nar.Found
out.FullName = nar.Name
return &out
}
func (mw *Middleware) NameServiceResolveSpaceId(ctx context.Context, req *pb.RpcNameServiceResolveSpaceIdRequest) *pb.RpcNameServiceResolveSpaceIdResponse {
// TODO: implement
// TODO: test
return &pb.RpcNameServiceResolveSpaceIdResponse{
Error: &pb.RpcNameServiceResolveSpaceIdResponseError{
Code: pb.RpcNameServiceResolveSpaceIdResponseError_UNKNOWN_ERROR,
Description: "not implemented",
},
}
}
func (mw *Middleware) NameServiceUserAccountGet(ctx context.Context, req *pb.RpcNameServiceUserAccountGetRequest) *pb.RpcNameServiceUserAccountGetResponse {
// 1 - get name service object that connects to the remote "namingNode"
// in order for that to work, we need to have a "namingNode" node in the nodes section of the config
ns := getService[nameserviceclient.AnyNsClientService](mw)
// 2 - get user's ETH address from the wallet
w := getService[wallet.Wallet](mw)
// 3 - get user's account info
//
// when AccountAbstraction is used to deploy a smart contract wallet
// then name is really owned by this SCW, but owner of this SCW is
// EOA that was used to sign transaction
//
// EOA (w.GetAccountEthAddress()) -> SCW (ua.OwnerSmartContracWalletAddress) -> name
var guar proto.GetUserAccountRequest
guar.OwnerEthAddress = w.GetAccountEthAddress().Hex()
ua, err := ns.GetUserAccount(ctx, &guar)
if err != nil {
return &pb.RpcNameServiceUserAccountGetResponse{
Error: &pb.RpcNameServiceUserAccountGetResponseError{
Code: pb.RpcNameServiceUserAccountGetResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
// 4 - check if any name is attached to the account (reverse resolve the name)
var in proto.NameByAddressRequest
// NOTE: we are passing here SCW address, not initial ETH address!
// read comment about SCW above please
in.OwnerScwEthAddress = ua.OwnerSmartContracWalletAddress
nar, err := ns.GetNameByAddress(ctx, &in)
if err != nil {
return &pb.RpcNameServiceUserAccountGetResponse{
Error: &pb.RpcNameServiceUserAccountGetResponseError{
// we don't map error codes here
Code: pb.RpcNameServiceUserAccountGetResponseError_BAD_NAME_RESOLVE,
Description: err.Error(),
},
}
}
// Return the response
var out pb.RpcNameServiceUserAccountGetResponse
out.NamesCountLeft = ua.NamesCountLeft
out.OperationsCountLeft = ua.OperationsCountLeft
// not checking nar.Found here, no need
out.AnyNameAttached = nar.Name
return &out
}

144
core/payments.go Normal file
View file

@ -0,0 +1,144 @@
package core
import (
"context"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/payments"
"github.com/anyproto/anytype-heart/pb"
)
func (mw *Middleware) MembershipGetStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) *pb.RpcMembershipGetStatusResponse {
log.Info("payments - client asked to get a subscription status", zap.Any("req", req))
ps := getService[payments.Service](mw)
out, err := ps.GetSubscriptionStatus(ctx, req)
if err != nil {
return &pb.RpcMembershipGetStatusResponse{
Error: &pb.RpcMembershipGetStatusResponseError{
Code: pb.RpcMembershipGetStatusResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipIsNameValid(ctx context.Context, req *pb.RpcMembershipIsNameValidRequest) *pb.RpcMembershipIsNameValidResponse {
ps := getService[payments.Service](mw)
out, err := ps.IsNameValid(ctx, req)
// something bad has happened
if err != nil {
return &pb.RpcMembershipIsNameValidResponse{
Error: &pb.RpcMembershipIsNameValidResponseError{
Code: pb.RpcMembershipIsNameValidResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
// out.Error will contain validation error if something is wrong with the name
return out
}
func (mw *Middleware) MembershipGetPaymentUrl(ctx context.Context, req *pb.RpcMembershipGetPaymentUrlRequest) *pb.RpcMembershipGetPaymentUrlResponse {
ps := getService[payments.Service](mw)
out, err := ps.GetPaymentURL(ctx, req)
log.Error("payments - client asked to get a payment url", zap.Any("req", req), zap.Any("out", out))
if err != nil {
return &pb.RpcMembershipGetPaymentUrlResponse{
Error: &pb.RpcMembershipGetPaymentUrlResponseError{
Code: pb.RpcMembershipGetPaymentUrlResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipGetPortalLinkUrl(ctx context.Context, req *pb.RpcMembershipGetPortalLinkUrlRequest) *pb.RpcMembershipGetPortalLinkUrlResponse {
ps := getService[payments.Service](mw)
out, err := ps.GetPortalLink(ctx, req)
if err != nil {
return &pb.RpcMembershipGetPortalLinkUrlResponse{
Error: &pb.RpcMembershipGetPortalLinkUrlResponseError{
Code: pb.RpcMembershipGetPortalLinkUrlResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipGetVerificationEmail(ctx context.Context, req *pb.RpcMembershipGetVerificationEmailRequest) *pb.RpcMembershipGetVerificationEmailResponse {
ps := getService[payments.Service](mw)
out, err := ps.GetVerificationEmail(ctx, req)
if err != nil {
return &pb.RpcMembershipGetVerificationEmailResponse{
Error: &pb.RpcMembershipGetVerificationEmailResponseError{
Code: pb.RpcMembershipGetVerificationEmailResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipVerifyEmailCode(ctx context.Context, req *pb.RpcMembershipVerifyEmailCodeRequest) *pb.RpcMembershipVerifyEmailCodeResponse {
ps := getService[payments.Service](mw)
out, err := ps.VerifyEmailCode(ctx, req)
if err != nil {
return &pb.RpcMembershipVerifyEmailCodeResponse{
Error: &pb.RpcMembershipVerifyEmailCodeResponseError{
Code: pb.RpcMembershipVerifyEmailCodeResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipFinalize(ctx context.Context, req *pb.RpcMembershipFinalizeRequest) *pb.RpcMembershipFinalizeResponse {
ps := getService[payments.Service](mw)
out, err := ps.FinalizeSubscription(ctx, req)
if err != nil {
return &pb.RpcMembershipFinalizeResponse{
Error: &pb.RpcMembershipFinalizeResponseError{
Code: pb.RpcMembershipFinalizeResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}
func (mw *Middleware) MembershipGetTiers(ctx context.Context, req *pb.RpcMembershipTiersGetRequest) *pb.RpcMembershipTiersGetResponse {
ps := getService[payments.Service](mw)
out, err := ps.GetTiers(ctx, req)
if err != nil {
return &pb.RpcMembershipTiersGetResponse{
Error: &pb.RpcMembershipTiersGetResponseError{
Code: pb.RpcMembershipTiersGetResponseError_UNKNOWN_ERROR,
Description: err.Error(),
},
}
}
return out
}

294
core/payments/cache/cache.go vendored Normal file
View file

@ -0,0 +1,294 @@
package cache
import (
"context"
"encoding/json"
"errors"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/dgraph-io/badger/v4"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
)
const CName = "cache"
var log = logger.NewNamed(CName)
var (
ErrCacheDbError = errors.New("cache db error")
ErrUnsupportedCacheVersion = errors.New("unsupported cache version")
ErrCacheDisabled = errors.New("cache is disabled")
ErrCacheExpired = errors.New("cache is empty")
)
const dbKey = "payments/subscription/v1"
type StorageStructV1 struct {
// to migrate old storage to new format
CurrentVersion uint16
// this variable is just for info
LastUpdated time.Time
// depending on the type of the subscription the cache will have different lifetime
// if current time is >= ExpireTime -> cache is expired
ExpireTime time.Time
// if this is 0 - then cache is enabled
DisableUntilTime time.Time
// v1 of the actual data
SubscriptionStatus pb.RpcMembershipGetStatusResponse
TiersData pb.RpcMembershipTiersGetResponse
}
func newStorageStructV1() *StorageStructV1 {
return &StorageStructV1{
CurrentVersion: 1,
LastUpdated: time.Now().UTC(),
ExpireTime: time.Time{},
DisableUntilTime: time.Time{},
SubscriptionStatus: pb.RpcMembershipGetStatusResponse{
Data: nil,
},
TiersData: pb.RpcMembershipTiersGetResponse{
Tiers: nil,
},
}
}
type CacheService interface {
// if cache is disabled -> will return objects and ErrCacheDisabled
// if cache is expired -> will return objects and ErrCacheExpired
CacheGet() (status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, err error)
// if cache is disabled -> will return no error
// if cache is expired -> will return no error
// status or tiers can be nil depending on what you want to update
CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, ExpireTime time.Time) (err error)
IsCacheEnabled() (enabled bool)
// if already enabled -> will not return error
CacheEnable() (err error)
// if already disabled -> will not return error
// if currently disabled -> will disable GETs for next N minutes
CacheDisableForNextMinutes(minutes int) (err error)
// does not take into account if cache is enabled or not, erases always
CacheClear() (err error)
app.Component
}
func New() CacheService {
return &cacheservice{}
}
type cacheservice struct {
dbProvider datastore.Datastore
db *badger.DB
m sync.Mutex
}
func (s *cacheservice) Name() (name string) {
return CName
}
func (s *cacheservice) Init(a *app.App) (err error) {
s.dbProvider = app.MustComponent[datastore.Datastore](a)
db, err := s.dbProvider.LocalStorage()
if err != nil {
return err
}
s.db = db
return nil
}
func (s *cacheservice) Run(_ context.Context) (err error) {
return nil
}
func (s *cacheservice) Close(_ context.Context) (err error) {
return s.db.Close()
}
func (s *cacheservice) CacheGet() (status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, err error) {
// 1 - check in storage
ss, err := s.get()
if err != nil {
log.Error("can not get subscription status from cache", zap.Error(err))
return nil, nil, ErrCacheDbError
}
if ss.CurrentVersion != 1 {
// currently we have only one version, but in future we can have more
// this error can happen if you "downgrade" the app
log.Error("unsupported cache version", zap.Uint16("version", ss.CurrentVersion))
return nil, nil, ErrUnsupportedCacheVersion
}
// 2 - check if cache is disabled
if !s.IsCacheEnabled() {
// return object too
return &ss.SubscriptionStatus, &ss.TiersData, ErrCacheDisabled
}
// 3 - check if cache is outdated
if time.Now().UTC().After(ss.ExpireTime) {
// return object too
return &ss.SubscriptionStatus, &ss.TiersData, ErrCacheExpired
}
// 4 - return value
return &ss.SubscriptionStatus, &ss.TiersData, nil
}
func (s *cacheservice) CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expireTime time.Time) (err error) {
// 1 - get existing storage
ss, err := s.get()
if err != nil {
// if there is no record in the cache, let's create it
ss = newStorageStructV1()
}
// 2 - update storage
if status != nil {
ss.SubscriptionStatus = *status
}
if tiers != nil {
ss.TiersData = *tiers
}
ss.ExpireTime = expireTime
// 3 - save to storage
return s.set(ss)
}
func (s *cacheservice) IsCacheEnabled() (enabled bool) {
// 1 - get existing storage
ss, err := s.get()
if err != nil {
return true
}
// 2 - check if cache is disabled
if (ss.DisableUntilTime != time.Time{}) && time.Now().UTC().Before(ss.DisableUntilTime) {
return false
}
return true
}
// will not return error if already enabled
func (s *cacheservice) CacheEnable() (err error) {
// 1 - get existing storage
ss, err := s.get()
if err != nil {
// if there is no record in the cache, let's create it
ss = newStorageStructV1()
}
// 2 - update storage
ss.DisableUntilTime = time.Time{}
// 3 - save to storage
err = s.set(ss)
if err != nil {
return ErrCacheDbError
}
return nil
}
// will not return error if already disabled
// if currently disabled - will disable for next N minutes
func (s *cacheservice) CacheDisableForNextMinutes(minutes int) (err error) {
// 1 - get existing storage
ss, err := s.get()
if err != nil {
// if there is no record in the cache, let's create it
ss = newStorageStructV1()
}
// 2 - update storage
ss.DisableUntilTime = time.Now().UTC().Add(time.Minute * time.Duration(minutes))
// 3 - save to storage
err = s.set(ss)
if err != nil {
return ErrCacheDbError
}
return nil
}
// does not take into account if cache is enabled or not, erases always
func (s *cacheservice) CacheClear() (err error) {
// 1 - get existing storage
_, err = s.get()
if err != nil {
// no error if there is no record in the cache
return nil
}
// 2 - update storage
ss := newStorageStructV1()
// 3 - save to storage
err = s.set(ss)
if err != nil {
return ErrCacheDbError
}
return nil
}
func (s *cacheservice) get() (out *StorageStructV1, err error) {
if s.db == nil {
return nil, errors.New("db is not initialized")
}
s.m.Lock()
defer s.m.Unlock()
var ss StorageStructV1
err = s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(dbKey))
if err != nil {
return err
}
return item.Value(func(val []byte) error {
// convert value to out
return json.Unmarshal(val, &ss)
})
})
out = &ss
return out, err
}
func (s *cacheservice) set(in *StorageStructV1) (err error) {
s.m.Lock()
defer s.m.Unlock()
return s.db.Update(func(txn *badger.Txn) error {
// convert
bytes, err := json.Marshal(*in)
if err != nil {
return err
}
return txn.Set([]byte(dbKey), bytes)
})
}

384
core/payments/cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,384 @@
package cache
import (
"context"
"testing"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/dgraph-io/badger/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var ctx = context.Background()
type fixture struct {
a *app.App
*cacheservice
}
func newFixture(t *testing.T) *fixture {
fx := &fixture{
a: new(app.App),
cacheservice: New().(*cacheservice),
}
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
require.NoError(t, err)
fx.db = db
//fx.a.Register(fx.ts)
require.NoError(t, fx.a.Start(ctx))
return fx
}
func (fx *fixture) finish(t *testing.T) {
assert.NoError(t, fx.a.Close(ctx))
//assert.NoError(t, fx.db.Close())
}
func TestPayments_EnableCache(t *testing.T) {
t.Run("should succeed", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheEnable()
require.NoError(t, err)
})
t.Run("should succeed even when called twice", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheEnable()
require.NoError(t, err)
err = fx.CacheEnable()
require.NoError(t, err)
})
}
func TestPayments_DisableCache(t *testing.T) {
t.Run("should succeed with 0", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheDisableForNextMinutes(0)
require.NoError(t, err)
})
t.Run("should succeed even when called twice", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheDisableForNextMinutes(60)
require.NoError(t, err)
err = fx.CacheDisableForNextMinutes(40)
require.NoError(t, err)
})
t.Run("clear cache should remove disabling", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheDisableForNextMinutes(60)
require.NoError(t, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheDisabled, err)
err = fx.CacheClear()
require.NoError(t, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheExpired, err)
})
}
func TestPayments_ClearCache(t *testing.T) {
t.Run("should succeed even if no cache in the DB", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheClear()
require.NoError(t, err)
})
t.Run("should succeed even when called twice", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheClear()
require.NoError(t, err)
err = fx.CacheClear()
require.NoError(t, err)
})
t.Run("should succeed when cache is disabled", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheDisableForNextMinutes(60)
require.NoError(t, err)
err = fx.CacheClear()
require.NoError(t, err)
})
t.Run("should succeed when cache is in DB", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err := fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours,
)
require.NoError(t, err)
err = fx.CacheClear()
require.NoError(t, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheExpired, err)
})
}
func TestPayments_CacheGetSubscriptionStatus(t *testing.T) {
t.Run("should fail if no record in the DB", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
_, _, err := fx.CacheGet()
require.Equal(t, ErrCacheDbError, err)
})
t.Run("should succeed", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err := fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.NoError(t, err)
out, _, err := fx.CacheGet()
require.NoError(t, err)
require.Equal(t, int32(model.Membership_TierExplorer), out.Data.Tier)
require.Equal(t, model.Membership_StatusActive, out.Data.Status)
err = fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
// here
Status: model.Membership_StatusUnknown,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.NoError(t, err)
out, _, err = fx.CacheGet()
require.NoError(t, err)
require.Equal(t, int32(model.Membership_TierExplorer), out.Data.Tier)
require.Equal(t, model.Membership_StatusUnknown, out.Data.Status)
})
t.Run("should return object and error if cache is disabled", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
en := fx.IsCacheEnabled()
require.Equal(t, true, en)
err := fx.CacheDisableForNextMinutes(10)
require.NoError(t, err)
en = fx.IsCacheEnabled()
require.Equal(t, false, en)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err = fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.NoError(t, err)
out, _, err := fx.CacheGet()
require.Equal(t, ErrCacheDisabled, err)
// HERE: weird semantics, error is returned too :-)
require.Equal(t, int32(model.Membership_TierExplorer), out.Data.Tier)
err = fx.CacheEnable()
require.NoError(t, err)
en = fx.IsCacheEnabled()
require.Equal(t, true, en)
out, _, err = fx.CacheGet()
require.NoError(t, err)
require.Equal(t, int32(model.Membership_TierExplorer), out.Data.Tier)
})
t.Run("should return error if cache is cleared", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err := fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.NoError(t, err)
err = fx.CacheClear()
require.NoError(t, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheExpired, err)
})
}
func TestPayments_CacheSetSubscriptionStatus(t *testing.T) {
t.Run("should succeed if no record was in the DB", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err := fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.Equal(t, nil, err)
out, _, err := fx.CacheGet()
require.NoError(t, err)
require.Equal(t, int32(model.Membership_TierExplorer), out.Data.Tier)
require.Equal(t, model.Membership_StatusActive, out.Data.Status)
})
t.Run("should succeed if cache is disabled", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheDisableForNextMinutes(10)
require.NoError(t, err)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err = fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.Equal(t, nil, err)
})
t.Run("should succeed if cache is cleared", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheClear()
require.NoError(t, err)
timeNow := time.Now().UTC()
timePlus5Hours := timeNow.Add(5 * time.Hour)
err = fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timePlus5Hours)
require.Equal(t, nil, err)
})
t.Run("should succeed if expire is set to 0", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
err := fx.CacheClear()
require.NoError(t, err)
timeNull := time.Time{}
err = fx.CacheSet(&pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: int32(model.Membership_TierExplorer),
Status: model.Membership_StatusActive,
},
},
&pb.RpcMembershipTiersGetResponse{
Tiers: []*model.MembershipTierData{},
},
timeNull)
require.Equal(t, nil, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheExpired, err)
})
}

View file

@ -0,0 +1,426 @@
// Code generated by mockery. DO NOT EDIT.
package mock_cache
import (
app "github.com/anyproto/any-sync/app"
mock "github.com/stretchr/testify/mock"
pb "github.com/anyproto/anytype-heart/pb"
time "time"
)
// MockCacheService is an autogenerated mock type for the CacheService type
type MockCacheService struct {
mock.Mock
}
type MockCacheService_Expecter struct {
mock *mock.Mock
}
func (_m *MockCacheService) EXPECT() *MockCacheService_Expecter {
return &MockCacheService_Expecter{mock: &_m.Mock}
}
// CacheClear provides a mock function with given fields:
func (_m *MockCacheService) CacheClear() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for CacheClear")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCacheService_CacheClear_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheClear'
type MockCacheService_CacheClear_Call struct {
*mock.Call
}
// CacheClear is a helper method to define mock.On call
func (_e *MockCacheService_Expecter) CacheClear() *MockCacheService_CacheClear_Call {
return &MockCacheService_CacheClear_Call{Call: _e.mock.On("CacheClear")}
}
func (_c *MockCacheService_CacheClear_Call) Run(run func()) *MockCacheService_CacheClear_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCacheService_CacheClear_Call) Return(err error) *MockCacheService_CacheClear_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockCacheService_CacheClear_Call) RunAndReturn(run func() error) *MockCacheService_CacheClear_Call {
_c.Call.Return(run)
return _c
}
// CacheDisableForNextMinutes provides a mock function with given fields: minutes
func (_m *MockCacheService) CacheDisableForNextMinutes(minutes int) error {
ret := _m.Called(minutes)
if len(ret) == 0 {
panic("no return value specified for CacheDisableForNextMinutes")
}
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(minutes)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCacheService_CacheDisableForNextMinutes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheDisableForNextMinutes'
type MockCacheService_CacheDisableForNextMinutes_Call struct {
*mock.Call
}
// CacheDisableForNextMinutes is a helper method to define mock.On call
// - minutes int
func (_e *MockCacheService_Expecter) CacheDisableForNextMinutes(minutes interface{}) *MockCacheService_CacheDisableForNextMinutes_Call {
return &MockCacheService_CacheDisableForNextMinutes_Call{Call: _e.mock.On("CacheDisableForNextMinutes", minutes)}
}
func (_c *MockCacheService_CacheDisableForNextMinutes_Call) Run(run func(minutes int)) *MockCacheService_CacheDisableForNextMinutes_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockCacheService_CacheDisableForNextMinutes_Call) Return(err error) *MockCacheService_CacheDisableForNextMinutes_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockCacheService_CacheDisableForNextMinutes_Call) RunAndReturn(run func(int) error) *MockCacheService_CacheDisableForNextMinutes_Call {
_c.Call.Return(run)
return _c
}
// CacheEnable provides a mock function with given fields:
func (_m *MockCacheService) CacheEnable() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for CacheEnable")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCacheService_CacheEnable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheEnable'
type MockCacheService_CacheEnable_Call struct {
*mock.Call
}
// CacheEnable is a helper method to define mock.On call
func (_e *MockCacheService_Expecter) CacheEnable() *MockCacheService_CacheEnable_Call {
return &MockCacheService_CacheEnable_Call{Call: _e.mock.On("CacheEnable")}
}
func (_c *MockCacheService_CacheEnable_Call) Run(run func()) *MockCacheService_CacheEnable_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCacheService_CacheEnable_Call) Return(err error) *MockCacheService_CacheEnable_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockCacheService_CacheEnable_Call) RunAndReturn(run func() error) *MockCacheService_CacheEnable_Call {
_c.Call.Return(run)
return _c
}
// CacheGet provides a mock function with given fields:
func (_m *MockCacheService) CacheGet() (*pb.RpcMembershipGetStatusResponse, *pb.RpcMembershipTiersGetResponse, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for CacheGet")
}
var r0 *pb.RpcMembershipGetStatusResponse
var r1 *pb.RpcMembershipTiersGetResponse
var r2 error
if rf, ok := ret.Get(0).(func() (*pb.RpcMembershipGetStatusResponse, *pb.RpcMembershipTiersGetResponse, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *pb.RpcMembershipGetStatusResponse); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*pb.RpcMembershipGetStatusResponse)
}
}
if rf, ok := ret.Get(1).(func() *pb.RpcMembershipTiersGetResponse); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*pb.RpcMembershipTiersGetResponse)
}
}
if rf, ok := ret.Get(2).(func() error); ok {
r2 = rf()
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockCacheService_CacheGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheGet'
type MockCacheService_CacheGet_Call struct {
*mock.Call
}
// CacheGet is a helper method to define mock.On call
func (_e *MockCacheService_Expecter) CacheGet() *MockCacheService_CacheGet_Call {
return &MockCacheService_CacheGet_Call{Call: _e.mock.On("CacheGet")}
}
func (_c *MockCacheService_CacheGet_Call) Run(run func()) *MockCacheService_CacheGet_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCacheService_CacheGet_Call) Return(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, err error) *MockCacheService_CacheGet_Call {
_c.Call.Return(status, tiers, err)
return _c
}
func (_c *MockCacheService_CacheGet_Call) RunAndReturn(run func() (*pb.RpcMembershipGetStatusResponse, *pb.RpcMembershipTiersGetResponse, error)) *MockCacheService_CacheGet_Call {
_c.Call.Return(run)
return _c
}
// CacheSet provides a mock function with given fields: status, tiers, ExpireTime
func (_m *MockCacheService) CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, ExpireTime time.Time) error {
ret := _m.Called(status, tiers, ExpireTime)
if len(ret) == 0 {
panic("no return value specified for CacheSet")
}
var r0 error
if rf, ok := ret.Get(0).(func(*pb.RpcMembershipGetStatusResponse, *pb.RpcMembershipTiersGetResponse, time.Time) error); ok {
r0 = rf(status, tiers, ExpireTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCacheService_CacheSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheSet'
type MockCacheService_CacheSet_Call struct {
*mock.Call
}
// CacheSet is a helper method to define mock.On call
// - status *pb.RpcMembershipGetStatusResponse
// - tiers *pb.RpcMembershipTiersGetResponse
// - ExpireTime time.Time
func (_e *MockCacheService_Expecter) CacheSet(status interface{}, tiers interface{}, ExpireTime interface{}) *MockCacheService_CacheSet_Call {
return &MockCacheService_CacheSet_Call{Call: _e.mock.On("CacheSet", status, tiers, ExpireTime)}
}
func (_c *MockCacheService_CacheSet_Call) Run(run func(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, ExpireTime time.Time)) *MockCacheService_CacheSet_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*pb.RpcMembershipGetStatusResponse), args[1].(*pb.RpcMembershipTiersGetResponse), args[2].(time.Time))
})
return _c
}
func (_c *MockCacheService_CacheSet_Call) Return(err error) *MockCacheService_CacheSet_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockCacheService_CacheSet_Call) RunAndReturn(run func(*pb.RpcMembershipGetStatusResponse, *pb.RpcMembershipTiersGetResponse, time.Time) error) *MockCacheService_CacheSet_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function with given fields: a
func (_m *MockCacheService) Init(a *app.App) error {
ret := _m.Called(a)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
r0 = rf(a)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCacheService_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type MockCacheService_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - a *app.App
func (_e *MockCacheService_Expecter) Init(a interface{}) *MockCacheService_Init_Call {
return &MockCacheService_Init_Call{Call: _e.mock.On("Init", a)}
}
func (_c *MockCacheService_Init_Call) Run(run func(a *app.App)) *MockCacheService_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*app.App))
})
return _c
}
func (_c *MockCacheService_Init_Call) Return(err error) *MockCacheService_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockCacheService_Init_Call) RunAndReturn(run func(*app.App) error) *MockCacheService_Init_Call {
_c.Call.Return(run)
return _c
}
// IsCacheEnabled provides a mock function with given fields:
func (_m *MockCacheService) IsCacheEnabled() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsCacheEnabled")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockCacheService_IsCacheEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCacheEnabled'
type MockCacheService_IsCacheEnabled_Call struct {
*mock.Call
}
// IsCacheEnabled is a helper method to define mock.On call
func (_e *MockCacheService_Expecter) IsCacheEnabled() *MockCacheService_IsCacheEnabled_Call {
return &MockCacheService_IsCacheEnabled_Call{Call: _e.mock.On("IsCacheEnabled")}
}
func (_c *MockCacheService_IsCacheEnabled_Call) Run(run func()) *MockCacheService_IsCacheEnabled_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCacheService_IsCacheEnabled_Call) Return(enabled bool) *MockCacheService_IsCacheEnabled_Call {
_c.Call.Return(enabled)
return _c
}
func (_c *MockCacheService_IsCacheEnabled_Call) RunAndReturn(run func() bool) *MockCacheService_IsCacheEnabled_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with given fields:
func (_m *MockCacheService) Name() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockCacheService_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockCacheService_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockCacheService_Expecter) Name() *MockCacheService_Name_Call {
return &MockCacheService_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockCacheService_Name_Call) Run(run func()) *MockCacheService_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCacheService_Name_Call) Return(name string) *MockCacheService_Name_Call {
_c.Call.Return(name)
return _c
}
func (_c *MockCacheService_Name_Call) RunAndReturn(run func() string) *MockCacheService_Name_Call {
_c.Call.Return(run)
return _c
}
// NewMockCacheService creates a new instance of MockCacheService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockCacheService(t interface {
mock.TestingT
Cleanup(func())
}) *MockCacheService {
mock := &MockCacheService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

696
core/payments/payments.go Normal file
View file

@ -0,0 +1,696 @@
package payments
import (
"context"
"errors"
"sync"
"time"
"unicode/utf8"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/anyproto/any-sync/util/periodicsync"
"go.uber.org/zap"
ppclient "github.com/anyproto/any-sync/paymentservice/paymentserviceclient"
psp "github.com/anyproto/any-sync/paymentservice/paymentserviceproto"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/payments/cache"
"github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const CName = "payments"
var log = logging.Logger(CName).Desugar()
const (
refreshIntervalSecs = 10
timeout = 10 * time.Second
initialStatus = -1
)
type globalNamesUpdater interface {
UpdateGlobalNames()
}
/*
CACHE LOGICS:
1. User installs Anytype
-> cache is clean
2. client gets his subscription from MW
- if cache is disabled and 30 minutes elapsed
-> enable cache again
- if cache is disabled or cache is clean or cache is expired
-> ask from PP node, then save to cache:
x if got no info -> cache it for N days
x if got into without expiration -> cache it for N days
x if got info -> cache it for until it expires
x if cache was disabled before and tier has changed or status is active -> enable cache again
x if can not connect to PP node -> return error
x if can not write to cache -> return error
- if we have it in cache
-> return from cache
3. User clicks on a Pay by card/crypto or Manage button:
-> disable cache for 30 minutes (so we always get from PP node)
4. User confirms his e-mail code
-> clear cache (it will cause getting again from PP node next)
*/
type Service interface {
GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) (*pb.RpcMembershipGetStatusResponse, error)
IsNameValid(ctx context.Context, req *pb.RpcMembershipIsNameValidRequest) (*pb.RpcMembershipIsNameValidResponse, error)
GetPaymentURL(ctx context.Context, req *pb.RpcMembershipGetPaymentUrlRequest) (*pb.RpcMembershipGetPaymentUrlResponse, error)
GetPortalLink(ctx context.Context, req *pb.RpcMembershipGetPortalLinkUrlRequest) (*pb.RpcMembershipGetPortalLinkUrlResponse, error)
GetVerificationEmail(ctx context.Context, req *pb.RpcMembershipGetVerificationEmailRequest) (*pb.RpcMembershipGetVerificationEmailResponse, error)
VerifyEmailCode(ctx context.Context, req *pb.RpcMembershipVerifyEmailCodeRequest) (*pb.RpcMembershipVerifyEmailCodeResponse, error)
FinalizeSubscription(ctx context.Context, req *pb.RpcMembershipFinalizeRequest) (*pb.RpcMembershipFinalizeResponse, error)
GetTiers(ctx context.Context, req *pb.RpcMembershipTiersGetRequest) (*pb.RpcMembershipTiersGetResponse, error)
app.ComponentRunnable
}
func New() Service {
return &service{}
}
type service struct {
cache cache.CacheService
ppclient ppclient.AnyPpClientService
wallet wallet.Wallet
mx sync.Mutex
periodicGetStatus periodicsync.PeriodicSync
eventSender event.Sender
profileUpdater globalNamesUpdater
}
func (s *service) Name() (name string) {
return CName
}
func (s *service) Init(a *app.App) (err error) {
s.cache = app.MustComponent[cache.CacheService](a)
s.ppclient = app.MustComponent[ppclient.AnyPpClientService](a)
s.wallet = app.MustComponent[wallet.Wallet](a)
s.eventSender = app.MustComponent[event.Sender](a)
s.periodicGetStatus = periodicsync.NewPeriodicSync(refreshIntervalSecs, timeout, s.getPeriodicStatus, logger.CtxLogger{Logger: log})
s.profileUpdater = app.MustComponent[globalNamesUpdater](a)
return nil
}
func (s *service) Run(ctx context.Context) (err error) {
// skip running loop if called from tests
val := ctx.Value("dontRunPeriodicGetStatus")
if val != nil && val.(bool) {
return nil
}
s.periodicGetStatus.Run()
return nil
}
func (s *service) Close(_ context.Context) (err error) {
s.periodicGetStatus.Close()
return nil
}
func (s *service) getPeriodicStatus(ctx context.Context) error {
// get subscription status (from cache or from PP node)
// if status is changed -> it will send an event
log.Debug("periodic: getting subscription status from cache/PP node")
_, err := s.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{})
return err
}
func (s *service) sendEvent(status *pb.RpcMembershipGetStatusResponse) {
s.eventSender.Broadcast(&pb.Event{
Messages: []*pb.EventMessage{
{
Value: &pb.EventMessageValueOfMembershipUpdate{
MembershipUpdate: &pb.EventMembershipUpdate{
Data: status.Data,
},
},
},
},
})
}
func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) (*pb.RpcMembershipGetStatusResponse, error) {
s.mx.Lock()
defer s.mx.Unlock()
ownerID := s.wallet.Account().SignKey.GetPublic().Account()
privKey := s.wallet.GetAccountPrivkey()
// 1 - check in cache
// tiers var. is unused here
cachedStatus, _, err := s.cache.CacheGet()
// if NoCache -> skip returning from cache
if !req.NoCache && (err == nil) && (cachedStatus != nil) && (cachedStatus.Data != nil) {
log.Debug("returning subscription status from cache", zap.Error(err), zap.Any("cachedStatus", cachedStatus))
return cachedStatus, nil
}
// 2 - send request to PP node
gsr := psp.GetSubscriptionRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyID: ownerID,
}
payload, err := gsr.Marshal()
if err != nil {
return nil, err
}
// this is the SignKey
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.GetSubscriptionRequestSigned{
Payload: payload,
Signature: signature,
}
log.Debug("get sub from PP node", zap.Any("cachedStatus", cachedStatus), zap.Bool("noCache", req.NoCache))
status, err := s.ppclient.GetSubscriptionStatus(ctx, &reqSigned)
if err != nil {
log.Info("creating empty subscription in cache because can not get subscription status from payment node")
// eat error and create empty status ("no tier") so that we will then save it to the cache
status = &psp.GetSubscriptionResponse{
Tier: int32(psp.SubscriptionTier_TierUnknown),
Status: psp.SubscriptionStatus_StatusUnknown,
}
}
out := pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{},
}
out.Data.Tier = status.Tier
out.Data.Status = model.MembershipStatus(status.Status)
out.Data.DateStarted = status.DateStarted
out.Data.DateEnds = status.DateEnds
out.Data.IsAutoRenew = status.IsAutoRenew
out.Data.PaymentMethod = model.MembershipPaymentMethod(status.PaymentMethod)
out.Data.RequestedAnyName = status.RequestedAnyName
out.Data.UserEmail = status.UserEmail
out.Data.SubscribeToNewsletter = status.SubscribeToNewsletter
// 3 - save into cache
// truncate nseconds here
var cacheExpireTime time.Time = time.Unix(int64(status.DateEnds), 0)
isExpired := time.Now().UTC().After(cacheExpireTime)
// if subscription DateEns is null - then default expire time is in 10 days
// or until user clicks on a “Pay by card/crypto” or “Manage” button
if status.DateEnds == 0 || isExpired {
log.Debug("setting cache to +1 day because subscription is isExpired")
timeNow := time.Now().UTC()
cacheExpireTime = timeNow.Add(1 * 24 * time.Hour)
}
// update only status, not tiers
err = s.cache.CacheSet(&out, nil, cacheExpireTime)
if err != nil {
return nil, err
}
isDiffTier := (cachedStatus != nil) && (cachedStatus.Data.Tier != status.Tier)
isDiffStatus := (cachedStatus != nil) && (cachedStatus.Data.Status != model.MembershipStatus(status.Status))
isDiffRequestedName := (cachedStatus != nil) && (cachedStatus.Data.RequestedAnyName != status.RequestedAnyName)
log.Debug("subscription status", zap.Any("from server", status), zap.Any("cached", cachedStatus))
// 4 - if cache was disabled but the tier is different or status is active
if cachedStatus == nil || (isDiffTier || isDiffStatus) {
log.Info("subscription status has changed. sending EventMembershipUpdate",
zap.Bool("cache was empty", cachedStatus == nil),
zap.Bool("isDiffTier", isDiffTier),
zap.Bool("isDiffStatus", isDiffStatus),
)
// 4.1 - send the event
s.sendEvent(&out)
// 4.2 - enable cache again (we have received new data)
log.Info("enabling cache again")
// or it will be automatically enabled after N minutes of DisableForNextMinutes() call
err := s.cache.CacheEnable()
if err != nil {
return nil, err
}
}
// 5 - if requested any name has changed, then we need to update details of local identity
if isDiffRequestedName {
s.profileUpdater.UpdateGlobalNames()
}
return &out, nil
}
func (s *service) IsNameValid(ctx context.Context, req *pb.RpcMembershipIsNameValidRequest) (*pb.RpcMembershipIsNameValidResponse, error) {
var code psp.IsNameValidResponse_Code
var desc string
out := pb.RpcMembershipIsNameValidResponse{}
/*
// 1 - send request to PP node and ask her please
invr := psp.IsNameValidRequest{
// payment node will check if signature matches with this OwnerAnyID
RequestedTier: req.RequestedTier,
RequestedAnyName: req.RequestedAnyName,
}
resp, err := s.ppclient.IsNameValid(ctx, &invr)
if err != nil {
return nil, err
}
if resp.Code == psp.IsNameValidResponse_Valid {
// no error
return &out, nil
}
out.Error = &pb.RpcMembershipIsNameValidResponseError{}
code = resp.Code
desc = resp.Description
*/
// 1 - get all tiers from cache or PP node
tiers, err := s.GetTiers(ctx, &pb.RpcMembershipTiersGetRequest{
NoCache: false,
// TODO: warning! no locale and payment method are passed here!
// Locale: "",
// PaymentMethod: pb.RpcMembershipPaymentMethod_PAYMENT_METHOD_UNKNOWN,
})
if err != nil {
return nil, err
}
if tiers.Tiers == nil {
return nil, errors.New("no tiers received")
}
// find req.RequestedTier
var tier *model.MembershipTierData
for _, t := range tiers.Tiers {
if t.Id == uint32(req.RequestedTier) {
tier = t
break
}
}
if tier == nil {
return nil, errors.New("requested tier not found")
}
code = s.validateAnyName(*tier, req.RequestedAnyName)
if code == psp.IsNameValidResponse_Valid {
// valid
return &out, nil
}
// 2 - convert code to error
out.Error = &pb.RpcMembershipIsNameValidResponseError{}
switch code {
case psp.IsNameValidResponse_NoDotAny:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_BAD_INPUT
out.Error.Description = "No .any at the end of the name"
case psp.IsNameValidResponse_TooShort:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_TOO_SHORT
case psp.IsNameValidResponse_TooLong:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_TOO_LONG
case psp.IsNameValidResponse_HasInvalidChars:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_HAS_INVALID_CHARS
case psp.IsNameValidResponse_TierFeatureNoName:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_TIER_FEATURES_NO_NAME
default:
out.Error.Code = pb.RpcMembershipIsNameValidResponseError_UNKNOWN_ERROR
}
out.Error.Description = desc
return &out, nil
}
func (s *service) validateAnyName(tier model.MembershipTierData, name string) psp.IsNameValidResponse_Code {
if name == "" {
// empty name means we don't want to register name, and this is valid
return psp.IsNameValidResponse_Valid
}
// if name has no .any postfix -> error
if len(name) < 4 || name[len(name)-4:] != ".any" {
return psp.IsNameValidResponse_NoDotAny
}
// for extra safety normalize name here too!
name, err := normalizeAnyName(name)
if err != nil {
log.Debug("can not normalize name", zap.Error(err), zap.String("name", name))
return psp.IsNameValidResponse_HasInvalidChars
}
// remove .any postfix
name = name[:len(name)-4]
// if minLen is zero - means "no check is required"
if tier.AnyNamesCountIncluded == 0 {
return psp.IsNameValidResponse_TierFeatureNoName
}
if tier.AnyNameMinLength == 0 {
return psp.IsNameValidResponse_TierFeatureNoName
}
if uint32(utf8.RuneCountInString(name)) < tier.AnyNameMinLength {
return psp.IsNameValidResponse_TooShort
}
// valid
return psp.IsNameValidResponse_Valid
}
func (s *service) GetPaymentURL(ctx context.Context, req *pb.RpcMembershipGetPaymentUrlRequest) (*pb.RpcMembershipGetPaymentUrlResponse, error) {
// 1 - send request
bsr := psp.BuySubscriptionRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
// not SCW address, but EOA address
// including 0x
OwnerEthAddress: s.wallet.GetAccountEthAddress().Hex(),
RequestedTier: req.RequestedTier,
PaymentMethod: psp.PaymentMethod(req.PaymentMethod),
RequestedAnyName: req.RequestedAnyName,
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.BuySubscriptionRequestSigned{
Payload: payload,
Signature: signature,
}
bsRet, err := s.ppclient.BuySubscription(ctx, &reqSigned)
if err != nil {
return nil, err
}
var out pb.RpcMembershipGetPaymentUrlResponse
out.PaymentUrl = bsRet.PaymentUrl
// 2 - disable cache for 30 minutes
log.Debug("disabling cache for 30 minutes after payment URL was received")
err = s.cache.CacheDisableForNextMinutes(30)
if err != nil {
return nil, err
}
return &out, nil
}
func (s *service) GetPortalLink(ctx context.Context, req *pb.RpcMembershipGetPortalLinkUrlRequest) (*pb.RpcMembershipGetPortalLinkUrlResponse, error) {
// 1 - send request
bsr := psp.GetSubscriptionPortalLinkRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.GetSubscriptionPortalLinkRequestSigned{
Payload: payload,
Signature: signature,
}
bsRet, err := s.ppclient.GetSubscriptionPortalLink(ctx, &reqSigned)
if err != nil {
return nil, err
}
var out pb.RpcMembershipGetPortalLinkUrlResponse
out.PortalUrl = bsRet.PortalUrl
// 2 - disable cache for 30 minutes
log.Debug("disabling cache for 30 minutes after portal link was received")
err = s.cache.CacheDisableForNextMinutes(30)
if err != nil {
return nil, err
}
return &out, nil
}
func (s *service) GetVerificationEmail(ctx context.Context, req *pb.RpcMembershipGetVerificationEmailRequest) (*pb.RpcMembershipGetVerificationEmailResponse, error) {
// 1 - send request
bsr := psp.GetVerificationEmailRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
Email: req.Email,
SubscribeToNewsletter: req.SubscribeToNewsletter,
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.GetVerificationEmailRequestSigned{
Payload: payload,
Signature: signature,
}
_, err = s.ppclient.GetVerificationEmail(ctx, &reqSigned)
if err != nil {
return nil, err
}
var out pb.RpcMembershipGetVerificationEmailResponse
return &out, nil
}
func (s *service) VerifyEmailCode(ctx context.Context, req *pb.RpcMembershipVerifyEmailCodeRequest) (*pb.RpcMembershipVerifyEmailCodeResponse, error) {
// 1 - send request
bsr := psp.VerifyEmailRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
OwnerEthAddress: s.wallet.GetAccountEthAddress().Hex(),
Code: req.Code,
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.VerifyEmailRequestSigned{
Payload: payload,
Signature: signature,
}
// empty return or error
_, err = s.ppclient.VerifyEmail(ctx, &reqSigned)
if err != nil {
return nil, err
}
// 2 - clear cache
log.Debug("clearing cache after email verification code was confirmed")
err = s.cache.CacheClear()
if err != nil {
return nil, err
}
// return out
var out pb.RpcMembershipVerifyEmailCodeResponse
return &out, nil
}
func (s *service) FinalizeSubscription(ctx context.Context, req *pb.RpcMembershipFinalizeRequest) (*pb.RpcMembershipFinalizeResponse, error) {
// 1 - send request
bsr := psp.FinalizeSubscriptionRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
OwnerEthAddress: s.wallet.GetAccountEthAddress().Hex(),
RequestedAnyName: req.RequestedAnyName,
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.FinalizeSubscriptionRequestSigned{
Payload: payload,
Signature: signature,
}
// empty return or error
_, err = s.ppclient.FinalizeSubscription(ctx, &reqSigned)
if err != nil {
return nil, err
}
// 2 - clear cache
log.Debug("clearing cache after subscription was finalized")
err = s.cache.CacheClear()
if err != nil {
return nil, err
}
// return out
var out pb.RpcMembershipFinalizeResponse
return &out, nil
}
func (s *service) GetTiers(ctx context.Context, req *pb.RpcMembershipTiersGetRequest) (*pb.RpcMembershipTiersGetResponse, error) {
// 1 - check in cache
// status var. is unused here
cachedStatus, cachedTiers, err := s.cache.CacheGet()
// if NoCache -> skip returning from cache
if !req.NoCache && (err == nil) && (cachedTiers != nil) && (cachedTiers.Tiers != nil) {
log.Debug("returning tiers from cache", zap.Error(err), zap.Any("cachedTiers", cachedTiers))
return cachedTiers, nil
}
// 2 - send request
bsr := psp.GetTiersRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyId: s.wallet.Account().SignKey.GetPublic().Account(),
// WARNING: we will save to cache data for THIS locale and payment method!!!
Locale: req.Locale,
PaymentMethod: req.PaymentMethod,
}
payload, err := bsr.Marshal()
if err != nil {
return nil, err
}
privKey := s.wallet.GetAccountPrivkey()
signature, err := privKey.Sign(payload)
if err != nil {
return nil, err
}
reqSigned := psp.GetTiersRequestSigned{
Payload: payload,
Signature: signature,
}
// empty return or error
tiers, err := s.ppclient.GetAllTiers(ctx, &reqSigned)
if err != nil {
// if error here -> we do not create empty array
// with GetStatus above the logic is different
// there we create empty status and save it to cache
return nil, err
}
// return out
var out pb.RpcMembershipTiersGetResponse
out.Tiers = make([]*model.MembershipTierData, len(tiers.Tiers))
for i, tier := range tiers.Tiers {
out.Tiers[i] = &model.MembershipTierData{
Id: tier.Id,
Name: tier.Name,
Description: tier.Description,
IsActive: tier.IsActive,
IsTest: tier.IsTest,
IsHiddenTier: tier.IsHiddenTier,
PeriodType: model.MembershipTierDataPeriodType(tier.PeriodType),
PeriodValue: tier.PeriodValue,
PriceStripeUsdCents: tier.PriceStripeUsdCents,
AnyNamesCountIncluded: tier.AnyNamesCountIncluded,
AnyNameMinLength: tier.AnyNameMinLength,
}
// copy all features
out.Tiers[i].Features = make(map[string]*model.MembershipTierDataFeature)
for k, v := range tier.Features {
out.Tiers[i].Features[k] = &model.MembershipTierDataFeature{
ValueStr: v.ValueStr,
ValueUint: v.ValueUint,
}
}
}
// 3 - update tiers, not status
var cacheExpireTime time.Time
if cachedStatus != nil {
cacheExpireTime = time.Unix(int64(cachedStatus.Data.DateEnds), 0)
} else {
log.Debug("setting tiers cache to +1 day")
timeNow := time.Now().UTC()
cacheExpireTime = timeNow.Add(1 * 24 * time.Hour)
}
err = s.cache.CacheSet(nil, &out, cacheExpireTime)
if err != nil {
return nil, err
}
return &out, nil
}

File diff suppressed because it is too large Load diff

27
core/payments/utils.go Normal file
View file

@ -0,0 +1,27 @@
package payments
import (
ens "github.com/wealdtech/go-ens/v3"
)
func normalizeAnyName(name string) (string, error) {
// 1. ENSIP1 standard: ens-go v3.6.0 (current) is using it
// 2. ENSIP15 standard: that is an another standard for ENS namehashes
// that was accepted in June 2023.
//
// Current AnyNS (as of February 2024) implementation support only ENSIP1
//
// https://eips.ethereum.org/EIPS/eip-137 (ENSIP1) grammar:
// <domain> ::= <label> | <domain> "." <label>
// <label> ::= any valid string label per [UTS46](https://unicode.org/reports/tr46/)
//
// "❶❷❸❹❺❻❼❽❾❿":
// under ENSIP1 this OK
// under ENSIP15 this is not OK, will fail
name, err := ens.Normalize(name)
if err != nil {
return name, err
}
return name, nil
}

View file

@ -6,8 +6,12 @@ import (
app "github.com/anyproto/any-sync/app"
accountdata "github.com/anyproto/any-sync/commonspace/object/accountdata"
common "github.com/ethereum/go-ethereum/common"
crypto "github.com/anyproto/any-sync/util/crypto"
ecdsa "crypto/ecdsa"
mock "github.com/stretchr/testify/mock"
wallet "github.com/anyproto/anytype-heart/core/wallet"
@ -73,6 +77,100 @@ func (_c *MockWallet_Account_Call) RunAndReturn(run func() *accountdata.AccountK
return _c
}
// GetAccountEthAddress provides a mock function with given fields:
func (_m *MockWallet) GetAccountEthAddress() common.Address {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetAccountEthAddress")
}
var r0 common.Address
if rf, ok := ret.Get(0).(func() common.Address); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(common.Address)
}
}
return r0
}
// MockWallet_GetAccountEthAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccountEthAddress'
type MockWallet_GetAccountEthAddress_Call struct {
*mock.Call
}
// GetAccountEthAddress is a helper method to define mock.On call
func (_e *MockWallet_Expecter) GetAccountEthAddress() *MockWallet_GetAccountEthAddress_Call {
return &MockWallet_GetAccountEthAddress_Call{Call: _e.mock.On("GetAccountEthAddress")}
}
func (_c *MockWallet_GetAccountEthAddress_Call) Run(run func()) *MockWallet_GetAccountEthAddress_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockWallet_GetAccountEthAddress_Call) Return(_a0 common.Address) *MockWallet_GetAccountEthAddress_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWallet_GetAccountEthAddress_Call) RunAndReturn(run func() common.Address) *MockWallet_GetAccountEthAddress_Call {
_c.Call.Return(run)
return _c
}
// GetAccountEthPrivkey provides a mock function with given fields:
func (_m *MockWallet) GetAccountEthPrivkey() *ecdsa.PrivateKey {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetAccountEthPrivkey")
}
var r0 *ecdsa.PrivateKey
if rf, ok := ret.Get(0).(func() *ecdsa.PrivateKey); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*ecdsa.PrivateKey)
}
}
return r0
}
// MockWallet_GetAccountEthPrivkey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccountEthPrivkey'
type MockWallet_GetAccountEthPrivkey_Call struct {
*mock.Call
}
// GetAccountEthPrivkey is a helper method to define mock.On call
func (_e *MockWallet_Expecter) GetAccountEthPrivkey() *MockWallet_GetAccountEthPrivkey_Call {
return &MockWallet_GetAccountEthPrivkey_Call{Call: _e.mock.On("GetAccountEthPrivkey")}
}
func (_c *MockWallet_GetAccountEthPrivkey_Call) Run(run func()) *MockWallet_GetAccountEthPrivkey_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockWallet_GetAccountEthPrivkey_Call) Return(_a0 *ecdsa.PrivateKey) *MockWallet_GetAccountEthPrivkey_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockWallet_GetAccountEthPrivkey_Call) RunAndReturn(run func() *ecdsa.PrivateKey) *MockWallet_GetAccountEthPrivkey_Call {
_c.Call.Return(run)
return _c
}
// GetAccountPrivkey provides a mock function with given fields:
func (_m *MockWallet) GetAccountPrivkey() crypto.PrivKey {
ret := _m.Called()

View file

@ -1,6 +1,7 @@
package wallet
import (
"crypto/ecdsa"
"fmt"
"io/ioutil"
"os"
@ -10,6 +11,8 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/util/crypto"
"github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/anyproto/anytype-heart/metrics"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
@ -20,6 +23,9 @@ const (
keyFileDevice = "device.key"
)
type EthPrivateKey = *ecdsa.PrivateKey
type EthAddress = common.Address
type wallet struct {
rootPath string
repoPath string // other components will init their files/dirs inside
@ -29,6 +35,11 @@ type wallet struct {
deviceKey crypto.PrivKey
masterKey crypto.PrivKey
oldAccountKey crypto.PrivKey
// this key is used to sign ethereum transactions
// and use Any Naming Service
ethereumKey ecdsa.PrivateKey
// this is needed for any-sync
accountData *accountdata.AccountKeys
}
@ -49,6 +60,20 @@ func (r *wallet) GetMasterKey() crypto.PrivKey {
return r.masterKey
}
func (r *wallet) GetAccountEthPrivkey() *ecdsa.PrivateKey {
return &r.ethereumKey
}
func (r *wallet) GetAccountEthAddress() EthAddress {
publicKey := r.ethereumKey.Public()
// eat the error, we know it's an ecdsa.PublicKey
publicKeyECDSA, _ := publicKey.(*ecdsa.PublicKey)
ethAddress := ethcrypto.PubkeyToAddress(*publicKeyECDSA)
return common.HexToAddress(ethAddress.String())
}
func (r *wallet) Init(a *app.App) (err error) {
if r.accountKey == nil {
return fmt.Errorf("no account key present")
@ -114,6 +139,7 @@ func NewWithAccountRepo(rootPath string, derivationResult crypto.DerivationResul
oldAccountKey: derivationResult.OldAccountKey,
accountKey: derivationResult.Identity,
deviceKeyPath: filepath.Join(repoPath, keyFileDevice),
ethereumKey: derivationResult.EthereumIdentity,
}
}
@ -138,6 +164,10 @@ type Wallet interface {
GetDevicePrivkey() crypto.PrivKey
GetOldAccountKey() crypto.PrivKey
GetMasterKey() crypto.PrivKey
GetAccountEthPrivkey() EthPrivateKey
GetAccountEthAddress() EthAddress
ReadAppLink(appKey string) (*AppLinkPayload, error)
PersistAppLink(payload *AppLinkPayload) (appKey string, err error)

View file

@ -0,0 +1,3 @@
---
- - :inherit_from
- open/decisions.yml

File diff suppressed because it is too large Load diff

11
go.mod
View file

@ -23,6 +23,7 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20210512043942-b434301c6836
github.com/ethereum/go-ethereum v1.13.12
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
github.com/go-chi/chi/v5 v5.0.10
github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b
@ -81,6 +82,7 @@ require (
github.com/uber/jaeger-client-go v2.30.0+incompatible
github.com/valyala/fastjson v1.6.4
github.com/vektra/mockery/v2 v2.38.0
github.com/wealdtech/go-ens/v3 v3.6.0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yuin/goldmark v1.7.0
go.uber.org/atomic v1.11.0
@ -106,7 +108,9 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
@ -144,6 +148,7 @@ require (
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
@ -153,7 +158,6 @@ require (
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
github.com/ethereum/go-ethereum v1.13.12 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
@ -174,6 +178,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
@ -233,8 +238,9 @@ require (
github.com/quic-go/quic-go v0.40.1 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/rs/zerolog v1.29.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
@ -249,6 +255,7 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/wealdtech/go-multicodec v1.4.0 // indirect
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

48
go.sum
View file

@ -54,6 +54,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
@ -197,6 +199,8 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -248,9 +252,11 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA=
github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
@ -271,6 +277,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQYf4tfk5sSwFsnDg3qYaBxSjsD9S8+59vW0dKUgme4=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
@ -337,6 +345,8 @@ github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1
github.com/ethereum/go-ethereum v1.13.12 h1:iDr9UM2JWkngBHGovRJEQn4Kor7mT4gt9rUZqB5M29Y=
github.com/ethereum/go-ethereum v1.13.12/go.mod h1:hKL2Qcj1OvStXNSEDbucexqnEt1Wh4Cz329XsjAalZY=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA=
github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:1i71OnUq3iUe1ma7Lr6yG6/rjvM3emb6yoL7xLFzcVQ=
github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
@ -353,6 +363,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -385,6 +397,7 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -433,6 +446,8 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -500,6 +515,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
@ -568,6 +585,8 @@ github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
@ -596,6 +615,10 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hbagdi/go-unsplash v0.0.0-20230414214043-474fc02c9119 h1:Xqi5LlXRyF1GlNGXSb2NZJuOeTrXGzwGiQDwkwNXEc8=
github.com/hbagdi/go-unsplash v0.0.0-20230414214043-474fc02c9119/go.mod h1:DEzhU5CxSdknL3hUXTco1n5AO2BZHs4KeJo5ADWU0Iw=
github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw=
github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -1063,6 +1086,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
@ -1275,8 +1300,10 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
@ -1308,8 +1335,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
@ -1350,6 +1378,8 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc=
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@ -1400,7 +1430,10 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vektra/mockery/v2 v2.38.0 h1:I0LBuUzZHqAU4d1DknW0DTFBPO6n8TaD38WL2KJf3yI=
@ -1412,6 +1445,12 @@ github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830/go.mod h1:x6AKhvS
github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/wealdtech/go-ens/v3 v3.6.0 h1:EAByZlHRQ3vxqzzwNi0GvEq1AjVozfWO4DMldHcoVg8=
github.com/wealdtech/go-ens/v3 v3.6.0/go.mod h1:hcmMr9qPoEgVSEXU2Bwzrn/9NczTWZ1rE53jIlqUpzw=
github.com/wealdtech/go-multicodec v1.4.0 h1:iq5PgxwssxnXGGPTIK1srvt6U5bJwIp7k6kBrudIWxg=
github.com/wealdtech/go-multicodec v1.4.0/go.mod h1:aedGMaTeYkIqi/KCPre1ho5rTb3hGpu/snBOS3GQLw4=
github.com/wealdtech/go-string2eth v1.2.1 h1:u9sofvGFkp+uvTg4Nvsvy5xBaiw8AibGLLngfC4F76g=
github.com/wealdtech/go-string2eth v1.2.1/go.mod h1:9uwxm18zKZfrReXrGIbdiRYJtbE91iGcj6TezKKEx80=
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E=
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
@ -1431,6 +1470,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -1749,6 +1790,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1791,6 +1833,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1994,6 +2038,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -6356,6 +6356,406 @@ message Rpc {
}
}
}
/**
* A Membership is a bundle of several "Features"
* every user should have one and only one tier
* users can not have N tiers (no combining)
*/
message Membership {
/**
* Get the current status of the membership
* including the tier, status, dates, etc
* WARNING: this can be cached by Anytype heart
*/
message GetStatus {
message Request {
// pass true to force the cache update
// by default this is false
bool noCache = 1;
}
message Response {
Error error = 1;
anytype.model.Membership data = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
/**
* Check if the requested name is valid for the requested tier
* before requesting a payment link and paying
*/
message IsNameValid {
message Request {
int32 requestedTier = 1;
// full name including .any suffix
string requestedAnyName = 2;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
TOO_SHORT = 3;
TOO_LONG = 4;
HAS_INVALID_CHARS = 5;
TIER_FEATURES_NO_NAME = 6;
// if everything is fine - "name is already taken" check should be done in the NS
// see IsNameAvailable()
}
}
}
}
/**
* Generate a link to the payment provider
* where user can pay for the membership
*/
message GetPaymentUrl {
message Request {
int32 requestedTier = 1;
anytype.model.Membership.PaymentMethod paymentMethod = 2;
// if empty - then no name requested
// if non-empty - PP node will register that name on behalf of the user
string requestedAnyName = 3;
}
message Response {
Error error = 1;
// will feature current billing ID
// stripe.com/?client_reference_id=1234
string paymentUrl = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
/**
* Generate a link to the portal where user can:
* a) change his billing details
* b) see payment info, invoices, etc
* c) cancel membership
*/
message GetPortalLinkUrl {
message Request {
}
message Response {
Error error = 1;
string portalUrl = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
message Finalize {
message Request {
// if empty - then no name requested
// if non-empty - PP node will register that name on behalf of the user
string requestedAnyName = 1;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
/**
* Send an e-mail with verification code to the user
* can be called multiple times but with some timeout (N seconds) between calls
*/
message GetVerificationEmail {
message Request {
string email = 1;
bool subscribeToNewsletter = 2;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
/**
* Verify the e-mail address of the user
* need a correct code that was sent to the user when calling GetVerificationEmail
*/
message VerifyEmailCode {
message Request {
string code = 1;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
/**
* Tiers can change on the backend so if you want to show users the latest data
* you can call this method to get the latest tiers
*/
message Tiers {
message Get {
message Request {
// pass true to force the cache update
// by default this is false
bool noCache = 1;
string locale = 2;
uint32 paymentMethod = 3;
}
message Response {
Error error = 1;
repeated anytype.model.MembershipTierData tiers = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
PAYMENT_NODE_ERROR = 4;
}
}
}
}
}
}
message NameService {
message ResolveName {
message Request {
// including ".any" suffix
string fullName = 1;
}
message Response {
Error error = 1;
bool available = 2;
// EOA -> SCW -> name
// This field is non-empty only if name is "already registered"
string ownerScwEthAddress = 3;
// This field is non-empty only if name is "already registered"
string ownerEthAddress = 4;
// A content hash attached to this name
// This field is non-empty only if name is "already registered"
string ownerAnyAddress = 5;
// A SpaceId attached to this name
// This field is non-empty only if name is "already registered"
string spaceId = 6;
// A timestamp when this name expires
int64 nameExpires = 7;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
}
}
}
}
message ResolveAnyId {
message Request {
string anyId = 1;
}
message Response {
Error error = 1;
bool found = 2;
// including ".any" suffix
string fullName = 3;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
}
}
}
}
message ResolveSpaceId {
message Request {
string spaceId = 1;
}
message Response {
Error error = 1;
bool found = 2;
// including ".any" suffix
string fullName = 3;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
}
}
}
}
message UserAccount {
message Get {
message Request {
}
message Response {
Error error = 1;
// this will use ReverseResolve to get current name
// user can buy many names, but
// only 1 name can be set as "current": ETH address <-> name
string anyNameAttached = 2;
// Number of names that the user can reserve
uint64 namesCountLeft = 3;
// Number of operations: update name, add new data, etc
uint64 operationsCountLeft = 4;
// TODO: all operations list
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NOT_LOGGED_IN = 3;
BAD_NAME_RESOLVE = 4;
}
}
}
}
}
}
message Broadcast {
message PayloadEvent {
message Request {
@ -6381,6 +6781,8 @@ message Rpc {
}
}
message Empty {
}

View file

@ -100,6 +100,8 @@ message Event {
Notification.Update notificationUpdate = 115;
Payload.Broadcast payloadBroadcast = 116;
Membership.Update membershipUpdate = 117;
}
}
@ -1034,6 +1036,12 @@ message Event {
}
}
message Membership {
message Update {
anytype.model.Membership data = 1;
}
}
message Notification {
message Send {
anytype.model.Notification notification = 1;

View file

@ -310,5 +310,36 @@ service ClientCommands {
rpc NotificationReply (anytype.Rpc.Notification.Reply.Request) returns (anytype.Rpc.Notification.Reply.Response);
rpc NotificationTest (anytype.Rpc.Notification.Test.Request) returns (anytype.Rpc.Notification.Test.Response);
// Membership
// ***
// Get current subscription status (tier, expiration date, etc.)
// WARNING: can be cached by Anytype Heart
rpc MembershipGetStatus (anytype.Rpc.Membership.GetStatus.Request) returns (anytype.Rpc.Membership.GetStatus.Response);
rpc MembershipIsNameValid( anytype.Rpc.Membership.IsNameValid.Request) returns (anytype.Rpc.Membership.IsNameValid.Response);
// Buy a subscription, will return a payment URL. The user should be redirected to this URL to complete the payment.
rpc MembershipGetPaymentUrl (anytype.Rpc.Membership.GetPaymentUrl.Request) returns (anytype.Rpc.Membership.GetPaymentUrl.Response);
// Get a link to the user's subscription management portal. The user should be redirected to this URL to manage their subscription:
// a) change his billing details
// b) see payment info, invoices, etc
// c) cancel the subscription
rpc MembershipGetPortalLinkUrl (anytype.Rpc.Membership.GetPortalLinkUrl.Request) returns (anytype.Rpc.Membership.GetPortalLinkUrl.Response);
// Send a verification code to the user's email. The user should enter this code to verify his email.
rpc MembershipGetVerificationEmail (anytype.Rpc.Membership.GetVerificationEmail.Request) returns (anytype.Rpc.Membership.GetVerificationEmail.Response);
// Verify the user's email with the code received in the previous step (MembershipGetVerificationEmail)
rpc MembershipVerifyEmailCode (anytype.Rpc.Membership.VerifyEmailCode.Request) returns (anytype.Rpc.Membership.VerifyEmailCode.Response);
// If your subscription is in PendingRequiresFinalization:
// please call MembershipFinalize to finish the process
rpc MembershipFinalize (anytype.Rpc.Membership.Finalize.Request) returns (anytype.Rpc.Membership.Finalize.Response);
rpc MembershipGetTiers (anytype.Rpc.Membership.Tiers.Get.Request) returns (anytype.Rpc.Membership.Tiers.Get.Response);
// Name Service:
// ***
// hello.any -> data
rpc NameServiceUserAccountGet( anytype.Rpc.NameService.UserAccount.Get.Request) returns (anytype.Rpc.NameService.UserAccount.Get.Response);
rpc NameServiceResolveName( anytype.Rpc.NameService.ResolveName.Request) returns (anytype.Rpc.NameService.ResolveName.Response);
// 12D3KooWA8EXV3KjBxEU5EnsPfneLx84vMWAtTBQBeyooN82KSuS -> hello.any
rpc NameServiceResolveAnyId( anytype.Rpc.NameService.ResolveAnyId.Request ) returns (anytype.Rpc.NameService.ResolveAnyId.Response);
rpc BroadcastPayloadEvent (anytype.Rpc.Broadcast.PayloadEvent.Request) returns (anytype.Rpc.Broadcast.PayloadEvent.Response);
}

File diff suppressed because it is too large Load diff

View file

@ -180,6 +180,7 @@ const (
RelationKeyRevision domain.RelationKey = "revision"
RelationKeyImageKind domain.RelationKey = "imageKind"
RelationKeyImportType domain.RelationKey = "importType"
RelationKeyGlobalName domain.RelationKey = "globalName"
)
var (
@ -856,6 +857,19 @@ var (
ReadOnlyRelation: true,
Scope: model.Relation_type,
},
RelationKeyGlobalName: {
DataSource: model.Relation_details,
Description: "Name of profile that the user could be mentioned by",
Format: model.RelationFormat_shorttext,
Id: "_brglobalName",
Key: "globalName",
MaxCount: 1,
Name: "Global name",
ReadOnly: true,
ReadOnlyRelation: true,
Scope: model.Relation_type,
},
RelationKeyHeightInPixels: {
DataSource: model.Relation_details,

View file

@ -1688,5 +1688,15 @@
"name": "Import Type",
"readonly": true,
"source": "details"
},
{
"description": "Name of profile that the user could be mentioned by",
"format": "shorttext",
"hidden": false,
"key": "globalName",
"maxCount": 1,
"name": "Global name",
"readonly": true,
"source": "details"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1117,6 +1117,7 @@ message IdentityProfile {
string iconCid = 3;
repeated FileEncryptionKey iconEncryptionKeys = 4;
string description = 5;
string globalName = 6;
}
message FileInfo {
@ -1155,3 +1156,110 @@ message ManifestInfo {
repeated string categories = 11;
string language = 12;
}
message Membership {
enum Tier {
TierNewUser = 0;
// "free" tier
TierExplorer = 1;
// this tier can be used just for testing in debug mode
// it will still create an active subscription, but with NO features
TierBuilder1WeekTEST = 2;
// this tier can be used just for testing in debug mode
// it will still create an active subscription, but with NO features
TierCoCreator1WeekTEST = 3;
TierBuilder = 4;
TierCoCreator = 5;
}
enum Status {
StatusUnknown = 0;
// please wait a bit more
StatusPending = 1;
StatusActive = 2;
// in some cases we need to finalize the process:
// - if user has bought membership directly without first calling
// the BuySubscription method
//
// in this case please call Finalize to finish the process
StatusPendingRequiresFinalization = 3;
}
enum PaymentMethod {
MethodCard = 0;
MethodCrypto = 1;
MethodApplePay = 2;
MethodGooglePay = 3;
MethodAppleInapp = 4;
MethodGoogleInapp = 5;
}
// it was Tier before, changed to int32 to allow dynamic values
int32 tier = 1;
Status status = 2;
uint64 dateStarted = 3;
uint64 dateEnds = 4;
bool isAutoRenew = 5;
PaymentMethod paymentMethod = 6;
// can be empty if user did not ask for any name
string requestedAnyName = 7;
// if the email was verified by the user or set during the checkout - it will be here
string userEmail = 8;
bool subscribeToNewsletter = 9;
}
message MembershipTierData {
enum PeriodType {
PeriodTypeUnknown = 0;
PeriodTypeUnlimited = 1;
PeriodTypeDays = 2;
PeriodTypeWeeks = 3;
PeriodTypeMonths = 4;
PeriodTypeYears = 5;
}
message Feature {
// usually feature has uint value
// like "storage" - 120
uint32 valueUint = 1;
// in case feature will have string value
string valueStr = 2;
}
// this is a unique ID of the tier
// you should hardcode this in your app and provide icon, graphics, etc for each tier
// (even for old/historical/inactive/hidden tiers)
uint32 id = 1;
// localazied name of the tier
string name = 2;
// just a short technical description
// you don't have to use it, you can use your own UI-friendly texts
string description = 3;
// can you buy it (ON ALL PLATFORMS, without clarification)?
bool isActive = 4;
// is this tier for debugging only?
bool isTest = 5;
// hidden tiers are only visible once user got them
bool isHiddenTier = 6;
// how long is the period of the subscription
PeriodType periodType = 7;
// i.e. "5 days" or "3 years"
uint32 periodValue = 8;
// this one is a price we use ONLY on Stripe platform
uint32 priceStripeUsdCents = 9;
// number of ANY NS names that this tier includes
// (not counted as a "feature" and not in the features list)
uint32 anyNamesCountIncluded = 10;
// somename.any - len of 8
uint32 anyNameMinLength = 11;
// each tier has a set of features
// each feature has a unique key: "storage", "invites", etc
// not using enum here to provide dynamic feature list:
//
// "stoageGB" -> {64, ""}
// "invites" -> {120, ""}
// "spaces-public" -> {10, ""}
// ...
map<string, Feature> features = 12;
}

View file

@ -184,6 +184,7 @@ func (a *aclObjectManager) initAndRegisterMyIdentity(ctx context.Context) error
details.Fields[bundle.RelationKeyDescription.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyDescription.String()))
details.Fields[bundle.RelationKeyIconImage.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyIconImage.String()))
details.Fields[bundle.RelationKeyIdentityProfileLink.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyId.String()))
details.Fields[bundle.RelationKeyGlobalName.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyGlobalName.String()))
err = a.modifier.ModifyDetails(id, func(current *types.Struct) (*types.Struct, error) {
return pbtypes.StructMerge(current, details, false), nil
})
@ -339,6 +340,7 @@ func (a *aclObjectManager) updateParticipantFromIdentity(ctx context.Context, id
bundle.RelationKeyName.String(): pbtypes.String(profile.Name),
bundle.RelationKeyDescription.String(): pbtypes.String(profile.Description),
bundle.RelationKeyIconImage.String(): pbtypes.String(profile.IconCid),
bundle.RelationKeyGlobalName.String(): pbtypes.String(profile.GlobalName),
}}
return a.modifier.ModifyDetails(id, func(current *types.Struct) (*types.Struct, error) {
return pbtypes.StructMerge(current, details, false), nil