From bf46949207d0c415578207847adae4db3b82f3a1 Mon Sep 17 00:00:00 2001 From: Anthony Akentiev Date: Tue, 26 Mar 2024 17:40:15 +0000 Subject: [PATCH] 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 Co-authored-by: Kirill Stonozhenko <40611691+KirillSto@users.noreply.github.com> --- .mockery.yaml | 3 + clientlibrary/service/service.pb.go | 992 +- core/anytype/bootstrap.go | 12 +- core/identity/identity.go | 74 +- core/identity/identity_test.go | 43 +- core/nameservice.go | 137 + core/payments.go | 144 + core/payments/cache/cache.go | 294 + core/payments/cache/cache_test.go | 384 + .../cache/mock_cache/mock_CacheService.go | 426 + core/payments/payments.go | 696 + core/payments/payments_test.go | 1071 ++ core/payments/utils.go | 27 + core/wallet/mock_wallet/mock_Wallet.go | 98 + core/wallet/wallet.go | 30 + doc/dependency_decisions.yml | 3 + docs/proto.md | 1172 ++ go.mod | 11 +- go.sum | 48 +- pb/commands.pb.go | 11995 ++++++++++++++-- pb/events.pb.go | 1092 +- pb/protos/commands.proto | 402 + pb/protos/events.proto | 8 + pb/protos/service/service.proto | 31 + pb/service/service.pb.go | 998 +- pkg/lib/bundle/relation.gen.go | 14 + pkg/lib/bundle/relations.json | 10 + pkg/lib/pb/model/models.pb.go | 2609 +++- pkg/lib/pb/model/protos/models.proto | 108 + .../aclobjectmanager/aclobjectmanager.go | 2 + 30 files changed, 20564 insertions(+), 2370 deletions(-) create mode 100644 core/nameservice.go create mode 100644 core/payments.go create mode 100644 core/payments/cache/cache.go create mode 100644 core/payments/cache/cache_test.go create mode 100644 core/payments/cache/mock_cache/mock_CacheService.go create mode 100644 core/payments/payments.go create mode 100644 core/payments/payments_test.go create mode 100644 core/payments/utils.go create mode 100644 doc/dependency_decisions.yml diff --git a/.mockery.yaml b/.mockery.yaml index 0fd7ba6f9..30f3790a1 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -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: diff --git a/clientlibrary/service/service.pb.go b/clientlibrary/service/service.pb.go index 0347c4f7e..ea38779ed 100644 --- a/clientlibrary/service/service.pb.go +++ b/clientlibrary/service/service.pb.go @@ -25,283 +25,299 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) } var fileDescriptor_93a29dc403579097 = []byte{ - // 4413 bytes of a gzipped FileDescriptorProto + // 4657 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdd, 0x6f, 0x1d, 0x49, - 0x56, 0xc0, 0xe7, 0xbe, 0x30, 0xd0, 0xcb, 0x0e, 0x70, 0x17, 0x86, 0xd9, 0xb0, 0xeb, 0x64, 0x32, - 0x89, 0x9d, 0xc4, 0xf1, 0x75, 0x26, 0x99, 0x8f, 0x65, 0x17, 0x09, 0x39, 0x76, 0xec, 0x31, 0x9b, - 0x38, 0xc6, 0xd7, 0x49, 0xa4, 0x91, 0x90, 0x68, 0xf7, 0xad, 0x5c, 0x37, 0x6e, 0x77, 0xf5, 0x76, - 0xd7, 0xbd, 0xb6, 0x41, 0x20, 0x10, 0x08, 0x04, 0x02, 0x81, 0xf8, 0x7a, 0xe1, 0x01, 0x89, 0x3f, - 0x85, 0x27, 0x1e, 0xe7, 0x91, 0x17, 0x24, 0x34, 0xf3, 0x8f, 0xac, 0xba, 0xfa, 0x74, 0x7d, 0x9c, - 0x3e, 0xa7, 0xba, 0x3d, 0x4f, 0x33, 0xf2, 0xf9, 0x9d, 0x8f, 0xea, 0x3a, 0x55, 0x75, 0xaa, 0xaa, - 0x6f, 0x27, 0xba, 0x59, 0x9c, 0x6c, 0x16, 0xa5, 0x54, 0xb2, 0xda, 0xac, 0x44, 0xb9, 0x4c, 0x13, - 0xd1, 0xfe, 0x77, 0xa2, 0xff, 0x3c, 0x7e, 0x37, 0xce, 0xaf, 0xd4, 0x55, 0x21, 0x6e, 0x7c, 0x60, - 0xc9, 0x44, 0x9e, 0x9f, 0xc7, 0xf9, 0xac, 0x6a, 0x90, 0x1b, 0xef, 0x5b, 0x89, 0x58, 0x8a, 0x5c, - 0xc1, 0xdf, 0x1f, 0xff, 0xdf, 0x7f, 0x8f, 0xa2, 0xf7, 0xb6, 0xb3, 0x54, 0xe4, 0x6a, 0x1b, 0x34, - 0xc6, 0x5f, 0x46, 0xdf, 0xdd, 0x2a, 0x8a, 0x3d, 0xa1, 0x5e, 0x8b, 0xb2, 0x4a, 0x65, 0x3e, 0xfe, - 0x68, 0x02, 0x0e, 0x26, 0x47, 0x45, 0x32, 0xd9, 0x2a, 0x8a, 0x89, 0x15, 0x4e, 0x8e, 0xc4, 0xcf, - 0x16, 0xa2, 0x52, 0x37, 0xee, 0x84, 0xa1, 0xaa, 0x90, 0x79, 0x25, 0xc6, 0x6f, 0xa3, 0x5f, 0xdb, - 0x2a, 0x8a, 0xa9, 0x50, 0x3b, 0xa2, 0x6e, 0xc0, 0x54, 0xc5, 0x4a, 0x8c, 0xd7, 0x3a, 0xaa, 0x3e, - 0x60, 0x7c, 0xdc, 0xeb, 0x07, 0xc1, 0xcf, 0x71, 0xf4, 0x9d, 0xda, 0xcf, 0xe9, 0x42, 0xcd, 0xe4, - 0x45, 0x3e, 0xfe, 0xb0, 0xab, 0x08, 0x22, 0x63, 0xfb, 0x76, 0x08, 0x01, 0xab, 0x6f, 0xa2, 0x5f, - 0x7e, 0x13, 0x67, 0x99, 0x50, 0xdb, 0xa5, 0xa8, 0x03, 0xf7, 0x75, 0x1a, 0xd1, 0xa4, 0x91, 0x19, - 0xbb, 0x1f, 0x05, 0x19, 0x30, 0xfc, 0x65, 0xf4, 0xdd, 0x46, 0x72, 0x24, 0x12, 0xb9, 0x14, 0xe5, - 0x98, 0xd4, 0x02, 0x21, 0xf3, 0xc8, 0x3b, 0x10, 0xb6, 0xbd, 0x2d, 0xf3, 0xa5, 0x28, 0x15, 0x6d, - 0x1b, 0x84, 0x61, 0xdb, 0x16, 0x02, 0xdb, 0x7f, 0x3b, 0x8a, 0x7e, 0xb0, 0x95, 0x24, 0x72, 0x91, - 0xab, 0xe7, 0x32, 0x89, 0xb3, 0xe7, 0x69, 0x7e, 0x76, 0x20, 0x2e, 0xb6, 0x4f, 0x6b, 0x3e, 0x9f, - 0x8b, 0xf1, 0x13, 0xff, 0xa9, 0x36, 0xe8, 0xc4, 0xb0, 0x13, 0x17, 0x36, 0xbe, 0x3f, 0xb9, 0x9e, - 0x12, 0xc4, 0xf2, 0x8f, 0xa3, 0x68, 0x05, 0xc7, 0x32, 0x95, 0xd9, 0x52, 0xd8, 0x68, 0x3e, 0xed, - 0x31, 0xec, 0xe3, 0x26, 0x9e, 0xcf, 0xae, 0xab, 0x06, 0x11, 0x65, 0xd1, 0xf7, 0xdc, 0x74, 0x99, - 0x8a, 0x4a, 0x0f, 0xa7, 0xfb, 0x7c, 0x46, 0x00, 0x62, 0x3c, 0x3f, 0x18, 0x82, 0x82, 0xb7, 0x34, - 0x1a, 0x83, 0xb7, 0x4c, 0x56, 0xc6, 0xd9, 0x3d, 0xd2, 0x82, 0x43, 0x18, 0x5f, 0xf7, 0x07, 0x90, - 0xe0, 0xea, 0x0f, 0xa3, 0x5f, 0x79, 0x23, 0xcb, 0xb3, 0xaa, 0x88, 0x13, 0x01, 0x43, 0xe1, 0xae, - 0xaf, 0xdd, 0x4a, 0xf1, 0x68, 0x58, 0xed, 0xc3, 0x9c, 0xa4, 0x6d, 0x85, 0x2f, 0x0b, 0x81, 0xe7, - 0x20, 0xab, 0x58, 0x0b, 0xb9, 0xa4, 0xc5, 0x10, 0xd8, 0x3e, 0x8b, 0xc6, 0xd6, 0xf6, 0xc9, 0x1f, - 0x89, 0x44, 0x6d, 0xcd, 0x66, 0xb8, 0x57, 0xac, 0xae, 0x26, 0x26, 0x5b, 0xb3, 0x19, 0xd7, 0x2b, - 0x34, 0x0a, 0xce, 0x2e, 0xa2, 0xf7, 0x91, 0xb3, 0xe7, 0x69, 0xa5, 0x1d, 0x6e, 0x84, 0xad, 0x00, - 0x66, 0x9c, 0x4e, 0x86, 0xe2, 0xe0, 0xf8, 0xcf, 0x47, 0xd1, 0xf7, 0x09, 0xcf, 0x47, 0xe2, 0x5c, - 0x2e, 0xc5, 0xf8, 0x51, 0xbf, 0xb5, 0x86, 0x34, 0xfe, 0x3f, 0xbe, 0x86, 0x06, 0x91, 0x26, 0x53, - 0x91, 0x89, 0x44, 0xb1, 0x69, 0xd2, 0x88, 0x7b, 0xd3, 0xc4, 0x60, 0xce, 0x08, 0x6b, 0x85, 0x7b, - 0x42, 0x6d, 0x2f, 0xca, 0x52, 0xe4, 0x8a, 0xed, 0x4b, 0x8b, 0xf4, 0xf6, 0xa5, 0x87, 0x12, 0xed, - 0xd9, 0x13, 0x6a, 0x2b, 0xcb, 0xd8, 0xf6, 0x34, 0xe2, 0xde, 0xf6, 0x18, 0x0c, 0x3c, 0x24, 0xd1, - 0xaf, 0x3a, 0x4f, 0x4c, 0xed, 0xe7, 0x6f, 0xe5, 0x98, 0x7f, 0x16, 0x5a, 0x6e, 0x7c, 0xac, 0xf5, - 0x72, 0x44, 0x33, 0x9e, 0x5d, 0x16, 0xb2, 0xe4, 0xbb, 0xa5, 0x11, 0xf7, 0x36, 0xc3, 0x60, 0xe0, - 0xe1, 0x0f, 0xa2, 0xf7, 0x60, 0x96, 0x6c, 0xd7, 0xb3, 0x3b, 0xe4, 0x14, 0x8a, 0x17, 0xb4, 0xbb, - 0x3d, 0x94, 0x9d, 0x1c, 0x40, 0x06, 0x93, 0xcf, 0x47, 0xa4, 0x1e, 0x9a, 0x7a, 0xee, 0x84, 0xa1, - 0x8e, 0xed, 0x1d, 0x91, 0x09, 0xd6, 0x76, 0x23, 0xec, 0xb1, 0x6d, 0x20, 0xb0, 0x5d, 0x46, 0xbf, - 0x61, 0x1e, 0x4b, 0xbd, 0x8e, 0x6a, 0x79, 0x3d, 0x49, 0xaf, 0x33, 0xed, 0x76, 0x21, 0xe3, 0xeb, - 0xe1, 0x30, 0xb8, 0xd3, 0x1e, 0x18, 0x81, 0x74, 0x7b, 0xd0, 0xf8, 0xbb, 0x13, 0x86, 0xc0, 0xf6, - 0xdf, 0x8d, 0xa2, 0x1f, 0x82, 0xec, 0x59, 0x1e, 0x9f, 0x64, 0x42, 0x2f, 0x89, 0x07, 0x42, 0x5d, - 0xc8, 0xf2, 0x6c, 0x7a, 0x95, 0x27, 0xcc, 0xf2, 0x4f, 0xc3, 0x3d, 0xcb, 0x3f, 0xab, 0xe4, 0x54, - 0x7c, 0xd0, 0x50, 0x25, 0x0b, 0x5c, 0xf1, 0xb5, 0x2d, 0x50, 0xb2, 0xe0, 0x2a, 0x3e, 0x1f, 0xe9, - 0x58, 0x7d, 0x51, 0x4f, 0x9b, 0xb4, 0xd5, 0x17, 0xee, 0x3c, 0x79, 0x3b, 0x84, 0xd8, 0x69, 0xab, - 0x4d, 0x60, 0x99, 0xbf, 0x4d, 0xe7, 0xaf, 0x8a, 0x59, 0x9d, 0xc6, 0xf7, 0xe9, 0x0c, 0x75, 0x10, - 0x66, 0xda, 0x62, 0x50, 0xf0, 0xf6, 0x0f, 0xb6, 0x30, 0x82, 0xa1, 0xb4, 0x5b, 0xca, 0xf3, 0xe7, - 0x62, 0x1e, 0x27, 0x57, 0x30, 0xfe, 0x3f, 0x09, 0x0d, 0x3c, 0x4c, 0x9b, 0x20, 0x3e, 0xbd, 0xa6, - 0x16, 0xc4, 0xf3, 0x9f, 0xa3, 0xe8, 0x4e, 0xdb, 0xfc, 0xd3, 0x38, 0x9f, 0x0b, 0xe8, 0xcf, 0x26, - 0xfa, 0xad, 0x7c, 0x76, 0x24, 0x2a, 0x15, 0x97, 0x6a, 0xfc, 0x63, 0xba, 0x91, 0x21, 0x1d, 0x13, - 0xdb, 0x4f, 0xbe, 0x95, 0xae, 0xed, 0xf5, 0x69, 0x3d, 0xb1, 0xc1, 0x14, 0xe0, 0xf7, 0xba, 0x96, - 0xe0, 0x09, 0xe0, 0x76, 0x08, 0xb1, 0xbd, 0xae, 0x05, 0xfb, 0xf9, 0x32, 0x55, 0x62, 0x4f, 0xe4, - 0xa2, 0xec, 0xf6, 0x7a, 0xa3, 0xea, 0x23, 0x4c, 0xaf, 0x33, 0xa8, 0x9d, 0x6c, 0x3c, 0x6f, 0x66, - 0x71, 0x5c, 0x0f, 0x18, 0xe9, 0x2c, 0x8f, 0x0f, 0x87, 0xc1, 0x76, 0x77, 0xe7, 0xf8, 0x3c, 0x12, - 0x4b, 0x79, 0x86, 0x77, 0x77, 0xae, 0x89, 0x06, 0x60, 0x76, 0x77, 0x24, 0x68, 0x57, 0x30, 0xc7, - 0xcf, 0xeb, 0x54, 0x5c, 0xa0, 0x15, 0xcc, 0x55, 0xae, 0xc5, 0xcc, 0x0a, 0x46, 0x60, 0xe0, 0xe1, - 0x20, 0xfa, 0x25, 0x2d, 0xfc, 0x3d, 0x99, 0xe6, 0xe3, 0x9b, 0x84, 0x52, 0x2d, 0x30, 0x56, 0x6f, - 0xf1, 0x00, 0x8a, 0xb8, 0xfe, 0xeb, 0x76, 0x9c, 0x27, 0x22, 0x23, 0x23, 0xb6, 0xe2, 0x60, 0xc4, - 0x1e, 0x86, 0x22, 0x7e, 0x76, 0x99, 0x2a, 0x32, 0xe2, 0x5a, 0x10, 0x8c, 0x18, 0x00, 0x5b, 0x8a, - 0xe8, 0x3f, 0xd7, 0xf3, 0xe1, 0xf4, 0x34, 0x2e, 0xd3, 0x7c, 0x3e, 0xa6, 0x62, 0x71, 0xe4, 0x4c, - 0x29, 0x42, 0x71, 0x68, 0x48, 0x80, 0xe2, 0x56, 0x51, 0x94, 0xf5, 0x34, 0x4b, 0x0d, 0x09, 0x1f, - 0x09, 0x0e, 0x89, 0x0e, 0x4a, 0x7b, 0xdb, 0x11, 0x49, 0x96, 0xe6, 0x41, 0x6f, 0x80, 0x0c, 0xf1, - 0x66, 0x51, 0xf0, 0xb6, 0x88, 0xde, 0xd7, 0xc0, 0x61, 0x5c, 0xaa, 0x34, 0x49, 0x8b, 0x38, 0x6f, - 0x8b, 0x6f, 0x6a, 0x50, 0x75, 0x28, 0xe3, 0x73, 0x63, 0x20, 0x0d, 0x6e, 0xff, 0x7d, 0x14, 0x7d, - 0x88, 0xfd, 0x1e, 0x8a, 0xf2, 0x3c, 0xd5, 0x7b, 0xb8, 0xaa, 0x99, 0x01, 0xc7, 0x9f, 0x87, 0x8d, - 0x76, 0x14, 0x4c, 0x34, 0x3f, 0xba, 0xbe, 0x22, 0x04, 0xf6, 0xfb, 0x51, 0xd4, 0xec, 0x15, 0xf4, - 0x7e, 0xce, 0x4f, 0x40, 0xd8, 0x44, 0x78, 0x9b, 0xb9, 0x0f, 0x03, 0x84, 0x9d, 0xa7, 0x9b, 0xbf, - 0xeb, 0x6d, 0xea, 0x98, 0xd4, 0xd0, 0x22, 0x66, 0x9e, 0x46, 0x08, 0x0e, 0x74, 0x7a, 0x2a, 0x2f, - 0xe8, 0x40, 0x6b, 0x49, 0x38, 0x50, 0x20, 0xec, 0xc1, 0x11, 0x04, 0x4a, 0x1d, 0x1c, 0xb5, 0x61, - 0x84, 0x0e, 0x8e, 0x30, 0x03, 0x86, 0x65, 0xf4, 0xeb, 0xae, 0xe1, 0xa7, 0x52, 0x9e, 0x9d, 0xc7, - 0xe5, 0xd9, 0xf8, 0x01, 0xaf, 0xdc, 0x32, 0xc6, 0xd1, 0xfa, 0x20, 0xd6, 0x8e, 0x21, 0xd7, 0x61, - 0xbd, 0xca, 0xbf, 0x2a, 0x33, 0x34, 0x86, 0x3c, 0x1b, 0x80, 0x30, 0x63, 0x88, 0x41, 0xed, 0xb4, - 0xe9, 0x7a, 0x9b, 0x0a, 0xbc, 0x55, 0xf1, 0xd4, 0xa7, 0x82, 0xdb, 0xaa, 0x10, 0x18, 0x4e, 0xa1, - 0xbd, 0x32, 0x2e, 0x4e, 0xe9, 0x14, 0xd2, 0xa2, 0x70, 0x0a, 0xb5, 0x08, 0xee, 0xef, 0xa9, 0x88, - 0xcb, 0xe4, 0x94, 0xee, 0xef, 0x46, 0x16, 0xee, 0x6f, 0xc3, 0xd8, 0x55, 0xdd, 0x35, 0x3c, 0x5d, - 0x9c, 0x54, 0x49, 0x99, 0x9e, 0x88, 0xf1, 0x3a, 0xaf, 0x6d, 0x20, 0x66, 0x55, 0x67, 0x61, 0x7b, - 0xb0, 0x04, 0x3e, 0x5b, 0xd9, 0xfe, 0xac, 0x42, 0x07, 0x4b, 0xad, 0x0d, 0x87, 0x60, 0x0e, 0x96, - 0x68, 0x12, 0x37, 0x6f, 0xaf, 0x94, 0x8b, 0xa2, 0xea, 0x69, 0x1e, 0x82, 0xc2, 0xcd, 0xeb, 0xc2, - 0xe0, 0xf3, 0x32, 0xfa, 0x4d, 0xf7, 0x91, 0xbe, 0xca, 0x2b, 0xe3, 0x75, 0x83, 0x7f, 0x4e, 0x0e, - 0xc6, 0x1c, 0xd1, 0x04, 0x70, 0xbb, 0xc4, 0xb6, 0x9e, 0xd5, 0x8e, 0x50, 0x71, 0x9a, 0x55, 0xe3, - 0x55, 0xda, 0x46, 0x2b, 0x67, 0x96, 0x58, 0x8a, 0xc3, 0x43, 0x68, 0x67, 0x51, 0x64, 0x69, 0xd2, - 0x3d, 0xab, 0x03, 0x5d, 0x23, 0x0e, 0x0f, 0x21, 0x17, 0xc3, 0x53, 0xc2, 0x54, 0xa8, 0xe6, 0x7f, - 0x8e, 0xaf, 0x0a, 0x41, 0x4f, 0x09, 0x1e, 0x12, 0x9e, 0x12, 0x30, 0x8a, 0xdb, 0x33, 0x15, 0xea, - 0x79, 0x7c, 0x25, 0x17, 0xcc, 0x94, 0x60, 0xc4, 0xe1, 0xf6, 0xb8, 0x98, 0x5d, 0xb8, 0x8d, 0x87, - 0xfd, 0x5c, 0x89, 0x32, 0x8f, 0xb3, 0xdd, 0x2c, 0x9e, 0x57, 0x63, 0x66, 0xdc, 0xf8, 0x14, 0xb3, - 0x70, 0xf3, 0x34, 0xf1, 0x18, 0xf7, 0xab, 0xdd, 0x78, 0x29, 0xcb, 0x54, 0xf1, 0x8f, 0xd1, 0x22, - 0xbd, 0x8f, 0xd1, 0x43, 0x49, 0x6f, 0x5b, 0x65, 0x72, 0x9a, 0x2e, 0xc5, 0x2c, 0xe0, 0xad, 0x45, - 0x06, 0x78, 0x73, 0x50, 0xa2, 0xd3, 0xa6, 0x72, 0x51, 0x26, 0x82, 0xed, 0xb4, 0x46, 0xdc, 0xdb, - 0x69, 0x06, 0x03, 0x0f, 0x7f, 0x35, 0x8a, 0x7e, 0xab, 0x91, 0xba, 0x07, 0x68, 0x3b, 0x71, 0x75, - 0x7a, 0x22, 0xe3, 0x72, 0x36, 0xfe, 0x98, 0xb2, 0x43, 0xa2, 0xc6, 0xf5, 0xe3, 0xeb, 0xa8, 0xe0, - 0xc7, 0xfa, 0x3c, 0xad, 0x9c, 0x11, 0x47, 0x3e, 0x56, 0x0f, 0x09, 0x3f, 0x56, 0x8c, 0xe2, 0x09, - 0x44, 0xcb, 0x9b, 0xcd, 0xea, 0x2a, 0xab, 0xef, 0xef, 0x58, 0xd7, 0x7a, 0x39, 0x3c, 0x3f, 0xd6, - 0x42, 0x3f, 0x5b, 0x36, 0x38, 0x1b, 0x74, 0xc6, 0x4c, 0x86, 0xe2, 0xac, 0x67, 0x33, 0x2a, 0xc2, - 0x9e, 0x3b, 0x23, 0x63, 0x32, 0x14, 0x67, 0x3c, 0x3b, 0xd3, 0x5a, 0xc8, 0x33, 0x31, 0xb5, 0x4d, - 0x86, 0xe2, 0x38, 0x81, 0xb6, 0x8a, 0x22, 0xbb, 0x3a, 0x16, 0xe7, 0x45, 0xc6, 0x26, 0x90, 0x87, - 0x84, 0x13, 0x08, 0xa3, 0xb8, 0xfa, 0x39, 0x96, 0x75, 0x6d, 0x45, 0x56, 0x3f, 0x5a, 0x14, 0xae, - 0x7e, 0x5a, 0x04, 0x17, 0x0c, 0xc7, 0x72, 0x5b, 0x66, 0x99, 0x48, 0x54, 0xf7, 0x26, 0xca, 0x68, - 0x5a, 0x22, 0x5c, 0x30, 0x20, 0xd2, 0x9e, 0x38, 0xb4, 0xb5, 0x7a, 0x5c, 0x8a, 0xa7, 0x57, 0xcf, - 0xd3, 0xfc, 0x6c, 0x4c, 0xaf, 0x8d, 0x16, 0x60, 0x4e, 0x1c, 0x48, 0x10, 0xef, 0x09, 0x5e, 0xe5, - 0x33, 0x49, 0xef, 0x09, 0x6a, 0x49, 0x78, 0x4f, 0x00, 0x04, 0x36, 0x79, 0x24, 0x38, 0x93, 0xb5, - 0x24, 0x6c, 0x12, 0x08, 0x6a, 0x3e, 0x80, 0xa3, 0x3d, 0x76, 0x3e, 0x40, 0x87, 0x79, 0x6b, 0xbd, - 0x1c, 0xce, 0xd0, 0x76, 0x73, 0xb0, 0x2b, 0x54, 0x72, 0x4a, 0x67, 0xa8, 0x87, 0x84, 0x33, 0x14, - 0xa3, 0xb8, 0x49, 0xc7, 0xd2, 0x6c, 0x6e, 0x56, 0xe9, 0xfc, 0xe8, 0x6c, 0x6c, 0xd6, 0x7a, 0x39, - 0x5c, 0xae, 0xef, 0x9f, 0xeb, 0x67, 0x46, 0x26, 0x79, 0x23, 0x0b, 0x97, 0xeb, 0x86, 0xc1, 0xd1, - 0x37, 0x82, 0xfa, 0x71, 0xd2, 0xd1, 0x5b, 0x79, 0x38, 0x7a, 0x8f, 0x03, 0x27, 0xff, 0x3a, 0x8a, - 0x6e, 0xba, 0x5e, 0x0e, 0x64, 0x3d, 0x46, 0x5e, 0xc7, 0x59, 0x3a, 0x8b, 0x95, 0x38, 0x96, 0x67, - 0x22, 0x47, 0xfb, 0x7d, 0x3f, 0xda, 0x86, 0x9f, 0x78, 0x0a, 0xcc, 0x7e, 0x7f, 0x90, 0x22, 0xce, - 0x93, 0x86, 0x7e, 0x55, 0x89, 0xed, 0xb8, 0x62, 0x66, 0x32, 0x0f, 0x09, 0xe7, 0x09, 0x46, 0x71, - 0xd1, 0xd6, 0xc8, 0x9f, 0x5d, 0x16, 0xa2, 0x4c, 0x45, 0x9e, 0x08, 0xba, 0x68, 0xc3, 0x54, 0xb8, - 0x68, 0x23, 0xe8, 0xce, 0x76, 0xd8, 0x4c, 0x4e, 0xdd, 0xcb, 0x64, 0x4c, 0x04, 0x2e, 0x93, 0x19, - 0x14, 0x37, 0xd2, 0x02, 0xe4, 0x91, 0x52, 0xc7, 0x4a, 0xf0, 0x48, 0x89, 0xa7, 0x3b, 0x87, 0x0c, - 0x86, 0x99, 0xd6, 0xc3, 0xa4, 0x27, 0xf4, 0xa9, 0x3b, 0x5c, 0xd6, 0x07, 0xb1, 0xf4, 0xa9, 0xc6, - 0x91, 0xc8, 0x62, 0xbd, 0x84, 0x04, 0x8e, 0x0e, 0x5a, 0x66, 0xc8, 0xa9, 0x86, 0xc3, 0x82, 0xc3, - 0xbf, 0x18, 0x45, 0x37, 0x28, 0x8f, 0x2f, 0x0b, 0xed, 0xf7, 0x51, 0xbf, 0xad, 0x86, 0x64, 0x6e, - 0xcb, 0xc3, 0x1a, 0x10, 0xc3, 0x9f, 0x44, 0x1f, 0xb4, 0x22, 0x7b, 0x99, 0x0e, 0x01, 0xf8, 0x55, - 0x84, 0x89, 0x1f, 0x73, 0xc6, 0xfd, 0xe6, 0x60, 0xde, 0x16, 0xe8, 0x7e, 0x5c, 0x15, 0x2a, 0xd0, - 0x8d, 0x0d, 0x10, 0x33, 0x05, 0x3a, 0x81, 0xe1, 0x95, 0xba, 0x45, 0xea, 0x71, 0x42, 0xcd, 0x71, - 0xc6, 0x84, 0x3b, 0x4a, 0xee, 0xf5, 0x83, 0x38, 0x77, 0x5a, 0x31, 0xd4, 0xc5, 0x0f, 0x42, 0x16, - 0x50, 0x6d, 0xbc, 0x3e, 0x88, 0x05, 0x87, 0x7f, 0x16, 0x7d, 0xbf, 0xd3, 0xb0, 0x5d, 0x11, 0xab, - 0x45, 0x29, 0x66, 0xe3, 0xcd, 0x9e, 0xb8, 0x5b, 0xd0, 0xb8, 0x7e, 0x34, 0x5c, 0x01, 0xfc, 0xff, - 0xcd, 0x28, 0xfa, 0x81, 0xcf, 0x35, 0x5d, 0x6c, 0x62, 0x78, 0x1c, 0x32, 0xe9, 0xb3, 0x26, 0x8c, - 0x27, 0xd7, 0xd2, 0xe9, 0xec, 0xc1, 0xdc, 0x44, 0xde, 0x5a, 0xc6, 0x69, 0x16, 0x9f, 0x64, 0x82, - 0xdc, 0x83, 0x79, 0xb9, 0x69, 0xd0, 0xe0, 0x1e, 0x8c, 0x55, 0xe9, 0xcc, 0x92, 0x7a, 0xbc, 0x39, - 0xb5, 0xfb, 0x43, 0x7e, 0x54, 0x12, 0xa5, 0xfb, 0xc6, 0x40, 0x1a, 0xdc, 0xaa, 0xf6, 0xec, 0xaa, - 0xfe, 0xb3, 0x9b, 0xe4, 0x94, 0x57, 0x50, 0x25, 0x32, 0x7d, 0x63, 0x20, 0x0d, 0x5e, 0xff, 0x34, - 0xfa, 0xa0, 0xeb, 0x15, 0x16, 0x85, 0xcd, 0x5e, 0x53, 0x68, 0x5d, 0x78, 0x34, 0x5c, 0xc1, 0x96, - 0xfa, 0x5f, 0xa4, 0x95, 0x92, 0xe5, 0xd5, 0xf4, 0x54, 0x5e, 0xb4, 0x2f, 0x8c, 0xfa, 0xa3, 0x15, - 0x80, 0x89, 0x43, 0x30, 0xa5, 0x3e, 0x4d, 0x76, 0x5c, 0xd9, 0x17, 0x4b, 0x2b, 0xc6, 0x95, 0x43, - 0xf4, 0xb8, 0xf2, 0x49, 0x3b, 0x57, 0xb5, 0xad, 0xb2, 0x6f, 0xc1, 0xae, 0xd1, 0xa1, 0x76, 0xdf, - 0x84, 0xbd, 0xd7, 0x0f, 0xda, 0xed, 0xd7, 0x6e, 0x9a, 0x89, 0x97, 0x6f, 0xdf, 0x66, 0x32, 0x9e, - 0xa1, 0xed, 0x57, 0x2d, 0x99, 0x80, 0x88, 0xd9, 0x7e, 0x21, 0xc4, 0x16, 0x9d, 0xb5, 0x40, 0xdf, - 0xcd, 0xb4, 0xa6, 0x57, 0xbb, 0x7a, 0xae, 0x9c, 0x29, 0x3a, 0x29, 0xce, 0x2e, 0x18, 0xb5, 0xb4, - 0x1e, 0x82, 0xad, 0x8f, 0xbb, 0x5d, 0x5d, 0x47, 0xcc, 0x2c, 0x18, 0x04, 0x66, 0xf7, 0x47, 0xb5, - 0xf0, 0x55, 0xa1, 0x8d, 0xdf, 0xea, 0x6a, 0x35, 0x12, 0x66, 0x7f, 0xe4, 0x13, 0xb6, 0xce, 0xaf, - 0xff, 0xbe, 0x23, 0x2f, 0x72, 0x6d, 0x94, 0x78, 0x9a, 0xad, 0x8c, 0xa9, 0xf3, 0x31, 0x03, 0x86, - 0x7f, 0x1a, 0xfd, 0xa2, 0x36, 0x5c, 0xca, 0x62, 0xbc, 0x42, 0x28, 0x94, 0xce, 0x6b, 0x27, 0x37, - 0x59, 0xb9, 0x7d, 0x7b, 0xca, 0xf4, 0xdf, 0xab, 0x2a, 0x9e, 0x0b, 0xf4, 0xf6, 0x94, 0xed, 0x15, - 0x2d, 0x65, 0xde, 0x9e, 0xea, 0x52, 0xf6, 0x8d, 0xa0, 0x5a, 0x76, 0x20, 0x67, 0x60, 0x9d, 0x68, - 0xa1, 0x11, 0x32, 0x6f, 0x04, 0x75, 0x20, 0x7b, 0x7e, 0x7f, 0x10, 0x2f, 0xd3, 0xb9, 0x99, 0xfc, - 0x9b, 0x39, 0xa4, 0x42, 0xe7, 0xf7, 0x96, 0x99, 0x38, 0x10, 0x73, 0x7e, 0xcf, 0xc2, 0xe0, 0xf3, - 0x5f, 0x46, 0xd1, 0x2d, 0xcb, 0xec, 0xb5, 0xc7, 0x2a, 0xfb, 0xf9, 0x5b, 0xf9, 0x26, 0x55, 0xa7, - 0xf5, 0x3e, 0xbe, 0x1a, 0x7f, 0xc6, 0x99, 0xa4, 0x79, 0x13, 0xca, 0xe7, 0xd7, 0xd6, 0xb3, 0xd5, - 0x5c, 0x7b, 0xdc, 0x62, 0xef, 0xb6, 0x1a, 0x0d, 0x54, 0xcd, 0x99, 0x53, 0x19, 0xcc, 0x31, 0xd5, - 0x5c, 0x88, 0xb7, 0x5d, 0x6c, 0x9c, 0x67, 0x32, 0xc7, 0x5d, 0x6c, 0x2d, 0xd4, 0x42, 0xa6, 0x8b, - 0x3b, 0x90, 0x9d, 0x1b, 0x5b, 0x51, 0x73, 0x32, 0xb0, 0x95, 0x65, 0x68, 0x6e, 0x34, 0xaa, 0x06, - 0x60, 0xe6, 0x46, 0x12, 0x04, 0x3f, 0x47, 0xd1, 0x77, 0xea, 0x47, 0x7a, 0x58, 0x8a, 0x65, 0x2a, - 0xf0, 0x35, 0xac, 0x23, 0x61, 0xc6, 0xbf, 0x4f, 0xd8, 0x91, 0xf5, 0x2a, 0xaf, 0x8a, 0x2c, 0xae, - 0x4e, 0xe1, 0x62, 0xce, 0x6f, 0x73, 0x2b, 0xc4, 0x57, 0x73, 0x77, 0x7b, 0x28, 0x3b, 0xf1, 0xb6, - 0x32, 0x33, 0xc5, 0xac, 0xd2, 0xaa, 0x9d, 0x69, 0x66, 0xad, 0x97, 0xb3, 0x47, 0x93, 0x7b, 0x71, - 0x96, 0x89, 0xf2, 0xaa, 0x95, 0xbd, 0x88, 0xf3, 0xf4, 0xad, 0xa8, 0x14, 0x3a, 0x9a, 0x04, 0x6a, - 0x82, 0x31, 0xe6, 0x68, 0x32, 0x80, 0xdb, 0xca, 0x1a, 0x79, 0xde, 0xcf, 0x67, 0xe2, 0x12, 0x55, - 0xd6, 0xd8, 0x8e, 0x66, 0x98, 0xca, 0x9a, 0x63, 0xed, 0xf2, 0xf8, 0x34, 0x93, 0xc9, 0x19, 0x2c, - 0x01, 0x7e, 0x07, 0x6b, 0x09, 0x5e, 0x03, 0x6e, 0x87, 0x10, 0xbb, 0x08, 0x68, 0xc1, 0x91, 0x28, - 0xb2, 0x38, 0xc1, 0x77, 0xf1, 0x8d, 0x0e, 0xc8, 0x98, 0x45, 0x00, 0x33, 0x28, 0x5c, 0xb8, 0xe3, - 0xa7, 0xc2, 0x45, 0x57, 0xfc, 0xb7, 0x43, 0x88, 0x5d, 0x06, 0xb5, 0x60, 0x5a, 0x64, 0xa9, 0x42, - 0xc3, 0xa0, 0xd1, 0xd0, 0x12, 0x66, 0x18, 0xf8, 0x04, 0x32, 0xf9, 0x42, 0x94, 0x73, 0x41, 0x9a, - 0xd4, 0x92, 0xa0, 0xc9, 0x96, 0xb0, 0x6f, 0x1f, 0x35, 0x6d, 0x97, 0xc5, 0x15, 0x7a, 0xfb, 0x08, - 0x9a, 0x25, 0x8b, 0x2b, 0xe6, 0xed, 0x23, 0x0f, 0x40, 0x21, 0x1e, 0xc6, 0x95, 0xa2, 0x43, 0xd4, - 0x92, 0x60, 0x88, 0x2d, 0x61, 0xd7, 0xe8, 0x26, 0xc4, 0x85, 0x42, 0x6b, 0x34, 0x04, 0xe0, 0x5c, - 0x15, 0xde, 0x64, 0xe5, 0x76, 0x26, 0x69, 0x7a, 0x45, 0xa8, 0xdd, 0x54, 0x64, 0xb3, 0x0a, 0xcd, - 0x24, 0xf0, 0xdc, 0x5b, 0x29, 0x33, 0x93, 0x74, 0x29, 0x94, 0x4a, 0x70, 0x86, 0x4b, 0xb5, 0x0e, - 0x1d, 0xdf, 0xde, 0x0e, 0x21, 0x76, 0x7e, 0x6a, 0x83, 0xde, 0x8e, 0xcb, 0x32, 0xad, 0x17, 0xff, - 0x55, 0x3a, 0xa0, 0x56, 0xce, 0xcc, 0x4f, 0x14, 0x87, 0x86, 0x57, 0x3b, 0x71, 0x53, 0x81, 0xe1, - 0xa9, 0xfb, 0xa3, 0x20, 0x63, 0x2b, 0x4e, 0x2d, 0x71, 0xee, 0xba, 0xa8, 0xa7, 0x49, 0x5c, 0x75, - 0xad, 0xf6, 0x61, 0xce, 0xfb, 0xcc, 0xc6, 0xc5, 0x0b, 0xb9, 0x14, 0xc7, 0xf2, 0xd9, 0x65, 0x5a, - 0xa9, 0x34, 0x9f, 0xc3, 0xca, 0xfd, 0x84, 0xb1, 0x44, 0xc1, 0xcc, 0xfb, 0xcc, 0xbd, 0x4a, 0xb6, - 0x80, 0x40, 0xb1, 0x1c, 0x88, 0x0b, 0xb2, 0x80, 0xc0, 0x16, 0x0d, 0xc7, 0x14, 0x10, 0x21, 0xde, - 0x9e, 0x69, 0x18, 0xe7, 0xf0, 0xa3, 0xaf, 0x63, 0xd9, 0xd6, 0x72, 0x9c, 0x35, 0x0c, 0x32, 0xdb, - 0xca, 0xa0, 0x82, 0xdd, 0xeb, 0x19, 0xff, 0x76, 0x88, 0xdd, 0x63, 0xec, 0x74, 0x87, 0xd9, 0xfd, - 0x01, 0x24, 0xe1, 0xca, 0x5e, 0xd8, 0x72, 0xae, 0xba, 0xf7, 0xb5, 0xf7, 0x07, 0x90, 0xce, 0xf9, - 0x88, 0xdb, 0xac, 0xa7, 0x71, 0x72, 0x36, 0x2f, 0xe5, 0x22, 0x9f, 0x6d, 0xcb, 0x4c, 0x96, 0xe8, - 0x7c, 0xc4, 0x8b, 0x1a, 0xa1, 0xcc, 0xf9, 0x48, 0x8f, 0x8a, 0xad, 0xe0, 0xdc, 0x28, 0xb6, 0xb2, - 0x74, 0x8e, 0x77, 0xb7, 0x9e, 0x21, 0x0d, 0x30, 0x15, 0x1c, 0x09, 0x12, 0x49, 0xd4, 0xec, 0x7e, - 0x55, 0x9a, 0xc4, 0x59, 0xe3, 0x6f, 0x93, 0x37, 0xe3, 0x81, 0xbd, 0x49, 0x44, 0x28, 0x10, 0xed, - 0x3c, 0x5e, 0x94, 0xf9, 0x7e, 0xae, 0x24, 0xdb, 0xce, 0x16, 0xe8, 0x6d, 0xa7, 0x03, 0xa2, 0x69, - 0xf5, 0x58, 0x5c, 0xd6, 0xd1, 0xd4, 0xff, 0xa1, 0xa6, 0xd5, 0xfa, 0xef, 0x13, 0x90, 0x87, 0xa6, - 0x55, 0xc4, 0xa1, 0xc6, 0x80, 0x93, 0x26, 0x61, 0x02, 0xda, 0x7e, 0x9a, 0xdc, 0xeb, 0x07, 0x69, - 0x3f, 0x53, 0x75, 0x95, 0x89, 0x90, 0x1f, 0x0d, 0x0c, 0xf1, 0xd3, 0x82, 0xf6, 0xe2, 0xc4, 0x6b, - 0xcf, 0xa9, 0x48, 0xce, 0x3a, 0xef, 0x9f, 0xf8, 0x81, 0x36, 0x08, 0x73, 0x71, 0xc2, 0xa0, 0x74, - 0x17, 0xed, 0x27, 0x32, 0x0f, 0x75, 0x51, 0x2d, 0x1f, 0xd2, 0x45, 0xc0, 0xd9, 0xcd, 0xaf, 0x91, - 0x42, 0x66, 0x36, 0xdd, 0xb4, 0xce, 0x58, 0x70, 0x21, 0x66, 0xf3, 0xcb, 0xc2, 0xb6, 0x26, 0xc7, - 0x3e, 0x5f, 0x74, 0xdf, 0xff, 0xec, 0x58, 0x79, 0xc1, 0xbf, 0xff, 0xc9, 0xb1, 0x7c, 0x23, 0x9b, - 0x1c, 0xe9, 0xb1, 0xe2, 0xe7, 0xc9, 0xc3, 0x61, 0xb0, 0xdd, 0xf2, 0x78, 0x3e, 0xb7, 0x33, 0x11, - 0x97, 0x8d, 0xd7, 0x8d, 0x80, 0x21, 0x8b, 0x31, 0x5b, 0x9e, 0x00, 0x8e, 0xa6, 0x30, 0xcf, 0xf3, - 0xb6, 0xcc, 0x95, 0xc8, 0x15, 0x35, 0x85, 0xf9, 0xc6, 0x00, 0x0c, 0x4d, 0x61, 0x9c, 0x02, 0xca, - 0x5b, 0x7d, 0x1e, 0x24, 0xd4, 0x41, 0x7c, 0x4e, 0x56, 0x6c, 0xcd, 0x59, 0x4f, 0x23, 0x0f, 0xe5, - 0x2d, 0xe2, 0xd0, 0x90, 0xdf, 0x3f, 0x8f, 0xe7, 0xc6, 0x0b, 0xa1, 0xad, 0xe5, 0x1d, 0x37, 0xf7, - 0xfa, 0x41, 0xe4, 0xe7, 0x75, 0x3a, 0x13, 0x32, 0xe0, 0x47, 0xcb, 0x87, 0xf8, 0xc1, 0x20, 0xaa, - 0x9c, 0xea, 0xd6, 0x36, 0xbb, 0xa9, 0xad, 0x7c, 0x06, 0x7b, 0xc8, 0x09, 0xf3, 0x50, 0x10, 0x17, - 0xaa, 0x9c, 0x18, 0x1e, 0x8d, 0x8f, 0xf6, 0x70, 0x34, 0x34, 0x3e, 0xcc, 0xd9, 0xe7, 0x90, 0xf1, - 0x41, 0xc1, 0xe0, 0xf3, 0x8f, 0x61, 0x7c, 0xec, 0xc4, 0x2a, 0xae, 0x6b, 0xe6, 0xd7, 0xa9, 0xb8, - 0x80, 0x4d, 0x28, 0xd1, 0xde, 0x96, 0x9a, 0xe8, 0x5f, 0xbc, 0xa0, 0x1d, 0xe9, 0xe6, 0x60, 0x3e, - 0xe0, 0x1b, 0xaa, 0xf3, 0x5e, 0xdf, 0xa8, 0x4c, 0xdf, 0x1c, 0xcc, 0x07, 0x7c, 0xc3, 0x4f, 0xe9, - 0x7a, 0x7d, 0xa3, 0xdf, 0xd3, 0x6d, 0x0e, 0xe6, 0xc1, 0xf7, 0x5f, 0x8e, 0xa2, 0x1b, 0x1d, 0xe7, - 0x75, 0x0d, 0x94, 0xa8, 0x74, 0x29, 0xa8, 0x52, 0xce, 0xb7, 0x67, 0xd0, 0x50, 0x29, 0xc7, 0xab, - 0x38, 0xdf, 0x5f, 0xa0, 0xa2, 0x38, 0x94, 0x55, 0xaa, 0x2f, 0x8e, 0x9f, 0x0c, 0x30, 0xda, 0xc2, - 0xa1, 0x0d, 0x4b, 0x48, 0xc9, 0x5e, 0xbb, 0x79, 0xa8, 0x7d, 0xd5, 0xf3, 0x61, 0xc0, 0x5e, 0xf7, - 0x8d, 0xcf, 0x8d, 0x81, 0xb4, 0xbd, 0x00, 0xf3, 0x18, 0xf7, 0xe6, 0x2d, 0xd4, 0xab, 0xe4, 0xe5, - 0xdb, 0xa3, 0xe1, 0x0a, 0xe0, 0xfe, 0xaf, 0xdb, 0x9a, 0x1e, 0xfb, 0x87, 0x41, 0xf0, 0x78, 0x88, - 0x45, 0x34, 0x10, 0x9e, 0x5c, 0x4b, 0x07, 0x02, 0xf9, 0x8f, 0x51, 0x74, 0x9b, 0x0c, 0xc4, 0xbf, - 0x83, 0xfd, 0xed, 0x21, 0xb6, 0xe9, 0xbb, 0xd8, 0x1f, 0x7f, 0x1b, 0x55, 0x88, 0xee, 0xef, 0xdb, - 0xad, 0x75, 0xab, 0xa1, 0x5f, 0xc7, 0x7f, 0x59, 0xce, 0x44, 0x09, 0x23, 0x36, 0x94, 0x74, 0x16, - 0xc6, 0xe3, 0xf6, 0xd3, 0x6b, 0x6a, 0x39, 0xdf, 0x0a, 0xf1, 0x60, 0xf8, 0x65, 0x92, 0x13, 0x4f, - 0xc8, 0xb2, 0x43, 0xe3, 0x80, 0x3e, 0xbb, 0xae, 0x1a, 0x37, 0x92, 0x1d, 0x58, 0xff, 0xf4, 0xf8, - 0xc9, 0x40, 0xc3, 0xde, 0x8f, 0x91, 0x3f, 0xb9, 0x9e, 0x12, 0xc4, 0xf2, 0x5f, 0xa3, 0xe8, 0xae, - 0xc7, 0xda, 0x9b, 0x06, 0x74, 0x1e, 0xf2, 0x93, 0x80, 0x7d, 0x4e, 0xc9, 0x04, 0xf7, 0x3b, 0xdf, - 0x4e, 0xd9, 0x7e, 0x58, 0xc3, 0x53, 0xd9, 0x4d, 0x33, 0x25, 0xca, 0xee, 0x87, 0x35, 0x7c, 0xbb, - 0x0d, 0x35, 0xe1, 0x3f, 0xac, 0x11, 0xc0, 0x9d, 0x0f, 0x6b, 0x10, 0x9e, 0xc9, 0x0f, 0x6b, 0x90, - 0xd6, 0x82, 0x1f, 0xd6, 0x08, 0x6b, 0x70, 0x8b, 0x4f, 0x1b, 0x42, 0x73, 0xa2, 0x3d, 0xc8, 0xa2, - 0x7f, 0xc0, 0xfd, 0xf8, 0x3a, 0x2a, 0xcc, 0xf2, 0xdb, 0x70, 0xfa, 0xcd, 0xb0, 0x01, 0xcf, 0xd4, - 0x7b, 0x3b, 0x6c, 0x73, 0x30, 0x0f, 0xbe, 0x7f, 0x06, 0xfb, 0x1e, 0xb3, 0xd8, 0xc8, 0x52, 0x7f, - 0x54, 0x65, 0x3d, 0xb4, 0x78, 0xd4, 0x16, 0xdc, 0x9e, 0x7f, 0x38, 0x0c, 0x66, 0x9a, 0x5b, 0x13, - 0xd0, 0xe9, 0x93, 0x3e, 0x43, 0xa8, 0xcb, 0x37, 0x07, 0xf3, 0xcc, 0x22, 0xd7, 0xf8, 0x6e, 0x7a, - 0x7b, 0x80, 0x31, 0xbf, 0xaf, 0x1f, 0x0d, 0x57, 0x00, 0xf7, 0x4b, 0x28, 0x6a, 0x5d, 0xf7, 0xba, - 0x9f, 0x37, 0xfa, 0x4c, 0x4d, 0xbd, 0x6e, 0x9e, 0x0c, 0xc5, 0x43, 0xe5, 0x8d, 0xbb, 0xc0, 0xf7, - 0x95, 0x37, 0xe4, 0x22, 0xff, 0xc9, 0xf5, 0x94, 0x20, 0x96, 0x7f, 0x1e, 0x45, 0x37, 0xd9, 0x58, - 0x20, 0x0f, 0x3e, 0x1b, 0x6a, 0x19, 0xe5, 0xc3, 0xe7, 0xd7, 0xd6, 0x83, 0xa0, 0xfe, 0x6d, 0x14, - 0xdd, 0x0a, 0x04, 0xd5, 0x24, 0xc8, 0x35, 0xac, 0xfb, 0x89, 0xf2, 0xa3, 0xeb, 0x2b, 0x72, 0xcb, - 0xbd, 0x8b, 0x4f, 0xbb, 0x5f, 0x9c, 0x08, 0xd8, 0x9e, 0xf2, 0x5f, 0x9c, 0xe8, 0xd7, 0xc2, 0xc7, - 0x3f, 0x75, 0x51, 0x02, 0x3b, 0x23, 0xea, 0xf8, 0x47, 0xd7, 0x2c, 0x68, 0x47, 0xb4, 0xd6, 0xcb, - 0x51, 0x4e, 0x9e, 0x5d, 0x16, 0x71, 0x3e, 0xe3, 0x9d, 0x34, 0xf2, 0x7e, 0x27, 0x86, 0xc3, 0xc7, - 0x66, 0xb5, 0xf4, 0x48, 0xb6, 0xdb, 0xbc, 0xfb, 0x9c, 0xbe, 0x41, 0x82, 0xc7, 0x66, 0x1d, 0x94, - 0xf1, 0x06, 0x35, 0x6d, 0xc8, 0x1b, 0x2a, 0x65, 0x1f, 0x0c, 0x41, 0xd1, 0x06, 0xc2, 0x78, 0x33, - 0xa7, 0xf1, 0x0f, 0x43, 0x56, 0x3a, 0x27, 0xf2, 0x1b, 0x03, 0x69, 0xc6, 0xed, 0x54, 0xa8, 0x2f, - 0x44, 0x3c, 0x13, 0x65, 0xd0, 0xad, 0xa1, 0x06, 0xb9, 0x75, 0x69, 0xca, 0xed, 0xb6, 0xcc, 0x16, - 0xe7, 0x39, 0x74, 0x26, 0xeb, 0xd6, 0xa5, 0xfa, 0xdd, 0x22, 0x1a, 0x1f, 0x18, 0x5a, 0xb7, 0xba, - 0xbc, 0x7c, 0x10, 0x36, 0xe3, 0x55, 0x95, 0xeb, 0x83, 0x58, 0xbe, 0x9d, 0x90, 0x46, 0x3d, 0xed, - 0x44, 0x99, 0xb4, 0x31, 0x90, 0xc6, 0x27, 0x77, 0x8e, 0x5b, 0x93, 0x4f, 0x9b, 0x3d, 0xb6, 0x3a, - 0x29, 0xf5, 0x68, 0xb8, 0x02, 0x3e, 0x27, 0x85, 0xac, 0xaa, 0xf7, 0x45, 0xbb, 0x69, 0x96, 0x8d, - 0xd7, 0x03, 0x69, 0xd2, 0x42, 0xc1, 0x73, 0x52, 0x02, 0x66, 0x32, 0xb9, 0x3d, 0x57, 0xcc, 0xc7, - 0x7d, 0x76, 0x34, 0x35, 0x28, 0x93, 0x5d, 0x1a, 0x9d, 0xb7, 0x39, 0x8f, 0xda, 0xb4, 0x76, 0x12, - 0x7e, 0x70, 0x9d, 0x06, 0x6f, 0x0e, 0xe6, 0xd1, 0x45, 0xbc, 0xa6, 0xf4, 0xca, 0x72, 0x87, 0x33, - 0xe1, 0xad, 0x24, 0x77, 0x7b, 0x28, 0x74, 0x66, 0xd9, 0x0c, 0xa3, 0x37, 0xe9, 0x6c, 0x2e, 0x14, - 0x79, 0x87, 0xe4, 0x02, 0xc1, 0x3b, 0x24, 0x04, 0xa2, 0xae, 0x6b, 0xfe, 0x3e, 0x15, 0xea, 0x38, - 0x2e, 0xe7, 0x42, 0xed, 0xcf, 0xa8, 0xae, 0x03, 0x65, 0x87, 0x0a, 0x75, 0x1d, 0x49, 0xa3, 0xd9, - 0xc0, 0xb8, 0x85, 0x5f, 0x54, 0x3f, 0x08, 0x99, 0x41, 0x3f, 0xab, 0x5e, 0x1f, 0xc4, 0xa2, 0x15, - 0xc5, 0x3a, 0x4c, 0xcf, 0x53, 0x45, 0xad, 0x28, 0x8e, 0x8d, 0x1a, 0x09, 0xad, 0x28, 0x5d, 0x94, - 0x6b, 0x5e, 0x5d, 0x23, 0xec, 0xcf, 0xc2, 0xcd, 0x6b, 0x98, 0x61, 0xcd, 0x33, 0x6c, 0xe7, 0xca, - 0x33, 0x37, 0x29, 0xa3, 0x4e, 0x61, 0xb3, 0x4c, 0xe4, 0xb6, 0xfe, 0x91, 0x21, 0x06, 0x43, 0xb3, - 0x0e, 0xa7, 0xe0, 0xfc, 0x8e, 0xc5, 0x70, 0xed, 0xad, 0x6c, 0x51, 0x88, 0xb8, 0x8c, 0xf3, 0x84, - 0xdc, 0x9c, 0x6a, 0x83, 0x1d, 0x32, 0xb4, 0x39, 0x65, 0x35, 0xd0, 0x85, 0xba, 0xff, 0xf3, 0x40, - 0x62, 0x28, 0x98, 0xdf, 0xe1, 0xf9, 0xbf, 0x0e, 0xbc, 0x3f, 0x80, 0xc4, 0x17, 0xea, 0x2d, 0x60, - 0x8e, 0xe5, 0x1b, 0xa7, 0x1f, 0x07, 0x4c, 0xf9, 0x68, 0x68, 0x23, 0xcc, 0xab, 0xa0, 0xa4, 0x36, - 0x05, 0xae, 0x50, 0x3f, 0x15, 0x57, 0x54, 0x52, 0xdb, 0xfa, 0x54, 0x23, 0xa1, 0xa4, 0xee, 0xa2, - 0xa8, 0xce, 0x74, 0xf7, 0x41, 0xab, 0x01, 0x7d, 0x77, 0xeb, 0xb3, 0xd6, 0xcb, 0xa1, 0x91, 0xb3, - 0x93, 0x2e, 0xbd, 0x5b, 0x0c, 0x22, 0xd0, 0x9d, 0x74, 0x49, 0x5f, 0x62, 0xac, 0x0f, 0x62, 0xf1, - 0x65, 0x7d, 0xac, 0xc4, 0x65, 0x7b, 0x8b, 0x4e, 0x84, 0xab, 0xe5, 0x9d, 0x6b, 0xf4, 0x7b, 0xfd, - 0xa0, 0x7d, 0x35, 0xf6, 0xb0, 0x94, 0x89, 0xa8, 0x2a, 0xf8, 0x0c, 0x97, 0xff, 0xee, 0x11, 0xc8, - 0x26, 0xe8, 0x23, 0x5c, 0x77, 0xc2, 0x10, 0xd8, 0xfe, 0x22, 0x7a, 0xf7, 0xb9, 0x9c, 0x4f, 0x45, - 0x3e, 0x1b, 0xff, 0xd0, 0x7f, 0x19, 0x55, 0xce, 0x27, 0xf5, 0x9f, 0x8d, 0xbd, 0x15, 0x4e, 0x6c, - 0x5f, 0xa7, 0xdb, 0x11, 0x27, 0x8b, 0xf9, 0x54, 0xc5, 0xf8, 0x63, 0x5e, 0xfa, 0xef, 0x93, 0x5a, - 0xc0, 0xbc, 0x4e, 0xe7, 0x01, 0xc8, 0xde, 0x71, 0x29, 0x04, 0x69, 0xaf, 0x16, 0x04, 0xed, 0x01, - 0x60, 0x57, 0x5d, 0x63, 0xaf, 0x2e, 0x6c, 0xf1, 0xeb, 0x6f, 0x56, 0x47, 0x4b, 0x99, 0x55, 0xb7, - 0x4b, 0xd9, 0x64, 0x68, 0x9a, 0xaf, 0x3f, 0xb4, 0xb0, 0x38, 0x3f, 0x8f, 0xcb, 0x2b, 0x94, 0x0c, - 0xd0, 0x4a, 0x07, 0x60, 0x92, 0x81, 0x04, 0x6d, 0x96, 0xb7, 0x8f, 0x39, 0x39, 0xdb, 0x93, 0xa5, - 0x5c, 0xa8, 0x34, 0x17, 0x15, 0xca, 0x72, 0xf3, 0x40, 0x5d, 0x86, 0xc9, 0x72, 0x8e, 0xb5, 0x55, - 0xa1, 0x26, 0x9a, 0x37, 0xf3, 0xf4, 0xc7, 0x2c, 0x2b, 0x25, 0x4b, 0x7c, 0x3b, 0xd8, 0x58, 0xc1, - 0x10, 0x53, 0x15, 0xb2, 0x30, 0xea, 0xfb, 0xc3, 0x34, 0x9f, 0x93, 0x7d, 0x7f, 0xe8, 0x7e, 0xba, - 0xed, 0x16, 0x0f, 0xd8, 0xf9, 0xbd, 0x79, 0x68, 0xcd, 0x07, 0x6d, 0xe0, 0xd7, 0x82, 0xe4, 0x43, - 0x77, 0x09, 0x66, 0x7e, 0xa7, 0x49, 0xe4, 0xea, 0x65, 0x21, 0x72, 0x31, 0x6b, 0xdf, 0x3f, 0xa3, - 0x5c, 0x79, 0x44, 0xd0, 0x15, 0x26, 0x6d, 0x2a, 0xbc, 0x10, 0xaa, 0x4c, 0x93, 0x6a, 0x2a, 0xd4, - 0x61, 0x5c, 0xc6, 0xe7, 0x42, 0x89, 0x12, 0xa7, 0x02, 0x20, 0x13, 0x8f, 0x61, 0x52, 0x81, 0x63, - 0xc1, 0xe1, 0xef, 0x46, 0xdf, 0xab, 0x67, 0x42, 0x91, 0xc3, 0xd7, 0xb5, 0x9f, 0xe9, 0xcf, 0xf2, - 0x8f, 0xdf, 0x37, 0x36, 0xa6, 0xaa, 0x14, 0xf1, 0x79, 0x6b, 0xfb, 0x3d, 0xf3, 0x77, 0x0d, 0x3e, - 0x1a, 0xd5, 0xeb, 0xc0, 0x81, 0x54, 0xe9, 0xdb, 0x7a, 0xe3, 0x01, 0xd7, 0x3c, 0x68, 0x1d, 0x70, - 0xc5, 0x93, 0xc0, 0x6f, 0xcb, 0x29, 0xce, 0x8e, 0x44, 0x57, 0x7a, 0x24, 0x8a, 0x0c, 0x8f, 0x44, - 0x4f, 0x5b, 0x03, 0xcc, 0x48, 0x24, 0x41, 0xbb, 0xa8, 0xb9, 0xe2, 0x63, 0x11, 0x6e, 0xcc, 0xb1, - 0x18, 0xd6, 0x98, 0x63, 0xef, 0x05, 0xf6, 0x7a, 0x4f, 0x56, 0xca, 0x78, 0x96, 0xc4, 0x95, 0x3a, - 0x8c, 0xaf, 0x32, 0x19, 0xcf, 0xf4, 0xc3, 0xc4, 0x7b, 0xb2, 0x96, 0x99, 0xb8, 0x10, 0xb7, 0x27, - 0xe3, 0xe0, 0xc6, 0xe7, 0xd3, 0x0f, 0xff, 0xe7, 0xeb, 0x95, 0xd1, 0x57, 0x5f, 0xaf, 0x8c, 0xfe, - 0xff, 0xeb, 0x95, 0xd1, 0x3f, 0x7d, 0xb3, 0xf2, 0xce, 0x57, 0xdf, 0xac, 0xbc, 0xf3, 0xbf, 0xdf, - 0xac, 0xbc, 0xf3, 0xe5, 0xbb, 0xf0, 0x8f, 0x38, 0x9c, 0xfc, 0x82, 0xfe, 0xa7, 0x18, 0x9e, 0xfc, - 0x3c, 0x00, 0x00, 0xff, 0xff, 0x59, 0xc7, 0x20, 0xda, 0xe8, 0x61, 0x00, 0x00, + 0x56, 0xc0, 0xe7, 0xbe, 0x30, 0xd0, 0xcb, 0x0e, 0x70, 0x07, 0x86, 0xd9, 0xb0, 0xeb, 0x64, 0x32, + 0x89, 0x9d, 0xc4, 0x71, 0xdb, 0x93, 0xcc, 0xc7, 0xb2, 0x8b, 0x84, 0x1c, 0x3b, 0xf6, 0x98, 0x8d, + 0x1d, 0xe3, 0x7b, 0x9d, 0x48, 0x23, 0x21, 0xd1, 0xee, 0x7b, 0x7c, 0xdd, 0xb8, 0x6f, 0x57, 0x6f, + 0x77, 0xdd, 0x6b, 0xdf, 0x45, 0x20, 0x10, 0x08, 0x04, 0x02, 0x81, 0xf8, 0x92, 0x10, 0x0f, 0x48, + 0xfc, 0x35, 0xbc, 0x20, 0xed, 0x23, 0x8f, 0x68, 0xe6, 0x1f, 0x41, 0x5d, 0x5d, 0x5d, 0x1f, 0xa7, + 0xeb, 0x54, 0xf7, 0x9d, 0xa7, 0x19, 0xf9, 0xfc, 0xce, 0x47, 0x75, 0x9d, 0xaa, 0x3a, 0x55, 0xd5, + 0xb7, 0x13, 0xdc, 0xcd, 0x2f, 0xb6, 0xf3, 0x82, 0x71, 0x56, 0x6e, 0x97, 0x50, 0x2c, 0x92, 0x18, + 0x9a, 0xff, 0x86, 0xe2, 0xcf, 0xc3, 0x77, 0xa3, 0x6c, 0xc9, 0x97, 0x39, 0xdc, 0xf9, 0x50, 0x93, + 0x31, 0x9b, 0xcd, 0xa2, 0x6c, 0x52, 0xd6, 0xc8, 0x9d, 0x0f, 0xb4, 0x04, 0x16, 0x90, 0x71, 0xf9, + 0xf7, 0x67, 0xff, 0xfe, 0x3f, 0x83, 0xe0, 0xbd, 0xbd, 0x34, 0x81, 0x8c, 0xef, 0x49, 0x8d, 0xe1, + 0x57, 0xc1, 0x77, 0x77, 0xf3, 0xfc, 0x10, 0xf8, 0x1b, 0x28, 0xca, 0x84, 0x65, 0xc3, 0x8f, 0x43, + 0xe9, 0x20, 0x3c, 0xcb, 0xe3, 0x70, 0x37, 0xcf, 0x43, 0x2d, 0x0c, 0xcf, 0xe0, 0xa7, 0x73, 0x28, + 0xf9, 0x9d, 0x07, 0x7e, 0xa8, 0xcc, 0x59, 0x56, 0xc2, 0xf0, 0x32, 0xf8, 0xb5, 0xdd, 0x3c, 0x1f, + 0x01, 0xdf, 0x87, 0xaa, 0x01, 0x23, 0x1e, 0x71, 0x18, 0x6e, 0xb4, 0x54, 0x6d, 0x40, 0xf9, 0x78, + 0xd4, 0x0d, 0x4a, 0x3f, 0xe3, 0xe0, 0x3b, 0x95, 0x9f, 0xab, 0x39, 0x9f, 0xb0, 0x9b, 0x6c, 0xf8, + 0x51, 0x5b, 0x51, 0x8a, 0x94, 0xed, 0xfb, 0x3e, 0x44, 0x5a, 0x7d, 0x1b, 0xfc, 0xf2, 0xdb, 0x28, + 0x4d, 0x81, 0xef, 0x15, 0x50, 0x05, 0x6e, 0xeb, 0xd4, 0xa2, 0xb0, 0x96, 0x29, 0xbb, 0x1f, 0x7b, + 0x19, 0x69, 0xf8, 0xab, 0xe0, 0xbb, 0xb5, 0xe4, 0x0c, 0x62, 0xb6, 0x80, 0x62, 0xe8, 0xd4, 0x92, + 0x42, 0xe2, 0x91, 0xb7, 0x20, 0x6c, 0x7b, 0x8f, 0x65, 0x0b, 0x28, 0xb8, 0xdb, 0xb6, 0x14, 0xfa, + 0x6d, 0x6b, 0x48, 0xda, 0xfe, 0x9b, 0x41, 0xf0, 0xfd, 0xdd, 0x38, 0x66, 0xf3, 0x8c, 0xbf, 0x62, + 0x71, 0x94, 0xbe, 0x4a, 0xb2, 0xeb, 0x13, 0xb8, 0xd9, 0xbb, 0xaa, 0xf8, 0x6c, 0x0a, 0xc3, 0xe7, + 0xf6, 0x53, 0xad, 0xd1, 0x50, 0xb1, 0xa1, 0x09, 0x2b, 0xdf, 0x9f, 0xae, 0xa6, 0x24, 0x63, 0xf9, + 0x87, 0x41, 0xb0, 0x86, 0x63, 0x19, 0xb1, 0x74, 0x01, 0x3a, 0x9a, 0xcf, 0x3a, 0x0c, 0xdb, 0xb8, + 0x8a, 0xe7, 0xf3, 0x55, 0xd5, 0x64, 0x44, 0x69, 0xf0, 0xbe, 0x99, 0x2e, 0x23, 0x28, 0xc5, 0x70, + 0x7a, 0x4c, 0x67, 0x84, 0x44, 0x94, 0xe7, 0x27, 0x7d, 0x50, 0xe9, 0x2d, 0x09, 0x86, 0xd2, 0x5b, + 0xca, 0x4a, 0xe5, 0xec, 0x91, 0xd3, 0x82, 0x41, 0x28, 0x5f, 0x8f, 0x7b, 0x90, 0xd2, 0xd5, 0x1f, + 0x06, 0xbf, 0xf2, 0x96, 0x15, 0xd7, 0x65, 0x1e, 0xc5, 0x20, 0x87, 0xc2, 0x43, 0x5b, 0xbb, 0x91, + 0xe2, 0xd1, 0xb0, 0xde, 0x85, 0x19, 0x49, 0xdb, 0x08, 0x5f, 0xe7, 0x80, 0xe7, 0x20, 0xad, 0x58, + 0x09, 0xa9, 0xa4, 0xc5, 0x90, 0xb4, 0x7d, 0x1d, 0x0c, 0xb5, 0xed, 0x8b, 0x3f, 0x82, 0x98, 0xef, + 0x4e, 0x26, 0xb8, 0x57, 0xb4, 0xae, 0x20, 0xc2, 0xdd, 0xc9, 0x84, 0xea, 0x15, 0x37, 0x2a, 0x9d, + 0xdd, 0x04, 0x1f, 0x20, 0x67, 0xaf, 0x92, 0x52, 0x38, 0xdc, 0xf2, 0x5b, 0x91, 0x98, 0x72, 0x1a, + 0xf6, 0xc5, 0xa5, 0xe3, 0x3f, 0x1b, 0x04, 0xdf, 0x73, 0x78, 0x3e, 0x83, 0x19, 0x5b, 0xc0, 0x70, + 0xa7, 0xdb, 0x5a, 0x4d, 0x2a, 0xff, 0x9f, 0xac, 0xa0, 0xe1, 0x48, 0x93, 0x11, 0xa4, 0x10, 0x73, + 0x32, 0x4d, 0x6a, 0x71, 0x67, 0x9a, 0x28, 0xcc, 0x18, 0x61, 0x8d, 0xf0, 0x10, 0xf8, 0xde, 0xbc, + 0x28, 0x20, 0xe3, 0x64, 0x5f, 0x6a, 0xa4, 0xb3, 0x2f, 0x2d, 0xd4, 0xd1, 0x9e, 0x43, 0xe0, 0xbb, + 0x69, 0x4a, 0xb6, 0xa7, 0x16, 0x77, 0xb6, 0x47, 0x61, 0xd2, 0x43, 0x1c, 0xfc, 0xaa, 0xf1, 0xc4, + 0xf8, 0x51, 0x76, 0xc9, 0x86, 0xf4, 0xb3, 0x10, 0x72, 0xe5, 0x63, 0xa3, 0x93, 0x73, 0x34, 0xe3, + 0xe5, 0x6d, 0xce, 0x0a, 0xba, 0x5b, 0x6a, 0x71, 0x67, 0x33, 0x14, 0x26, 0x3d, 0xfc, 0x41, 0xf0, + 0x9e, 0x9c, 0x25, 0x9b, 0xf5, 0xec, 0x81, 0x73, 0x0a, 0xc5, 0x0b, 0xda, 0xc3, 0x0e, 0x4a, 0x4f, + 0x0e, 0x52, 0x26, 0x27, 0x9f, 0x8f, 0x9d, 0x7a, 0x68, 0xea, 0x79, 0xe0, 0x87, 0x5a, 0xb6, 0xf7, + 0x21, 0x05, 0xd2, 0x76, 0x2d, 0xec, 0xb0, 0xad, 0x20, 0x69, 0xbb, 0x08, 0x7e, 0x43, 0x3d, 0x96, + 0x6a, 0x1d, 0x15, 0xf2, 0x6a, 0x92, 0xde, 0x24, 0xda, 0x6d, 0x42, 0xca, 0xd7, 0xd3, 0x7e, 0x70, + 0xab, 0x3d, 0x72, 0x04, 0xba, 0xdb, 0x83, 0xc6, 0xdf, 0x03, 0x3f, 0x24, 0x6d, 0xff, 0xed, 0x20, + 0xf8, 0x81, 0x94, 0xbd, 0xcc, 0xa2, 0x8b, 0x14, 0xc4, 0x92, 0x78, 0x02, 0xfc, 0x86, 0x15, 0xd7, + 0xa3, 0x65, 0x16, 0x13, 0xcb, 0xbf, 0x1b, 0xee, 0x58, 0xfe, 0x49, 0x25, 0xa3, 0xe2, 0x93, 0x0d, + 0xe5, 0x2c, 0xc7, 0x15, 0x5f, 0xd3, 0x02, 0xce, 0x72, 0xaa, 0xe2, 0xb3, 0x91, 0x96, 0xd5, 0xe3, + 0x6a, 0xda, 0x74, 0x5b, 0x3d, 0x36, 0xe7, 0xc9, 0xfb, 0x3e, 0x44, 0x4f, 0x5b, 0x4d, 0x02, 0xb3, + 0xec, 0x32, 0x99, 0x9e, 0xe7, 0x93, 0x2a, 0x8d, 0x1f, 0xbb, 0x33, 0xd4, 0x40, 0x88, 0x69, 0x8b, + 0x40, 0xa5, 0xb7, 0xbf, 0xd7, 0x85, 0x91, 0x1c, 0x4a, 0x07, 0x05, 0x9b, 0xbd, 0x82, 0x69, 0x14, + 0x2f, 0xe5, 0xf8, 0xff, 0xd4, 0x37, 0xf0, 0x30, 0xad, 0x82, 0xf8, 0x6c, 0x45, 0x2d, 0x19, 0xcf, + 0x7f, 0x0e, 0x82, 0x07, 0x4d, 0xf3, 0xaf, 0xa2, 0x6c, 0x0a, 0xb2, 0x3f, 0xeb, 0xe8, 0x77, 0xb3, + 0xc9, 0x19, 0x94, 0x3c, 0x2a, 0xf8, 0xf0, 0x47, 0xee, 0x46, 0xfa, 0x74, 0x54, 0x6c, 0x3f, 0xfe, + 0x56, 0xba, 0xba, 0xd7, 0x47, 0xd5, 0xc4, 0x26, 0xa7, 0x00, 0xbb, 0xd7, 0x85, 0x04, 0x4f, 0x00, + 0xf7, 0x7d, 0x88, 0xee, 0x75, 0x21, 0x38, 0xca, 0x16, 0x09, 0x87, 0x43, 0xc8, 0xa0, 0x68, 0xf7, + 0x7a, 0xad, 0x6a, 0x23, 0x44, 0xaf, 0x13, 0xa8, 0x9e, 0x6c, 0x2c, 0x6f, 0x6a, 0x71, 0xdc, 0xf4, + 0x18, 0x69, 0x2d, 0x8f, 0x4f, 0xfb, 0xc1, 0x7a, 0x77, 0x67, 0xf8, 0x3c, 0x83, 0x05, 0xbb, 0xc6, + 0xbb, 0x3b, 0xd3, 0x44, 0x0d, 0x10, 0xbb, 0x3b, 0x27, 0xa8, 0x57, 0x30, 0xc3, 0xcf, 0x9b, 0x04, + 0x6e, 0xd0, 0x0a, 0x66, 0x2a, 0x57, 0x62, 0x62, 0x05, 0x73, 0x60, 0xd2, 0xc3, 0x49, 0xf0, 0x4b, + 0x42, 0xf8, 0x7b, 0x2c, 0xc9, 0x86, 0x77, 0x1d, 0x4a, 0x95, 0x40, 0x59, 0xbd, 0x47, 0x03, 0x28, + 0xe2, 0xea, 0xaf, 0x7b, 0x51, 0x16, 0x43, 0xea, 0x8c, 0x58, 0x8b, 0xbd, 0x11, 0x5b, 0x18, 0x8a, + 0xf8, 0xe5, 0x6d, 0xc2, 0x9d, 0x11, 0x57, 0x02, 0x6f, 0xc4, 0x12, 0xd0, 0xa5, 0x88, 0xf8, 0x73, + 0x35, 0x1f, 0x8e, 0xae, 0xa2, 0x22, 0xc9, 0xa6, 0x43, 0x57, 0x2c, 0x86, 0x9c, 0x28, 0x45, 0x5c, + 0x1c, 0x1a, 0x12, 0x52, 0x71, 0x37, 0xcf, 0x8b, 0x6a, 0x9a, 0x75, 0x0d, 0x09, 0x1b, 0xf1, 0x0e, + 0x89, 0x16, 0xea, 0xf6, 0xb6, 0x0f, 0x71, 0x9a, 0x64, 0x5e, 0x6f, 0x12, 0xe9, 0xe3, 0x4d, 0xa3, + 0xd2, 0xdb, 0x3c, 0xf8, 0x40, 0x00, 0xa7, 0x51, 0xc1, 0x93, 0x38, 0xc9, 0xa3, 0xac, 0x29, 0xbe, + 0x5d, 0x83, 0xaa, 0x45, 0x29, 0x9f, 0x5b, 0x3d, 0x69, 0xe9, 0xf6, 0xdf, 0x06, 0xc1, 0x47, 0xd8, + 0xef, 0x29, 0x14, 0xb3, 0x44, 0xec, 0xe1, 0xca, 0x7a, 0x06, 0x1c, 0x7e, 0xe1, 0x37, 0xda, 0x52, + 0x50, 0xd1, 0xfc, 0x70, 0x75, 0x45, 0x19, 0xd8, 0xef, 0x07, 0x41, 0xbd, 0x57, 0x10, 0xfb, 0x39, + 0x3b, 0x01, 0xe5, 0x26, 0xc2, 0xda, 0xcc, 0x7d, 0xe4, 0x21, 0xf4, 0x3c, 0x5d, 0xff, 0x5d, 0x6c, + 0x53, 0x87, 0x4e, 0x0d, 0x21, 0x22, 0xe6, 0x69, 0x84, 0xe0, 0x40, 0x47, 0x57, 0xec, 0xc6, 0x1d, + 0x68, 0x25, 0xf1, 0x07, 0x2a, 0x09, 0x7d, 0x70, 0x24, 0x03, 0x75, 0x1d, 0x1c, 0x35, 0x61, 0xf8, + 0x0e, 0x8e, 0x30, 0x23, 0x0d, 0xb3, 0xe0, 0xd7, 0x4d, 0xc3, 0x2f, 0x18, 0xbb, 0x9e, 0x45, 0xc5, + 0xf5, 0xf0, 0x09, 0xad, 0xdc, 0x30, 0xca, 0xd1, 0x66, 0x2f, 0x56, 0x8f, 0x21, 0xd3, 0x61, 0xb5, + 0xca, 0x9f, 0x17, 0x29, 0x1a, 0x43, 0x96, 0x0d, 0x89, 0x10, 0x63, 0x88, 0x40, 0xf5, 0xb4, 0x69, + 0x7a, 0x1b, 0x01, 0xde, 0xaa, 0x58, 0xea, 0x23, 0xa0, 0xb6, 0x2a, 0x0e, 0x0c, 0xa7, 0xd0, 0x61, + 0x11, 0xe5, 0x57, 0xee, 0x14, 0x12, 0x22, 0x7f, 0x0a, 0x35, 0x08, 0xee, 0xef, 0x11, 0x44, 0x45, + 0x7c, 0xe5, 0xee, 0xef, 0x5a, 0xe6, 0xef, 0x6f, 0xc5, 0xe8, 0x55, 0xdd, 0x34, 0x3c, 0x9a, 0x5f, + 0x94, 0x71, 0x91, 0x5c, 0xc0, 0x70, 0x93, 0xd6, 0x56, 0x10, 0xb1, 0xaa, 0x93, 0xb0, 0x3e, 0x58, + 0x92, 0x3e, 0x1b, 0xd9, 0xd1, 0xa4, 0x44, 0x07, 0x4b, 0x8d, 0x0d, 0x83, 0x20, 0x0e, 0x96, 0xdc, + 0x24, 0x6e, 0xde, 0x61, 0xc1, 0xe6, 0x79, 0xd9, 0xd1, 0x3c, 0x04, 0xf9, 0x9b, 0xd7, 0x86, 0xa5, + 0xcf, 0xdb, 0xe0, 0x37, 0xcd, 0x47, 0x7a, 0x9e, 0x95, 0xca, 0xeb, 0x16, 0xfd, 0x9c, 0x0c, 0x8c, + 0x38, 0xa2, 0xf1, 0xe0, 0x7a, 0x89, 0x6d, 0x3c, 0xf3, 0x7d, 0xe0, 0x51, 0x92, 0x96, 0xc3, 0x75, + 0xb7, 0x8d, 0x46, 0x4e, 0x2c, 0xb1, 0x2e, 0x0e, 0x0f, 0xa1, 0xfd, 0x79, 0x9e, 0x26, 0x71, 0xfb, + 0xac, 0x4e, 0xea, 0x2a, 0xb1, 0x7f, 0x08, 0x99, 0x18, 0x9e, 0x12, 0x46, 0xc0, 0xeb, 0xff, 0x19, + 0x2f, 0x73, 0x70, 0x4f, 0x09, 0x16, 0xe2, 0x9f, 0x12, 0x30, 0x8a, 0xdb, 0x33, 0x02, 0xfe, 0x2a, + 0x5a, 0xb2, 0x39, 0x31, 0x25, 0x28, 0xb1, 0xbf, 0x3d, 0x26, 0xa6, 0x17, 0x6e, 0xe5, 0xe1, 0x28, + 0xe3, 0x50, 0x64, 0x51, 0x7a, 0x90, 0x46, 0xd3, 0x72, 0x48, 0x8c, 0x1b, 0x9b, 0x22, 0x16, 0x6e, + 0x9a, 0x76, 0x3c, 0xc6, 0xa3, 0xf2, 0x20, 0x5a, 0xb0, 0x22, 0xe1, 0xf4, 0x63, 0xd4, 0x48, 0xe7, + 0x63, 0xb4, 0x50, 0xa7, 0xb7, 0xdd, 0x22, 0xbe, 0x4a, 0x16, 0x30, 0xf1, 0x78, 0x6b, 0x90, 0x1e, + 0xde, 0x0c, 0xd4, 0xd1, 0x69, 0x23, 0x36, 0x2f, 0x62, 0x20, 0x3b, 0xad, 0x16, 0x77, 0x76, 0x9a, + 0xc2, 0xa4, 0x87, 0xbf, 0x1c, 0x04, 0xbf, 0x55, 0x4b, 0xcd, 0x03, 0xb4, 0xfd, 0xa8, 0xbc, 0xba, + 0x60, 0x51, 0x31, 0x19, 0x7e, 0xe2, 0xb2, 0xe3, 0x44, 0x95, 0xeb, 0x67, 0xab, 0xa8, 0xe0, 0xc7, + 0xfa, 0x2a, 0x29, 0x8d, 0x11, 0xe7, 0x7c, 0xac, 0x16, 0xe2, 0x7f, 0xac, 0x18, 0xc5, 0x13, 0x88, + 0x90, 0xd7, 0x9b, 0xd5, 0x75, 0x52, 0xdf, 0xde, 0xb1, 0x6e, 0x74, 0x72, 0x78, 0x7e, 0xac, 0x84, + 0x76, 0xb6, 0x6c, 0x51, 0x36, 0xdc, 0x19, 0x13, 0xf6, 0xc5, 0x49, 0xcf, 0x6a, 0x54, 0xf8, 0x3d, + 0xb7, 0x46, 0x46, 0xd8, 0x17, 0x27, 0x3c, 0x1b, 0xd3, 0x9a, 0xcf, 0xb3, 0x63, 0x6a, 0x0b, 0xfb, + 0xe2, 0x38, 0x81, 0x76, 0xf3, 0x3c, 0x5d, 0x8e, 0x61, 0x96, 0xa7, 0x64, 0x02, 0x59, 0x88, 0x3f, + 0x81, 0x30, 0x8a, 0xab, 0x9f, 0x31, 0xab, 0x6a, 0x2b, 0x67, 0xf5, 0x23, 0x44, 0xfe, 0xea, 0xa7, + 0x41, 0x70, 0xc1, 0x30, 0x66, 0x7b, 0x2c, 0x4d, 0x21, 0xe6, 0xed, 0x9b, 0x28, 0xa5, 0xa9, 0x09, + 0x7f, 0xc1, 0x80, 0x48, 0x7d, 0xe2, 0xd0, 0xd4, 0xea, 0x51, 0x01, 0x2f, 0x96, 0xaf, 0x92, 0xec, + 0x7a, 0xe8, 0x5e, 0x1b, 0x35, 0x40, 0x9c, 0x38, 0x38, 0x41, 0xbc, 0x27, 0x38, 0xcf, 0x26, 0xcc, + 0xbd, 0x27, 0xa8, 0x24, 0xfe, 0x3d, 0x81, 0x24, 0xb0, 0xc9, 0x33, 0xa0, 0x4c, 0x56, 0x12, 0xbf, + 0x49, 0x49, 0xb8, 0xe6, 0x03, 0x79, 0xb4, 0x47, 0xce, 0x07, 0xe8, 0x30, 0x6f, 0xa3, 0x93, 0xc3, + 0x19, 0xda, 0x6c, 0x0e, 0x0e, 0x80, 0xc7, 0x57, 0xee, 0x0c, 0xb5, 0x10, 0x7f, 0x86, 0x62, 0x14, + 0x37, 0x69, 0xcc, 0xd4, 0xe6, 0x66, 0xdd, 0x9d, 0x1f, 0xad, 0x8d, 0xcd, 0x46, 0x27, 0x87, 0xcb, + 0xf5, 0xa3, 0x99, 0x78, 0x66, 0xce, 0x24, 0xaf, 0x65, 0xfe, 0x72, 0x5d, 0x31, 0x38, 0xfa, 0x5a, + 0x50, 0x3d, 0x4e, 0x77, 0xf4, 0x5a, 0xee, 0x8f, 0xde, 0xe2, 0xa4, 0x93, 0x7f, 0x19, 0x04, 0x77, + 0x4d, 0x2f, 0x27, 0xac, 0x1a, 0x23, 0x6f, 0xa2, 0x34, 0x99, 0x44, 0x1c, 0xc6, 0xec, 0x1a, 0x32, + 0xb4, 0xdf, 0xb7, 0xa3, 0xad, 0xf9, 0xd0, 0x52, 0x20, 0xf6, 0xfb, 0xbd, 0x14, 0x71, 0x9e, 0xd4, + 0xf4, 0x79, 0x09, 0x7b, 0x51, 0x49, 0xcc, 0x64, 0x16, 0xe2, 0xcf, 0x13, 0x8c, 0xe2, 0xa2, 0xad, + 0x96, 0xbf, 0xbc, 0xcd, 0xa1, 0x48, 0x20, 0x8b, 0xc1, 0x5d, 0xb4, 0x61, 0xca, 0x5f, 0xb4, 0x39, + 0xe8, 0xd6, 0x76, 0x58, 0x4d, 0x4e, 0xed, 0xcb, 0x64, 0x4c, 0x78, 0x2e, 0x93, 0x09, 0x14, 0x37, + 0x52, 0x03, 0xce, 0x23, 0xa5, 0x96, 0x15, 0xef, 0x91, 0x12, 0x4d, 0xb7, 0x0e, 0x19, 0x14, 0x33, + 0xaa, 0x86, 0x49, 0x47, 0xe8, 0x23, 0x73, 0xb8, 0x6c, 0xf6, 0x62, 0xdd, 0xa7, 0x1a, 0x67, 0x90, + 0x46, 0x62, 0x09, 0xf1, 0x1c, 0x1d, 0x34, 0x4c, 0x9f, 0x53, 0x0d, 0x83, 0x95, 0x0e, 0xff, 0x7c, + 0x10, 0xdc, 0x71, 0x79, 0x7c, 0x9d, 0x0b, 0xbf, 0x3b, 0xdd, 0xb6, 0x6a, 0x92, 0xb8, 0x2d, 0xf7, + 0x6b, 0xc8, 0x18, 0xfe, 0x38, 0xf8, 0xb0, 0x11, 0xe9, 0xcb, 0x74, 0x19, 0x80, 0x5d, 0x45, 0xa8, + 0xf8, 0x31, 0xa7, 0xdc, 0x6f, 0xf7, 0xe6, 0x75, 0x81, 0x6e, 0xc7, 0x55, 0xa2, 0x02, 0x5d, 0xd9, + 0x90, 0x62, 0xa2, 0x40, 0x77, 0x60, 0x78, 0xa5, 0x6e, 0x90, 0x6a, 0x9c, 0xb8, 0xe6, 0x38, 0x65, + 0xc2, 0x1c, 0x25, 0x8f, 0xba, 0x41, 0x9c, 0x3b, 0x8d, 0x58, 0xd6, 0xc5, 0x4f, 0x7c, 0x16, 0x50, + 0x6d, 0xbc, 0xd9, 0x8b, 0x95, 0x0e, 0xff, 0x34, 0xf8, 0x5e, 0xab, 0x61, 0x07, 0x10, 0xf1, 0x79, + 0x01, 0x93, 0xe1, 0x76, 0x47, 0xdc, 0x0d, 0xa8, 0x5c, 0xef, 0xf4, 0x57, 0x90, 0xfe, 0xff, 0x7a, + 0x10, 0x7c, 0xdf, 0xe6, 0xea, 0x2e, 0x56, 0x31, 0x3c, 0xf3, 0x99, 0xb4, 0x59, 0x15, 0xc6, 0xf3, + 0x95, 0x74, 0x5a, 0x7b, 0x30, 0x33, 0x91, 0x77, 0x17, 0x51, 0x92, 0x46, 0x17, 0x29, 0x38, 0xf7, + 0x60, 0x56, 0x6e, 0x2a, 0xd4, 0xbb, 0x07, 0x23, 0x55, 0x5a, 0xb3, 0xa4, 0x18, 0x6f, 0x46, 0xed, + 0xfe, 0x94, 0x1e, 0x95, 0x8e, 0xd2, 0x7d, 0xab, 0x27, 0x2d, 0xdd, 0xf2, 0xe6, 0xec, 0xaa, 0xfa, + 0xb3, 0x99, 0xe4, 0x2e, 0xaf, 0x52, 0xd5, 0x91, 0xe9, 0x5b, 0x3d, 0x69, 0xe9, 0xf5, 0x4f, 0x82, + 0x0f, 0xdb, 0x5e, 0xe5, 0xa2, 0xb0, 0xdd, 0x69, 0x0a, 0xad, 0x0b, 0x3b, 0xfd, 0x15, 0x74, 0xa9, + 0xff, 0x65, 0x52, 0x72, 0x56, 0x2c, 0x47, 0x57, 0xec, 0xa6, 0x79, 0x61, 0xd4, 0x1e, 0xad, 0x12, + 0x08, 0x0d, 0x82, 0x28, 0xf5, 0xdd, 0x64, 0xcb, 0x95, 0x7e, 0xb1, 0xb4, 0x24, 0x5c, 0x19, 0x44, + 0x87, 0x2b, 0x9b, 0xd4, 0x73, 0x55, 0xd3, 0x2a, 0xfd, 0x16, 0xec, 0x86, 0x3b, 0xd4, 0xf6, 0x9b, + 0xb0, 0x8f, 0xba, 0x41, 0xbd, 0xfd, 0x3a, 0x48, 0x52, 0x78, 0x7d, 0x79, 0x99, 0xb2, 0x68, 0x82, + 0xb6, 0x5f, 0x95, 0x24, 0x94, 0x22, 0x62, 0xfb, 0x85, 0x10, 0x5d, 0x74, 0x56, 0x02, 0x71, 0x37, + 0xd3, 0x98, 0x5e, 0x6f, 0xeb, 0x99, 0x72, 0xa2, 0xe8, 0x74, 0x71, 0x7a, 0xc1, 0xa8, 0xa4, 0xd5, + 0x10, 0x6c, 0x7c, 0x3c, 0x6c, 0xeb, 0x1a, 0x62, 0x62, 0xc1, 0x70, 0x60, 0x7a, 0x7f, 0x54, 0x09, + 0xcf, 0x73, 0x61, 0xfc, 0x5e, 0x5b, 0xab, 0x96, 0x10, 0xfb, 0x23, 0x9b, 0xd0, 0x75, 0x7e, 0xf5, + 0xf7, 0x7d, 0x76, 0x93, 0x09, 0xa3, 0x8e, 0xa7, 0xd9, 0xc8, 0x88, 0x3a, 0x1f, 0x33, 0xd2, 0xf0, + 0x4f, 0x82, 0x5f, 0x14, 0x86, 0x0b, 0x96, 0x0f, 0xd7, 0x1c, 0x0a, 0x85, 0xf1, 0xda, 0xc9, 0x5d, + 0x52, 0xae, 0xdf, 0x9e, 0x52, 0xfd, 0x77, 0x5e, 0x46, 0x53, 0x40, 0x6f, 0x4f, 0xe9, 0x5e, 0x11, + 0x52, 0xe2, 0xed, 0xa9, 0x36, 0xa5, 0xdf, 0x08, 0xaa, 0x64, 0x27, 0x6c, 0x22, 0xad, 0x3b, 0x5a, + 0xa8, 0x84, 0xc4, 0x1b, 0x41, 0x2d, 0x48, 0x9f, 0xdf, 0x9f, 0x44, 0x8b, 0x64, 0xaa, 0x26, 0xff, + 0x7a, 0x0e, 0x29, 0xd1, 0xf9, 0xbd, 0x66, 0x42, 0x03, 0x22, 0xce, 0xef, 0x49, 0x58, 0xfa, 0xfc, + 0xe7, 0x41, 0x70, 0x4f, 0x33, 0x87, 0xcd, 0xb1, 0xca, 0x51, 0x76, 0xc9, 0xde, 0x26, 0xfc, 0xaa, + 0xda, 0xc7, 0x97, 0xc3, 0xcf, 0x29, 0x93, 0x6e, 0x5e, 0x85, 0xf2, 0xc5, 0xca, 0x7a, 0xba, 0x9a, + 0x6b, 0x8e, 0x5b, 0xf4, 0xdd, 0x56, 0xad, 0x81, 0xaa, 0x39, 0x75, 0x2a, 0x83, 0x39, 0xa2, 0x9a, + 0xf3, 0xf1, 0xba, 0x8b, 0x95, 0xf3, 0x94, 0x65, 0xb8, 0x8b, 0xb5, 0x85, 0x4a, 0x48, 0x74, 0x71, + 0x0b, 0xd2, 0x73, 0x63, 0x23, 0xaa, 0x4f, 0x06, 0x76, 0xd3, 0x14, 0xcd, 0x8d, 0x4a, 0x55, 0x01, + 0xc4, 0xdc, 0xe8, 0x04, 0xa5, 0x9f, 0xb3, 0xe0, 0x3b, 0xd5, 0x23, 0x3d, 0x2d, 0x60, 0x91, 0x00, + 0xbe, 0x86, 0x35, 0x24, 0xc4, 0xf8, 0xb7, 0x09, 0x3d, 0xb2, 0xce, 0xb3, 0x32, 0x4f, 0xa3, 0xf2, + 0x4a, 0x5e, 0xcc, 0xd9, 0x6d, 0x6e, 0x84, 0xf8, 0x6a, 0xee, 0x61, 0x07, 0xa5, 0x27, 0xde, 0x46, + 0xa6, 0xa6, 0x98, 0x75, 0xb7, 0x6a, 0x6b, 0x9a, 0xd9, 0xe8, 0xe4, 0xf4, 0xd1, 0xe4, 0x61, 0x94, + 0xa6, 0x50, 0x2c, 0x1b, 0xd9, 0x71, 0x94, 0x25, 0x97, 0x50, 0x72, 0x74, 0x34, 0x29, 0xa9, 0x10, + 0x63, 0xc4, 0xd1, 0xa4, 0x07, 0xd7, 0x95, 0x35, 0xf2, 0x7c, 0x94, 0x4d, 0xe0, 0x16, 0x55, 0xd6, + 0xd8, 0x8e, 0x60, 0x88, 0xca, 0x9a, 0x62, 0xf5, 0xf2, 0xf8, 0x22, 0x65, 0xf1, 0xb5, 0x5c, 0x02, + 0xec, 0x0e, 0x16, 0x12, 0xbc, 0x06, 0xdc, 0xf7, 0x21, 0x7a, 0x11, 0x10, 0x82, 0x33, 0xc8, 0xd3, + 0x28, 0xc6, 0x77, 0xf1, 0xb5, 0x8e, 0x94, 0x11, 0x8b, 0x00, 0x66, 0x50, 0xb8, 0xf2, 0x8e, 0xdf, + 0x15, 0x2e, 0xba, 0xe2, 0xbf, 0xef, 0x43, 0xf4, 0x32, 0x28, 0x04, 0xa3, 0x3c, 0x4d, 0x38, 0x1a, + 0x06, 0xb5, 0x86, 0x90, 0x10, 0xc3, 0xc0, 0x26, 0x90, 0xc9, 0x63, 0x28, 0xa6, 0xe0, 0x34, 0x29, + 0x24, 0x5e, 0x93, 0x0d, 0xa1, 0xdf, 0x3e, 0xaa, 0xdb, 0xce, 0xf2, 0x25, 0x7a, 0xfb, 0x48, 0x36, + 0x8b, 0xe5, 0x4b, 0xe2, 0xed, 0x23, 0x0b, 0x40, 0x21, 0x9e, 0x46, 0x25, 0x77, 0x87, 0x28, 0x24, + 0xde, 0x10, 0x1b, 0x42, 0xaf, 0xd1, 0x75, 0x88, 0x73, 0x8e, 0xd6, 0x68, 0x19, 0x80, 0x71, 0x55, + 0x78, 0x97, 0x94, 0xeb, 0x99, 0xa4, 0xee, 0x15, 0xe0, 0x07, 0x09, 0xa4, 0x93, 0x12, 0xcd, 0x24, + 0xf2, 0xb9, 0x37, 0x52, 0x62, 0x26, 0x69, 0x53, 0x28, 0x95, 0xe4, 0x19, 0xae, 0xab, 0x75, 0xe8, + 0xf8, 0xf6, 0xbe, 0x0f, 0xd1, 0xf3, 0x53, 0x13, 0xf4, 0x5e, 0x54, 0x14, 0x49, 0xb5, 0xf8, 0xaf, + 0xbb, 0x03, 0x6a, 0xe4, 0xc4, 0xfc, 0xe4, 0xe2, 0xd0, 0xf0, 0x6a, 0x26, 0x6e, 0x57, 0x60, 0x78, + 0xea, 0xfe, 0xd8, 0xcb, 0xe8, 0x8a, 0x53, 0x48, 0x8c, 0xbb, 0x2e, 0xd7, 0xd3, 0x74, 0x5c, 0x75, + 0xad, 0x77, 0x61, 0xc6, 0xfb, 0xcc, 0xca, 0xc5, 0x31, 0x5b, 0xc0, 0x98, 0xbd, 0xbc, 0x4d, 0x4a, + 0x9e, 0x64, 0x53, 0xb9, 0x72, 0x3f, 0x27, 0x2c, 0xb9, 0x60, 0xe2, 0x7d, 0xe6, 0x4e, 0x25, 0x5d, + 0x40, 0xa0, 0x58, 0x4e, 0xe0, 0xc6, 0x59, 0x40, 0x60, 0x8b, 0x8a, 0x23, 0x0a, 0x08, 0x1f, 0xaf, + 0xcf, 0x34, 0x94, 0x73, 0xf9, 0xa3, 0xaf, 0x31, 0x6b, 0x6a, 0x39, 0xca, 0x1a, 0x06, 0x89, 0x6d, + 0xa5, 0x57, 0x41, 0xef, 0xf5, 0x94, 0x7f, 0x3d, 0xc4, 0x1e, 0x11, 0x76, 0xda, 0xc3, 0xec, 0x71, + 0x0f, 0xd2, 0xe1, 0x4a, 0x5f, 0xd8, 0x52, 0xae, 0xda, 0xf7, 0xb5, 0x8f, 0x7b, 0x90, 0xc6, 0xf9, + 0x88, 0xd9, 0xac, 0x17, 0x51, 0x7c, 0x3d, 0x2d, 0xd8, 0x3c, 0x9b, 0xec, 0xb1, 0x94, 0x15, 0xe8, + 0x7c, 0xc4, 0x8a, 0x1a, 0xa1, 0xc4, 0xf9, 0x48, 0x87, 0x8a, 0xae, 0xe0, 0xcc, 0x28, 0x76, 0xd3, + 0x64, 0x8a, 0x77, 0xb7, 0x96, 0x21, 0x01, 0x10, 0x15, 0x9c, 0x13, 0x74, 0x24, 0x51, 0xbd, 0xfb, + 0xe5, 0x49, 0x1c, 0xa5, 0xb5, 0xbf, 0x6d, 0xda, 0x8c, 0x05, 0x76, 0x26, 0x91, 0x43, 0xc1, 0xd1, + 0xce, 0xf1, 0xbc, 0xc8, 0x8e, 0x32, 0xce, 0xc8, 0x76, 0x36, 0x40, 0x67, 0x3b, 0x0d, 0x10, 0x4d, + 0xab, 0x63, 0xb8, 0xad, 0xa2, 0xa9, 0xfe, 0xe3, 0x9a, 0x56, 0xab, 0xbf, 0x87, 0x52, 0xee, 0x9b, + 0x56, 0x11, 0x87, 0x1a, 0x23, 0x9d, 0xd4, 0x09, 0xe3, 0xd1, 0xb6, 0xd3, 0xe4, 0x51, 0x37, 0xe8, + 0xf6, 0x33, 0xe2, 0xcb, 0x14, 0x7c, 0x7e, 0x04, 0xd0, 0xc7, 0x4f, 0x03, 0xea, 0x8b, 0x13, 0xab, + 0x3d, 0x57, 0x10, 0x5f, 0xb7, 0xde, 0x3f, 0xb1, 0x03, 0xad, 0x11, 0xe2, 0xe2, 0x84, 0x40, 0xdd, + 0x5d, 0x74, 0x14, 0xb3, 0xcc, 0xd7, 0x45, 0x95, 0xbc, 0x4f, 0x17, 0x49, 0x4e, 0x6f, 0x7e, 0x95, + 0x54, 0x66, 0x66, 0xdd, 0x4d, 0x9b, 0x84, 0x05, 0x13, 0x22, 0x36, 0xbf, 0x24, 0xac, 0x6b, 0x72, + 0xec, 0xf3, 0xb8, 0xfd, 0xfe, 0x67, 0xcb, 0xca, 0x31, 0xfd, 0xfe, 0x27, 0xc5, 0xd2, 0x8d, 0xac, + 0x73, 0xa4, 0xc3, 0x8a, 0x9d, 0x27, 0x4f, 0xfb, 0xc1, 0x7a, 0xcb, 0x63, 0xf9, 0xdc, 0x4b, 0x21, + 0x2a, 0x6a, 0xaf, 0x5b, 0x1e, 0x43, 0x1a, 0x23, 0xb6, 0x3c, 0x1e, 0x1c, 0x4d, 0x61, 0x96, 0xe7, + 0x3d, 0x96, 0x71, 0xc8, 0xb8, 0x6b, 0x0a, 0xb3, 0x8d, 0x49, 0xd0, 0x37, 0x85, 0x51, 0x0a, 0x28, + 0x6f, 0xc5, 0x79, 0x10, 0xf0, 0x93, 0x68, 0xe6, 0xac, 0xd8, 0xea, 0xb3, 0x9e, 0x5a, 0xee, 0xcb, + 0x5b, 0xc4, 0xa1, 0x21, 0x7f, 0x34, 0x8b, 0xa6, 0xca, 0x8b, 0x43, 0x5b, 0xc8, 0x5b, 0x6e, 0x1e, + 0x75, 0x83, 0xc8, 0xcf, 0x9b, 0x64, 0x02, 0xcc, 0xe3, 0x47, 0xc8, 0xfb, 0xf8, 0xc1, 0x20, 0xaa, + 0x9c, 0xaa, 0xd6, 0xd6, 0xbb, 0xa9, 0xdd, 0x6c, 0x22, 0xf7, 0x90, 0x21, 0xf1, 0x50, 0x10, 0xe7, + 0xab, 0x9c, 0x08, 0x1e, 0x8d, 0x8f, 0xe6, 0x70, 0xd4, 0x37, 0x3e, 0xd4, 0xd9, 0x67, 0x9f, 0xf1, + 0xe1, 0x82, 0xa5, 0xcf, 0x9f, 0xc9, 0xf1, 0xb1, 0x1f, 0xf1, 0xa8, 0xaa, 0x99, 0xdf, 0x24, 0x70, + 0x23, 0x37, 0xa1, 0x8e, 0xf6, 0x36, 0x54, 0x28, 0x7e, 0xf1, 0x82, 0x76, 0xa4, 0xdb, 0xbd, 0x79, + 0x8f, 0x6f, 0x59, 0x9d, 0x77, 0xfa, 0x46, 0x65, 0xfa, 0x76, 0x6f, 0xde, 0xe3, 0x5b, 0xfe, 0x94, + 0xae, 0xd3, 0x37, 0xfa, 0x3d, 0xdd, 0x76, 0x6f, 0x5e, 0xfa, 0xfe, 0x8b, 0x41, 0x70, 0xa7, 0xe5, + 0xbc, 0xaa, 0x81, 0x62, 0x9e, 0x2c, 0xc0, 0x55, 0xca, 0xd9, 0xf6, 0x14, 0xea, 0x2b, 0xe5, 0x68, + 0x15, 0xe3, 0xfb, 0x0b, 0xae, 0x28, 0x4e, 0x59, 0x99, 0x88, 0x8b, 0xe3, 0xe7, 0x3d, 0x8c, 0x36, + 0xb0, 0x6f, 0xc3, 0xe2, 0x53, 0xd2, 0xd7, 0x6e, 0x16, 0xaa, 0x5f, 0xf5, 0x7c, 0xea, 0xb1, 0xd7, + 0x7e, 0xe3, 0x73, 0xab, 0x27, 0xad, 0x2f, 0xc0, 0x2c, 0xc6, 0xbc, 0x79, 0xf3, 0xf5, 0xaa, 0xf3, + 0xf2, 0x6d, 0xa7, 0xbf, 0x82, 0x74, 0xff, 0x57, 0x4d, 0x4d, 0x8f, 0xfd, 0xcb, 0x41, 0xf0, 0xac, + 0x8f, 0x45, 0x34, 0x10, 0x9e, 0xaf, 0xa4, 0x23, 0x03, 0xf9, 0x8f, 0x41, 0x70, 0xdf, 0x19, 0x88, + 0x7d, 0x07, 0xfb, 0xdb, 0x7d, 0x6c, 0xbb, 0xef, 0x62, 0x7f, 0xf4, 0x6d, 0x54, 0x65, 0x74, 0x7f, + 0xd7, 0x6c, 0xad, 0x1b, 0x0d, 0xf1, 0x3a, 0xfe, 0xeb, 0x62, 0x02, 0x85, 0x1c, 0xb1, 0xbe, 0xa4, + 0xd3, 0x30, 0x1e, 0xb7, 0x9f, 0xad, 0xa8, 0x65, 0x7c, 0x2b, 0xc4, 0x82, 0xe5, 0x2f, 0x93, 0x8c, + 0x78, 0x7c, 0x96, 0x0d, 0x1a, 0x07, 0xf4, 0xf9, 0xaa, 0x6a, 0xd4, 0x48, 0x36, 0x60, 0xf1, 0xd3, + 0xe3, 0xe7, 0x3d, 0x0d, 0x5b, 0x3f, 0x46, 0xfe, 0x74, 0x35, 0x25, 0x19, 0xcb, 0x7f, 0x0d, 0x82, + 0x87, 0x16, 0xab, 0x6f, 0x1a, 0xd0, 0x79, 0xc8, 0x8f, 0x3d, 0xf6, 0x29, 0x25, 0x15, 0xdc, 0xef, + 0x7c, 0x3b, 0x65, 0xfd, 0x61, 0x0d, 0x4b, 0xe5, 0x20, 0x49, 0x39, 0x14, 0xed, 0x0f, 0x6b, 0xd8, + 0x76, 0x6b, 0x2a, 0xa4, 0x3f, 0xac, 0xe1, 0xc1, 0x8d, 0x0f, 0x6b, 0x38, 0x3c, 0x3b, 0x3f, 0xac, + 0xe1, 0xb4, 0xe6, 0xfd, 0xb0, 0x86, 0x5f, 0x83, 0x5a, 0x7c, 0x9a, 0x10, 0xea, 0x13, 0xed, 0x5e, + 0x16, 0xed, 0x03, 0xee, 0x67, 0xab, 0xa8, 0x10, 0xcb, 0x6f, 0xcd, 0x89, 0x37, 0xc3, 0x7a, 0x3c, + 0x53, 0xeb, 0xed, 0xb0, 0xed, 0xde, 0xbc, 0xf4, 0xfd, 0x53, 0xb9, 0xef, 0x51, 0x8b, 0x0d, 0x2b, + 0xc4, 0x47, 0x55, 0x36, 0x7d, 0x8b, 0x47, 0x65, 0xc1, 0xec, 0xf9, 0xa7, 0xfd, 0x60, 0xa2, 0xb9, + 0x15, 0x21, 0x3b, 0x3d, 0xec, 0x32, 0x84, 0xba, 0x7c, 0xbb, 0x37, 0x4f, 0x2c, 0x72, 0xb5, 0xef, + 0xba, 0xb7, 0x7b, 0x18, 0xb3, 0xfb, 0x7a, 0xa7, 0xbf, 0x82, 0x74, 0xbf, 0x90, 0x45, 0xad, 0xe9, + 0x5e, 0xf4, 0xf3, 0x56, 0x97, 0xa9, 0x91, 0xd5, 0xcd, 0x61, 0x5f, 0xdc, 0x57, 0xde, 0x98, 0x0b, + 0x7c, 0x57, 0x79, 0xe3, 0x5c, 0xe4, 0x3f, 0x5d, 0x4d, 0x49, 0xc6, 0xf2, 0x4f, 0x83, 0xe0, 0x2e, + 0x19, 0x8b, 0xcc, 0x83, 0xcf, 0xfb, 0x5a, 0x46, 0xf9, 0xf0, 0xc5, 0xca, 0x7a, 0x32, 0xa8, 0x7f, + 0x1d, 0x04, 0xf7, 0x3c, 0x41, 0xd5, 0x09, 0xb2, 0x82, 0x75, 0x3b, 0x51, 0x7e, 0xb8, 0xba, 0x22, + 0xb5, 0xdc, 0x9b, 0xf8, 0xa8, 0xfd, 0xc5, 0x09, 0x8f, 0xed, 0x11, 0xfd, 0xc5, 0x89, 0x6e, 0x2d, + 0x7c, 0xfc, 0x53, 0x15, 0x25, 0x72, 0x67, 0xe4, 0x3a, 0xfe, 0x11, 0x35, 0x0b, 0xda, 0x11, 0x6d, + 0x74, 0x72, 0x2e, 0x27, 0x2f, 0x6f, 0xf3, 0x28, 0x9b, 0xd0, 0x4e, 0x6a, 0x79, 0xb7, 0x13, 0xc5, + 0xe1, 0x63, 0xb3, 0x4a, 0x7a, 0xc6, 0x9a, 0x6d, 0xde, 0x63, 0x4a, 0x5f, 0x21, 0xde, 0x63, 0xb3, + 0x16, 0x4a, 0x78, 0x93, 0x35, 0xad, 0xcf, 0x1b, 0x2a, 0x65, 0x9f, 0xf4, 0x41, 0xd1, 0x06, 0x42, + 0x79, 0x53, 0xa7, 0xf1, 0x4f, 0x7d, 0x56, 0x5a, 0x27, 0xf2, 0x5b, 0x3d, 0x69, 0xc2, 0xed, 0x08, + 0xf8, 0x97, 0x10, 0x4d, 0xa0, 0xf0, 0xba, 0x55, 0x54, 0x2f, 0xb7, 0x26, 0xed, 0x72, 0xbb, 0xc7, + 0xd2, 0xf9, 0x2c, 0x93, 0x9d, 0x49, 0xba, 0x35, 0xa9, 0x6e, 0xb7, 0x88, 0xc6, 0x07, 0x86, 0xda, + 0xad, 0x28, 0x2f, 0x9f, 0xf8, 0xcd, 0x58, 0x55, 0xe5, 0x66, 0x2f, 0x96, 0x6e, 0xa7, 0x4c, 0xa3, + 0x8e, 0x76, 0xa2, 0x4c, 0xda, 0xea, 0x49, 0xe3, 0x93, 0x3b, 0xc3, 0xad, 0xca, 0xa7, 0xed, 0x0e, + 0x5b, 0xad, 0x94, 0xda, 0xe9, 0xaf, 0x80, 0xcf, 0x49, 0x65, 0x56, 0x55, 0xfb, 0xa2, 0x83, 0x24, + 0x4d, 0x87, 0x9b, 0x9e, 0x34, 0x69, 0x20, 0xef, 0x39, 0xa9, 0x03, 0x26, 0x32, 0xb9, 0x39, 0x57, + 0xcc, 0x86, 0x5d, 0x76, 0x04, 0xd5, 0x2b, 0x93, 0x4d, 0x1a, 0x9d, 0xb7, 0x19, 0x8f, 0x5a, 0xb5, + 0x36, 0xf4, 0x3f, 0xb8, 0x56, 0x83, 0xb7, 0x7b, 0xf3, 0xe8, 0x22, 0x5e, 0x50, 0x62, 0x65, 0x79, + 0x40, 0x99, 0xb0, 0x56, 0x92, 0x87, 0x1d, 0x14, 0x3a, 0xb3, 0xac, 0x87, 0xd1, 0xdb, 0x64, 0x32, + 0x05, 0xee, 0xbc, 0x43, 0x32, 0x01, 0xef, 0x1d, 0x12, 0x02, 0x51, 0xd7, 0xd5, 0x7f, 0x1f, 0x01, + 0x1f, 0x47, 0xc5, 0x14, 0xf8, 0xd1, 0xc4, 0xd5, 0x75, 0x52, 0xd9, 0xa0, 0x7c, 0x5d, 0xe7, 0xa4, + 0xd1, 0x6c, 0xa0, 0xdc, 0xca, 0x5f, 0x54, 0x3f, 0xf1, 0x99, 0x41, 0x3f, 0xab, 0xde, 0xec, 0xc5, + 0xa2, 0x15, 0x45, 0x3b, 0x4c, 0x66, 0x09, 0x77, 0xad, 0x28, 0x86, 0x8d, 0x0a, 0xf1, 0xad, 0x28, + 0x6d, 0x94, 0x6a, 0x5e, 0x55, 0x23, 0x1c, 0x4d, 0xfc, 0xcd, 0xab, 0x99, 0x7e, 0xcd, 0x53, 0x6c, + 0xeb, 0xca, 0x33, 0x53, 0x29, 0xc3, 0xaf, 0xe4, 0x66, 0xd9, 0x91, 0xdb, 0xe2, 0x47, 0x86, 0x18, + 0xf4, 0xcd, 0x3a, 0x94, 0x82, 0xf1, 0x3b, 0x16, 0xc5, 0x35, 0xb7, 0xb2, 0x79, 0x0e, 0x51, 0x11, + 0x65, 0xb1, 0x73, 0x73, 0x2a, 0x0c, 0xb6, 0x48, 0xdf, 0xe6, 0x94, 0xd4, 0x40, 0x17, 0xea, 0xf6, + 0xcf, 0x03, 0x1d, 0x43, 0x41, 0xfd, 0x0e, 0xcf, 0xfe, 0x75, 0xe0, 0xe3, 0x1e, 0x24, 0xbe, 0x50, + 0x6f, 0x00, 0x75, 0x2c, 0x5f, 0x3b, 0xfd, 0xc4, 0x63, 0xca, 0x46, 0x7d, 0x1b, 0x61, 0x5a, 0x05, + 0x25, 0xb5, 0x2a, 0x70, 0x81, 0xff, 0x04, 0x96, 0xae, 0xa4, 0xd6, 0xf5, 0xa9, 0x40, 0x7c, 0x49, + 0xdd, 0x46, 0x51, 0x9d, 0x69, 0xee, 0x83, 0xd6, 0x3d, 0xfa, 0xe6, 0xd6, 0x67, 0xa3, 0x93, 0x43, + 0x23, 0x67, 0x3f, 0x59, 0x58, 0xb7, 0x18, 0x8e, 0x40, 0xf7, 0x93, 0x85, 0xfb, 0x12, 0x63, 0xb3, + 0x17, 0x8b, 0x2f, 0xeb, 0x23, 0x0e, 0xb7, 0xcd, 0x2d, 0xba, 0x23, 0x5c, 0x21, 0x6f, 0x5d, 0xa3, + 0x3f, 0xea, 0x06, 0xf5, 0xab, 0xb1, 0xa7, 0x05, 0x8b, 0xa1, 0x2c, 0xe5, 0x67, 0xb8, 0xec, 0x77, + 0x8f, 0xa4, 0x2c, 0x44, 0x1f, 0xe1, 0x7a, 0xe0, 0x87, 0xa4, 0xed, 0x2f, 0x83, 0x77, 0x5f, 0xb1, + 0xe9, 0x08, 0xb2, 0xc9, 0xf0, 0x07, 0xf6, 0xcb, 0xa8, 0x6c, 0x1a, 0x56, 0x7f, 0x56, 0xf6, 0xd6, + 0x28, 0xb1, 0x7e, 0x9d, 0x6e, 0x1f, 0x2e, 0xe6, 0xd3, 0x11, 0x8f, 0xf0, 0xc7, 0xbc, 0xc4, 0xdf, + 0xc3, 0x4a, 0x40, 0xbc, 0x4e, 0x67, 0x01, 0xc8, 0xde, 0xb8, 0x00, 0x70, 0xda, 0xab, 0x04, 0x5e, + 0x7b, 0x12, 0xd0, 0xab, 0xae, 0xb2, 0x57, 0x15, 0xb6, 0xf8, 0xf5, 0x37, 0xad, 0x23, 0xa4, 0xc4, + 0xaa, 0xdb, 0xa6, 0x74, 0x32, 0xd4, 0xcd, 0x17, 0x1f, 0x5a, 0x98, 0xcf, 0x66, 0x51, 0xb1, 0x44, + 0xc9, 0x20, 0x5b, 0x69, 0x00, 0x44, 0x32, 0x38, 0x41, 0x9d, 0xe5, 0xcd, 0x63, 0x8e, 0xaf, 0x0f, + 0x59, 0xc1, 0xe6, 0x3c, 0xc9, 0xa0, 0x44, 0x59, 0xae, 0x1e, 0xa8, 0xc9, 0x10, 0x59, 0x4e, 0xb1, + 0xba, 0x2a, 0x14, 0x44, 0xfd, 0x66, 0x9e, 0xf8, 0x98, 0x65, 0xc9, 0x59, 0x81, 0x6f, 0x07, 0x6b, + 0x2b, 0x18, 0x22, 0xaa, 0x42, 0x12, 0x46, 0x7d, 0x7f, 0x9a, 0x64, 0x53, 0x67, 0xdf, 0x9f, 0x9a, + 0x9f, 0x6e, 0xbb, 0x47, 0x03, 0x7a, 0x7e, 0xaf, 0x1f, 0x5a, 0xfd, 0x41, 0x1b, 0xf9, 0x6b, 0x41, + 0xe7, 0x43, 0x37, 0x09, 0x62, 0x7e, 0x77, 0x93, 0xc8, 0xd5, 0xeb, 0x1c, 0x32, 0x98, 0x34, 0xef, + 0x9f, 0xb9, 0x5c, 0x59, 0x84, 0xd7, 0x15, 0x26, 0x75, 0x2a, 0x1c, 0x03, 0x2f, 0x92, 0xb8, 0x1c, + 0x01, 0x3f, 0x8d, 0x8a, 0x68, 0x06, 0x1c, 0x0a, 0x9c, 0x0a, 0x12, 0x09, 0x2d, 0x86, 0x48, 0x05, + 0x8a, 0x95, 0x0e, 0x7f, 0x37, 0x78, 0xbf, 0x9a, 0x09, 0x21, 0x93, 0x5f, 0xd7, 0x7e, 0x29, 0x3e, + 0xcb, 0x3f, 0xfc, 0x40, 0xd9, 0x18, 0xf1, 0x02, 0xa2, 0x59, 0x63, 0xfb, 0x3d, 0xf5, 0x77, 0x01, + 0xee, 0x0c, 0xaa, 0x75, 0xe0, 0x84, 0xf1, 0xe4, 0xb2, 0xda, 0x78, 0xc8, 0x6b, 0x1e, 0xb4, 0x0e, + 0x98, 0xe2, 0xd0, 0xf3, 0xdb, 0x72, 0x17, 0xa7, 0x47, 0xa2, 0x29, 0x3d, 0x83, 0x3c, 0xc5, 0x23, + 0xd1, 0xd2, 0x16, 0x00, 0x31, 0x12, 0x9d, 0xa0, 0x5e, 0xd4, 0x4c, 0xf1, 0x18, 0xfc, 0x8d, 0x19, + 0x43, 0xbf, 0xc6, 0x8c, 0xad, 0x17, 0xd8, 0xd3, 0xe0, 0xfd, 0x63, 0x98, 0x5d, 0x40, 0x51, 0x5e, + 0x25, 0xf9, 0x61, 0xb5, 0x04, 0x45, 0x7c, 0x5e, 0xa2, 0x75, 0x5a, 0x13, 0xa1, 0x42, 0x88, 0x75, + 0x9a, 0x40, 0xf5, 0x58, 0xd7, 0xc0, 0x51, 0x79, 0x12, 0xcd, 0x40, 0xfc, 0x52, 0x7e, 0xb8, 0x49, + 0x19, 0x31, 0x20, 0x62, 0xac, 0x93, 0xb0, 0x7e, 0x53, 0xc6, 0x6a, 0xe1, 0x69, 0xb4, 0x9c, 0x41, + 0xc6, 0xcf, 0x8b, 0x14, 0x1d, 0xd5, 0xda, 0xa1, 0x6b, 0x8c, 0x38, 0xaa, 0xf5, 0xe0, 0x46, 0xe5, + 0x69, 0xbb, 0x66, 0x05, 0xaf, 0xbf, 0x8f, 0x5f, 0x79, 0xdf, 0xf1, 0x99, 0x33, 0x49, 0xa2, 0xf2, + 0xf4, 0x6b, 0x18, 0x1f, 0xba, 0xb5, 0x62, 0x78, 0x03, 0x85, 0xca, 0x85, 0x97, 0xb3, 0x28, 0x49, + 0xd1, 0xb1, 0xa3, 0x6d, 0xb5, 0x45, 0x13, 0xc7, 0x8e, 0xdd, 0x5a, 0x7a, 0x37, 0xa0, 0x59, 0xc1, + 0x2d, 0x05, 0xb1, 0xc7, 0x26, 0xf8, 0x0c, 0xc2, 0xb0, 0x89, 0x40, 0x62, 0x37, 0xe0, 0x55, 0xd0, + 0xd3, 0xa7, 0xc6, 0x0e, 0x92, 0x2c, 0x4a, 0x93, 0x9f, 0xe1, 0x57, 0x5b, 0x0d, 0x3b, 0x0d, 0x41, + 0x4c, 0x9f, 0x6e, 0x52, 0x7f, 0x53, 0xdf, 0x7a, 0xf2, 0xe3, 0xa4, 0x9a, 0x3c, 0x49, 0x03, 0x42, + 0x5c, 0x3d, 0xbd, 0xee, 0x91, 0x65, 0xa2, 0xc6, 0x0d, 0x5c, 0x95, 0xfc, 0xa3, 0xfa, 0x5f, 0x41, + 0x39, 0x2f, 0xa1, 0x90, 0x1f, 0xf8, 0x3d, 0x04, 0x8e, 0x52, 0xcd, 0xe0, 0x42, 0x03, 0xb4, 0x7c, + 0x7f, 0xb2, 0x82, 0x86, 0xde, 0xaf, 0x1b, 0xdc, 0x19, 0x94, 0x2c, 0x5d, 0x80, 0x78, 0xa1, 0xe9, + 0x29, 0x69, 0xcc, 0xa0, 0x88, 0xfd, 0x3a, 0x4d, 0xeb, 0xf1, 0xdd, 0x76, 0xbb, 0x9b, 0x2d, 0x8f, + 0xf0, 0xad, 0xa7, 0xc3, 0x92, 0xc0, 0x88, 0xf1, 0xed, 0xc1, 0x8d, 0xf3, 0xac, 0x82, 0x45, 0x93, + 0x38, 0x2a, 0xab, 0xf1, 0x9f, 0xb2, 0x68, 0x22, 0x16, 0x22, 0x7c, 0x9e, 0xd5, 0x30, 0xa1, 0x09, + 0x51, 0xe7, 0x59, 0x14, 0x5c, 0xfb, 0x7c, 0xf1, 0xd1, 0x7f, 0x7f, 0xbd, 0x36, 0xf8, 0xf9, 0xd7, + 0x6b, 0x83, 0xff, 0xfb, 0x7a, 0x6d, 0xf0, 0x8f, 0xdf, 0xac, 0xbd, 0xf3, 0xf3, 0x6f, 0xd6, 0xde, + 0xf9, 0xdf, 0x6f, 0xd6, 0xde, 0xf9, 0xea, 0x5d, 0xf9, 0x0f, 0xe0, 0x5c, 0xfc, 0x82, 0xf8, 0x67, + 0x6c, 0x9e, 0xff, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbb, 0x7b, 0x9a, 0x9c, 0x24, 0x67, 0x00, + 0x00, } // This is a compile-time assertion to ensure that this generated file @@ -578,6 +594,34 @@ type ClientCommandsHandler interface { NotificationList(context.Context, *pb.RpcNotificationListRequest) *pb.RpcNotificationListResponse NotificationReply(context.Context, *pb.RpcNotificationReplyRequest) *pb.RpcNotificationReplyResponse NotificationTest(context.Context, *pb.RpcNotificationTestRequest) *pb.RpcNotificationTestResponse + // Membership + // *** + // Get current subscription status (tier, expiration date, etc.) + // WARNING: can be cached by Anytype Heart + MembershipGetStatus(context.Context, *pb.RpcMembershipGetStatusRequest) *pb.RpcMembershipGetStatusResponse + MembershipIsNameValid(context.Context, *pb.RpcMembershipIsNameValidRequest) *pb.RpcMembershipIsNameValidResponse + // Buy a subscription, will return a payment URL. The user should be redirected to this URL to complete the payment. + MembershipGetPaymentUrl(context.Context, *pb.RpcMembershipGetPaymentUrlRequest) *pb.RpcMembershipGetPaymentUrlResponse + // 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 + MembershipGetPortalLinkUrl(context.Context, *pb.RpcMembershipGetPortalLinkUrlRequest) *pb.RpcMembershipGetPortalLinkUrlResponse + // Send a verification code to the user's email. The user should enter this code to verify his email. + MembershipGetVerificationEmail(context.Context, *pb.RpcMembershipGetVerificationEmailRequest) *pb.RpcMembershipGetVerificationEmailResponse + // Verify the user's email with the code received in the previous step (MembershipGetVerificationEmail) + MembershipVerifyEmailCode(context.Context, *pb.RpcMembershipVerifyEmailCodeRequest) *pb.RpcMembershipVerifyEmailCodeResponse + // If your subscription is in PendingRequiresFinalization: + // please call MembershipFinalize to finish the process + MembershipFinalize(context.Context, *pb.RpcMembershipFinalizeRequest) *pb.RpcMembershipFinalizeResponse + MembershipGetTiers(context.Context, *pb.RpcMembershipTiersGetRequest) *pb.RpcMembershipTiersGetResponse + // Name Service: + // *** + // hello.any -> data + NameServiceUserAccountGet(context.Context, *pb.RpcNameServiceUserAccountGetRequest) *pb.RpcNameServiceUserAccountGetResponse + NameServiceResolveName(context.Context, *pb.RpcNameServiceResolveNameRequest) *pb.RpcNameServiceResolveNameResponse + // 12D3KooWA8EXV3KjBxEU5EnsPfneLx84vMWAtTBQBeyooN82KSuS -> hello.any + NameServiceResolveAnyId(context.Context, *pb.RpcNameServiceResolveAnyIdRequest) *pb.RpcNameServiceResolveAnyIdResponse BroadcastPayloadEvent(context.Context, *pb.RpcBroadcastPayloadEventRequest) *pb.RpcBroadcastPayloadEventResponse } @@ -5085,6 +5129,226 @@ func NotificationTest(b []byte) (resp []byte) { return resp } +func MembershipGetStatus(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipGetStatusResponse{Error: &pb.RpcMembershipGetStatusResponseError{Code: pb.RpcMembershipGetStatusResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipGetStatusRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipGetStatusResponse{Error: &pb.RpcMembershipGetStatusResponseError{Code: pb.RpcMembershipGetStatusResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipGetStatus(context.Background(), in).Marshal() + return resp +} + +func MembershipIsNameValid(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipIsNameValidResponse{Error: &pb.RpcMembershipIsNameValidResponseError{Code: pb.RpcMembershipIsNameValidResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipIsNameValidRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipIsNameValidResponse{Error: &pb.RpcMembershipIsNameValidResponseError{Code: pb.RpcMembershipIsNameValidResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipIsNameValid(context.Background(), in).Marshal() + return resp +} + +func MembershipGetPaymentUrl(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipGetPaymentUrlResponse{Error: &pb.RpcMembershipGetPaymentUrlResponseError{Code: pb.RpcMembershipGetPaymentUrlResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipGetPaymentUrlRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipGetPaymentUrlResponse{Error: &pb.RpcMembershipGetPaymentUrlResponseError{Code: pb.RpcMembershipGetPaymentUrlResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipGetPaymentUrl(context.Background(), in).Marshal() + return resp +} + +func MembershipGetPortalLinkUrl(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipGetPortalLinkUrlResponse{Error: &pb.RpcMembershipGetPortalLinkUrlResponseError{Code: pb.RpcMembershipGetPortalLinkUrlResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipGetPortalLinkUrlRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipGetPortalLinkUrlResponse{Error: &pb.RpcMembershipGetPortalLinkUrlResponseError{Code: pb.RpcMembershipGetPortalLinkUrlResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipGetPortalLinkUrl(context.Background(), in).Marshal() + return resp +} + +func MembershipGetVerificationEmail(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipGetVerificationEmailResponse{Error: &pb.RpcMembershipGetVerificationEmailResponseError{Code: pb.RpcMembershipGetVerificationEmailResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipGetVerificationEmailRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipGetVerificationEmailResponse{Error: &pb.RpcMembershipGetVerificationEmailResponseError{Code: pb.RpcMembershipGetVerificationEmailResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipGetVerificationEmail(context.Background(), in).Marshal() + return resp +} + +func MembershipVerifyEmailCode(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipVerifyEmailCodeResponse{Error: &pb.RpcMembershipVerifyEmailCodeResponseError{Code: pb.RpcMembershipVerifyEmailCodeResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipVerifyEmailCodeRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipVerifyEmailCodeResponse{Error: &pb.RpcMembershipVerifyEmailCodeResponseError{Code: pb.RpcMembershipVerifyEmailCodeResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipVerifyEmailCode(context.Background(), in).Marshal() + return resp +} + +func MembershipFinalize(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipFinalizeResponse{Error: &pb.RpcMembershipFinalizeResponseError{Code: pb.RpcMembershipFinalizeResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipFinalizeRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipFinalizeResponse{Error: &pb.RpcMembershipFinalizeResponseError{Code: pb.RpcMembershipFinalizeResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipFinalize(context.Background(), in).Marshal() + return resp +} + +func MembershipGetTiers(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcMembershipTiersGetResponse{Error: &pb.RpcMembershipTiersGetResponseError{Code: pb.RpcMembershipTiersGetResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcMembershipTiersGetRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcMembershipTiersGetResponse{Error: &pb.RpcMembershipTiersGetResponseError{Code: pb.RpcMembershipTiersGetResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.MembershipGetTiers(context.Background(), in).Marshal() + return resp +} + +func NameServiceUserAccountGet(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcNameServiceUserAccountGetResponse{Error: &pb.RpcNameServiceUserAccountGetResponseError{Code: pb.RpcNameServiceUserAccountGetResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcNameServiceUserAccountGetRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcNameServiceUserAccountGetResponse{Error: &pb.RpcNameServiceUserAccountGetResponseError{Code: pb.RpcNameServiceUserAccountGetResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.NameServiceUserAccountGet(context.Background(), in).Marshal() + return resp +} + +func NameServiceResolveName(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcNameServiceResolveNameResponse{Error: &pb.RpcNameServiceResolveNameResponseError{Code: pb.RpcNameServiceResolveNameResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcNameServiceResolveNameRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcNameServiceResolveNameResponse{Error: &pb.RpcNameServiceResolveNameResponseError{Code: pb.RpcNameServiceResolveNameResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.NameServiceResolveName(context.Background(), in).Marshal() + return resp +} + +func NameServiceResolveAnyId(b []byte) (resp []byte) { + defer func() { + if PanicHandler != nil { + if r := recover(); r != nil { + resp, _ = (&pb.RpcNameServiceResolveAnyIdResponse{Error: &pb.RpcNameServiceResolveAnyIdResponseError{Code: pb.RpcNameServiceResolveAnyIdResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal() + PanicHandler(r) + } + } + }() + + in := new(pb.RpcNameServiceResolveAnyIdRequest) + if err := in.Unmarshal(b); err != nil { + resp, _ = (&pb.RpcNameServiceResolveAnyIdResponse{Error: &pb.RpcNameServiceResolveAnyIdResponseError{Code: pb.RpcNameServiceResolveAnyIdResponseError_BAD_INPUT, Description: err.Error()}}).Marshal() + return resp + } + + resp, _ = clientCommandsHandler.NameServiceResolveAnyId(context.Background(), in).Marshal() + return resp +} + func BroadcastPayloadEvent(b []byte) (resp []byte) { defer func() { if PanicHandler != nil { @@ -5561,6 +5825,28 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) { cd = NotificationReply(data) case "NotificationTest": cd = NotificationTest(data) + case "MembershipGetStatus": + cd = MembershipGetStatus(data) + case "MembershipIsNameValid": + cd = MembershipIsNameValid(data) + case "MembershipGetPaymentUrl": + cd = MembershipGetPaymentUrl(data) + case "MembershipGetPortalLinkUrl": + cd = MembershipGetPortalLinkUrl(data) + case "MembershipGetVerificationEmail": + cd = MembershipGetVerificationEmail(data) + case "MembershipVerifyEmailCode": + cd = MembershipVerifyEmailCode(data) + case "MembershipFinalize": + cd = MembershipFinalize(data) + case "MembershipGetTiers": + cd = MembershipGetTiers(data) + case "NameServiceUserAccountGet": + cd = NameServiceUserAccountGet(data) + case "NameServiceResolveName": + cd = NameServiceResolveName(data) + case "NameServiceResolveAnyId": + cd = NameServiceResolveAnyId(data) case "BroadcastPayloadEvent": cd = BroadcastPayloadEvent(data) default: @@ -8735,6 +9021,160 @@ func (h *ClientCommandsHandlerProxy) NotificationTest(ctx context.Context, req * call, _ := actualCall(ctx, req) return call.(*pb.RpcNotificationTestResponse) } +func (h *ClientCommandsHandlerProxy) MembershipGetStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) *pb.RpcMembershipGetStatusResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipGetStatus(ctx, req.(*pb.RpcMembershipGetStatusRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipGetStatus", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipGetStatusResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipIsNameValid(ctx context.Context, req *pb.RpcMembershipIsNameValidRequest) *pb.RpcMembershipIsNameValidResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipIsNameValid(ctx, req.(*pb.RpcMembershipIsNameValidRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipIsNameValid", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipIsNameValidResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipGetPaymentUrl(ctx context.Context, req *pb.RpcMembershipGetPaymentUrlRequest) *pb.RpcMembershipGetPaymentUrlResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipGetPaymentUrl(ctx, req.(*pb.RpcMembershipGetPaymentUrlRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipGetPaymentUrl", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipGetPaymentUrlResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipGetPortalLinkUrl(ctx context.Context, req *pb.RpcMembershipGetPortalLinkUrlRequest) *pb.RpcMembershipGetPortalLinkUrlResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipGetPortalLinkUrl(ctx, req.(*pb.RpcMembershipGetPortalLinkUrlRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipGetPortalLinkUrl", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipGetPortalLinkUrlResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipGetVerificationEmail(ctx context.Context, req *pb.RpcMembershipGetVerificationEmailRequest) *pb.RpcMembershipGetVerificationEmailResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipGetVerificationEmail(ctx, req.(*pb.RpcMembershipGetVerificationEmailRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipGetVerificationEmail", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipGetVerificationEmailResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipVerifyEmailCode(ctx context.Context, req *pb.RpcMembershipVerifyEmailCodeRequest) *pb.RpcMembershipVerifyEmailCodeResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipVerifyEmailCode(ctx, req.(*pb.RpcMembershipVerifyEmailCodeRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipVerifyEmailCode", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipVerifyEmailCodeResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipFinalize(ctx context.Context, req *pb.RpcMembershipFinalizeRequest) *pb.RpcMembershipFinalizeResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipFinalize(ctx, req.(*pb.RpcMembershipFinalizeRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipFinalize", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipFinalizeResponse) +} +func (h *ClientCommandsHandlerProxy) MembershipGetTiers(ctx context.Context, req *pb.RpcMembershipTiersGetRequest) *pb.RpcMembershipTiersGetResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.MembershipGetTiers(ctx, req.(*pb.RpcMembershipTiersGetRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "MembershipGetTiers", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcMembershipTiersGetResponse) +} +func (h *ClientCommandsHandlerProxy) NameServiceUserAccountGet(ctx context.Context, req *pb.RpcNameServiceUserAccountGetRequest) *pb.RpcNameServiceUserAccountGetResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.NameServiceUserAccountGet(ctx, req.(*pb.RpcNameServiceUserAccountGetRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "NameServiceUserAccountGet", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcNameServiceUserAccountGetResponse) +} +func (h *ClientCommandsHandlerProxy) NameServiceResolveName(ctx context.Context, req *pb.RpcNameServiceResolveNameRequest) *pb.RpcNameServiceResolveNameResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.NameServiceResolveName(ctx, req.(*pb.RpcNameServiceResolveNameRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "NameServiceResolveName", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcNameServiceResolveNameResponse) +} +func (h *ClientCommandsHandlerProxy) NameServiceResolveAnyId(ctx context.Context, req *pb.RpcNameServiceResolveAnyIdRequest) *pb.RpcNameServiceResolveAnyIdResponse { + actualCall := func(ctx context.Context, req any) (any, error) { + return h.client.NameServiceResolveAnyId(ctx, req.(*pb.RpcNameServiceResolveAnyIdRequest)), nil + } + for _, interceptor := range h.interceptors { + toCall := actualCall + currentInterceptor := interceptor + actualCall = func(ctx context.Context, req any) (any, error) { + return currentInterceptor(ctx, req, "NameServiceResolveAnyId", toCall) + } + } + call, _ := actualCall(ctx, req) + return call.(*pb.RpcNameServiceResolveAnyIdResponse) +} func (h *ClientCommandsHandlerProxy) BroadcastPayloadEvent(ctx context.Context, req *pb.RpcBroadcastPayloadEventRequest) *pb.RpcBroadcastPayloadEventResponse { actualCall := func(ctx context.Context, req any) (any, error) { return h.client.BroadcastPayloadEvent(ctx, req.(*pb.RpcBroadcastPayloadEventRequest)), nil diff --git a/core/anytype/bootstrap.go b/core/anytype/bootstrap.go index 03d2fd9dc..87c7728ca 100644 --- a/core/anytype/bootstrap.go +++ b/core/anytype/bootstrap.go @@ -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 { diff --git a/core/identity/identity.go b/core/identity/identity.go index b7ff9405a..720fec45d 100644 --- a/core/identity/identity.go +++ b/core/identity/identity.go @@ -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) diff --git a/core/identity/identity_test.go b/core/identity/identity_test.go index 64475e771..cf473f8ff 100644 --- a/core/identity/identity_test.go +++ b/core/identity/identity_test.go @@ -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) diff --git a/core/nameservice.go b/core/nameservice.go new file mode 100644 index 000000000..9592926a1 --- /dev/null +++ b/core/nameservice.go @@ -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 +} diff --git a/core/payments.go b/core/payments.go new file mode 100644 index 000000000..8dc2d79fc --- /dev/null +++ b/core/payments.go @@ -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 +} diff --git a/core/payments/cache/cache.go b/core/payments/cache/cache.go new file mode 100644 index 000000000..d45e44d80 --- /dev/null +++ b/core/payments/cache/cache.go @@ -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) + }) +} diff --git a/core/payments/cache/cache_test.go b/core/payments/cache/cache_test.go new file mode 100644 index 000000000..d884ba5fd --- /dev/null +++ b/core/payments/cache/cache_test.go @@ -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) + }) +} diff --git a/core/payments/cache/mock_cache/mock_CacheService.go b/core/payments/cache/mock_cache/mock_CacheService.go new file mode 100644 index 000000000..d36968a36 --- /dev/null +++ b/core/payments/cache/mock_cache/mock_CacheService.go @@ -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 +} diff --git a/core/payments/payments.go b/core/payments/payments.go new file mode 100644 index 000000000..84c1f56fb --- /dev/null +++ b/core/payments/payments.go @@ -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 +} diff --git a/core/payments/payments_test.go b/core/payments/payments_test.go new file mode 100644 index 000000000..cd34348e9 --- /dev/null +++ b/core/payments/payments_test.go @@ -0,0 +1,1071 @@ +package payments + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/anyproto/any-sync/app" + "github.com/anyproto/any-sync/commonspace/object/accountdata" + "github.com/anyproto/any-sync/util/crypto" + "github.com/anyproto/any-sync/util/periodicsync/mock_periodicsync" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + mock_ppclient "github.com/anyproto/any-sync/paymentservice/paymentserviceclient/mock" + psp "github.com/anyproto/any-sync/paymentservice/paymentserviceproto" + + "github.com/anyproto/anytype-heart/core/event/mock_event" + "github.com/anyproto/anytype-heart/core/payments/cache" + "github.com/anyproto/anytype-heart/core/payments/cache/mock_cache" + "github.com/anyproto/anytype-heart/core/wallet/mock_wallet" + "github.com/anyproto/anytype-heart/pb" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/tests/testutil" +) + +var ctx = context.Background() + +var timeNow time.Time = time.Now().UTC() +var subsExpire time.Time = timeNow.Add(365 * 24 * time.Hour) + +// truncate nseconds +var cacheExpireTime time.Time = time.Unix(int64(subsExpire.Unix()), 0) + +type mockGlobalNamesUpdater struct{} + +func (u *mockGlobalNamesUpdater) UpdateGlobalNames() {} + +func (u *mockGlobalNamesUpdater) Init(*app.App) (err error) { + return nil +} + +func (u *mockGlobalNamesUpdater) Name() string { + return "" +} + +type fixture struct { + a *app.App + ctrl *gomock.Controller + cache *mock_cache.MockCacheService + ppclient *mock_ppclient.MockAnyPpClientService + wallet *mock_wallet.MockWallet + eventSender *mock_event.MockSender + periodicGetStatus *mock_periodicsync.MockPeriodicSync + identitiesUpdater *mockGlobalNamesUpdater + + *service +} + +func newFixture(t *testing.T) *fixture { + fx := &fixture{ + a: new(app.App), + ctrl: gomock.NewController(t), + service: New().(*service), + } + + fx.cache = mock_cache.NewMockCacheService(t) + fx.ppclient = mock_ppclient.NewMockAnyPpClientService(fx.ctrl) + fx.wallet = mock_wallet.NewMockWallet(t) + fx.eventSender = mock_event.NewMockSender(t) + + // init w mock + SignKey := "psqF8Rj52Ci6gsUl5ttwBVhINTP8Yowc2hea73MeFm4Ek9AxedYSB4+r7DYCclDL4WmLggj2caNapFUmsMtn5Q==" + decodedSignKey, err := crypto.DecodeKeyFromString( + SignKey, + crypto.UnmarshalEd25519PrivateKey, + nil) + + assert.NoError(t, err) + + ak := accountdata.AccountKeys{ + PeerId: "123", + SignKey: decodedSignKey, + } + + fx.wallet.EXPECT().Account().Return(&ak).Maybe() + fx.wallet.EXPECT().GetAccountPrivkey().Return(decodedSignKey).Maybe() + + fx.eventSender.EXPECT().Broadcast(mock.AnythingOfType("*pb.Event")).Maybe() + + ctx = context.WithValue(ctx, "dontRunPeriodicGetStatus", true) + + fx.a.Register(fx.service). + Register(testutil.PrepareMock(ctx, fx.a, fx.cache)). + Register(testutil.PrepareMock(ctx, fx.a, fx.ppclient)). + Register(testutil.PrepareMock(ctx, fx.a, fx.wallet)). + Register(testutil.PrepareMock(ctx, fx.a, fx.eventSender)). + Register(fx.identitiesUpdater) + + require.NoError(t, fx.a.Start(ctx)) + return fx +} + +func (fx *fixture) finish(t *testing.T) { + assert.NoError(t, fx.a.Close(ctx)) +} + +func TestGetStatus(t *testing.T) { + t.Run("success if no cache and GetSubscriptionStatus returns error", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return nil, errors.New("test error") + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + fx.cache.EXPECT().CacheEnable().Return(nil) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierUnknown), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(psp.SubscriptionStatus_StatusUnknown), resp.Data.Status) + }) + + t.Run("success if NoCache flag is passed", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return nil, errors.New("test error") + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + fx.cache.EXPECT().CacheEnable().Return(nil) + + // Call the function being tested + req := pb.RpcMembershipGetStatusRequest{ + // / >>> here: + NoCache: true, + } + resp, err := fx.GetSubscriptionStatus(ctx, &req) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierUnknown), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(psp.SubscriptionStatus_StatusUnknown), resp.Data.Status) + }) + + t.Run("success if cache is expired and GetSubscriptionStatus returns no error", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: psp.PaymentMethod_MethodCrypto, + RequestedAnyName: "something.any", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + // fx.cache.EXPECT().CacheEnable().Return(nil) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierExplorer), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(2), resp.Data.Status) + assert.Equal(t, sr.DateStarted, resp.Data.DateStarted) + assert.Equal(t, sr.DateEnds, resp.Data.DateEnds) + assert.Equal(t, true, resp.Data.IsAutoRenew) + assert.Equal(t, model.MembershipPaymentMethod(1), resp.Data.PaymentMethod) + assert.Equal(t, "something.any", resp.Data.RequestedAnyName) + }) + + t.Run("success if cache is disabled and GetSubscriptionStatus returns no error", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: psp.PaymentMethod_MethodCrypto, + RequestedAnyName: "something.any", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + // here: cache is disabled + fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, cache.ErrCacheDisabled) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + // fx.cache.EXPECT().CacheEnable().Return(nil) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierExplorer), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(2), resp.Data.Status) + assert.Equal(t, sr.DateStarted, resp.Data.DateStarted) + assert.Equal(t, sr.DateEnds, resp.Data.DateEnds) + assert.Equal(t, true, resp.Data.IsAutoRenew) + assert.Equal(t, model.MembershipPaymentMethod(1), resp.Data.PaymentMethod) + assert.Equal(t, "something.any", resp.Data.RequestedAnyName) + }) + + t.Run("fail if no cache, GetSubscriptionStatus returns no error, but can not save to cache", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: psp.PaymentMethod_MethodCrypto, + RequestedAnyName: "something.any", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return errors.New("can not write to cache!") + }) + + // Call the function being tested + _, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.Error(t, err) + + // resp object is nil in case of error + // assert.Equal(t, pb.RpcPaymentsSubscriptionGetStatusResponseErrorCode(pb.RpcPaymentsSubscriptionGetStatusResponseError_UNKNOWN_ERROR), resp.Error.Code) + // assert.Equal(t, "can not write to cache!", resp.Error.Description) + }) + + t.Run("success if in cache", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: model.MembershipStatus(2), + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: model.MembershipPaymentMethod(1), + RequestedAnyName: "something.any", + }, + } + + // HERE>>> + fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, nil) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierExplorer), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(2), resp.Data.Status) + }) + + t.Run("if GetSubscriptionStatus returns 0 tier -> cache it for 10 days", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierUnknown), + Status: psp.SubscriptionStatus_StatusUnknown, + DateStarted: 0, + DateEnds: 0, + IsAutoRenew: false, + PaymentMethod: psp.PaymentMethod_MethodCard, + RequestedAnyName: "", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheEnable().Return(nil) + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + // here time.Now() will be passed which can be a bit different from the the cacheExpireTime + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheDisabled) + fx.cache.EXPECT().CacheSet(&psgsr, mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierUnknown), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(0), resp.Data.Status) + assert.Equal(t, uint64(0), resp.Data.DateStarted) + assert.Equal(t, uint64(0), resp.Data.DateEnds) + assert.Equal(t, false, resp.Data.IsAutoRenew) + assert.Equal(t, model.MembershipPaymentMethod(0), resp.Data.PaymentMethod) + assert.Equal(t, "", resp.Data.RequestedAnyName) + }) + + t.Run("if GetSubscriptionStatus returns active tier and it expires in 5 days -> cache it for 5 days", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + var subsExpire5 time.Time = timeNow.Add(365 * 24 * time.Hour) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire5.Unix()), + IsAutoRenew: false, + PaymentMethod: psp.PaymentMethod_MethodCard, + RequestedAnyName: "", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(&psgsr, mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + fx.cache.EXPECT().CacheEnable().Return(nil) + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierExplorer), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(2), resp.Data.Status) + }) + + t.Run("if cache was disabled and tier has changed -> save, but enable cache back", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + var subsExpire5 time.Time = timeNow.Add(365 * 24 * time.Hour) + + // this is from PP node + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierBuilder1Year), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire5.Unix()), + IsAutoRenew: false, + PaymentMethod: psp.PaymentMethod_MethodCard, + RequestedAnyName: "", + } + + // this is from DB + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(model.Membership_TierExplorer), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + // this is the new state + var psgsr2 pb.RpcMembershipGetStatusResponse = psgsr + psgsr2.Data.Tier = int32(model.Membership_TierBuilder) + + fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) { + return &sr, nil + }).MinTimes(1) + + // return real struct and error + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheDisabled) + fx.cache.EXPECT().CacheSet(&psgsr2, mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + // this should be called + fx.cache.EXPECT().CacheEnable().Return(nil).Maybe() + + // Call the function being tested + resp, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{}) + assert.NoError(t, err) + + assert.Equal(t, int32(psp.SubscriptionTier_TierBuilder1Year), resp.Data.Tier) + assert.Equal(t, model.MembershipStatus(2), resp.Data.Status) + }) +} + +func TestGetPaymentURL(t *testing.T) { + t.Run("fail if BuySubscription method fails", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")) + + fx.ppclient.EXPECT().BuySubscription(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.BuySubscriptionResponse, error) { + return nil, errors.New("bad error") + }).MinTimes(1) + + // ethPrivateKey := ecdsa.PrivateKey{} + // w.EXPECT().GetAccountEthPrivkey().Return(ðPrivateKey) + + // Create a test request + req := &pb.RpcMembershipGetPaymentUrlRequest{ + RequestedTier: int32(model.Membership_TierBuilder), + PaymentMethod: model.Membership_MethodCrypto, + RequestedAnyName: "something.any", + } + + // Call the function being tested + _, err := fx.GetPaymentURL(ctx, req) + assert.Error(t, err) + }) + + t.Run("success", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")) + + fx.ppclient.EXPECT().BuySubscription(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.BuySubscriptionResponse, error) { + var out psp.BuySubscriptionResponse + out.PaymentUrl = "https://xxxx.com" + + return &out, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheDisableForNextMinutes(30).Return(nil).Once() + + // Create a test request + req := &pb.RpcMembershipGetPaymentUrlRequest{ + RequestedTier: int32(model.Membership_TierBuilder), + PaymentMethod: model.Membership_MethodCrypto, + RequestedAnyName: "something.any", + } + + // Call the function being tested + resp, err := fx.GetPaymentURL(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "https://xxxx.com", resp.PaymentUrl) + }) +} + +func TestGetPortalURL(t *testing.T) { + t.Run("fail if GetPortal method fails", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetSubscriptionPortalLink(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetSubscriptionPortalLinkResponse, error) { + return nil, errors.New("bad error") + }).MinTimes(1) + + // Create a test request + req := &pb.RpcMembershipGetPortalLinkUrlRequest{} + + // Call the function being tested + _, err := fx.GetPortalLink(ctx, req) + assert.Error(t, err) + }) + + t.Run("success", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetSubscriptionPortalLink(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetSubscriptionPortalLinkResponse, error) { + return &psp.GetSubscriptionPortalLinkResponse{ + PortalUrl: "https://xxxx.com", + }, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheDisableForNextMinutes(30).Return(nil).Once() + + // Create a test request + req := &pb.RpcMembershipGetPortalLinkUrlRequest{} + + // Call the function being tested + resp, err := fx.GetPortalLink(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "https://xxxx.com", resp.PortalUrl) + }) +} + +func TestGetVerificationEmail(t *testing.T) { + t.Run("fail if GetVerificationEmail method fails", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetVerificationEmail(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetVerificationEmailResponse, error) { + return nil, errors.New("bad error") + }).MinTimes(1) + + // Create a test request + req := &pb.RpcMembershipGetVerificationEmailRequest{} + req.Email = "some@mail.com" + req.SubscribeToNewsletter = true + + // Call the function being tested + _, err := fx.GetVerificationEmail(ctx, req) + assert.Error(t, err) + }) + + t.Run("success", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetVerificationEmail(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetVerificationEmailResponse, error) { + return &psp.GetVerificationEmailResponse{}, nil + }).MinTimes(1) + + // Create a test request + req := &pb.RpcMembershipGetVerificationEmailRequest{} + req.Email = "some@mail.com" + req.SubscribeToNewsletter = true + + // Call the function being tested + resp, err := fx.GetVerificationEmail(ctx, req) + assert.NoError(t, err) + assert.True(t, resp.Error == nil) + }) +} + +func TestVerifyEmailCode(t *testing.T) { + t.Run("fail if VerifyEmail method fails", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + // no errors + fx.ppclient.EXPECT().VerifyEmail(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.VerifyEmailResponse, error) { + return nil, errors.New("bad error") + }).MinTimes(1) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")) + + // Create a test request + req := &pb.RpcMembershipVerifyEmailCodeRequest{} + req.Code = "1234" + + // Call the function being tested + _, err := fx.VerifyEmailCode(ctx, req) + assert.Error(t, err) + }) + + t.Run("success", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + // no errors + fx.ppclient.EXPECT().VerifyEmail(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.VerifyEmailResponse, error) { + return &psp.VerifyEmailResponse{}, nil + }).MinTimes(1) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")) + + fx.cache.EXPECT().CacheClear().Return(nil).Once() + + // Create a test request + req := &pb.RpcMembershipVerifyEmailCodeRequest{} + req.Code = "1234" + + // Call the function being tested + _, err := fx.VerifyEmailCode(ctx, req) + assert.NoError(t, err) + }) +} + +func TestFinalizeSubscription(t *testing.T) { + t.Run("fail if FinalizeSubscription method fails", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + // no errors + fx.ppclient.EXPECT().FinalizeSubscription(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.FinalizeSubscriptionResponse, error) { + return nil, errors.New("bad error") + }).MinTimes(1) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")).Once() + + // Create a test request + req := &pb.RpcMembershipFinalizeRequest{} + + // Call the function being tested + _, err := fx.FinalizeSubscription(ctx, req) + assert.Error(t, err) + }) + + t.Run("success", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + // no errors + fx.ppclient.EXPECT().FinalizeSubscription(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.FinalizeSubscriptionResponse, error) { + return &psp.FinalizeSubscriptionResponse{}, nil + }).MinTimes(1) + + fx.wallet.EXPECT().GetAccountEthAddress().Return(common.HexToAddress("0x55DCad916750C19C4Ec69D65Ff0317767B36cE90")).Once() + + fx.cache.EXPECT().CacheClear().Return(nil).Once() + + // Create a test request + req := &pb.RpcMembershipFinalizeRequest{} + + // Call the function being tested + _, err := fx.FinalizeSubscription(ctx, req) + assert.NoError(t, err) + }) +} + +func TestGetTiers(t *testing.T) { + t.Run("fail if no cache, pp client returned error", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetAllTiers(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetTiersResponse, error) { + return nil, errors.New("test error") + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + + req := pb.RpcMembershipTiersGetRequest{ + NoCache: false, + Locale: "EN_us", + PaymentMethod: 0, + } + _, err := fx.GetTiers(ctx, &req) + assert.Error(t, err) + }) + + t.Run("success if no cache, empty response", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + fx.ppclient.EXPECT().GetAllTiers(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetTiersResponse, error) { + return &psp.GetTiersResponse{}, nil + }).MinTimes(1) + + req := pb.RpcMembershipTiersGetRequest{ + NoCache: true, + Locale: "EN_us", + PaymentMethod: 0, + } + _, err := fx.GetTiers(ctx, &req) + assert.NoError(t, err) + }) + + t.Run("success if no cache, response", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + fx.ppclient.EXPECT().GetAllTiers(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetTiersResponse, error) { + return &psp.GetTiersResponse{ + + Tiers: []*psp.TierData{ + { + Id: 1, + Name: "Explorer", + Description: "Explorer tier", + IsActive: true, + IsHiddenTier: false, + }, + }, + }, nil + }).MinTimes(1) + + req := pb.RpcMembershipTiersGetRequest{ + NoCache: false, + Locale: "EN_us", + PaymentMethod: 0, + } + out, err := fx.GetTiers(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, 1, len(out.Tiers)) + + assert.Equal(t, uint32(1), out.Tiers[0].Id) + assert.Equal(t, "Explorer", out.Tiers[0].Name) + assert.Equal(t, "Explorer tier", out.Tiers[0].Description) + assert.Equal(t, true, out.Tiers[0].IsActive) + assert.Equal(t, false, out.Tiers[0].IsHiddenTier) + }) + + t.Run("success if status is in cache", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: psp.PaymentMethod_MethodCrypto, + RequestedAnyName: "something.any", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, nil) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + fx.ppclient.EXPECT().GetAllTiers(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetTiersResponse, error) { + return &psp.GetTiersResponse{ + + Tiers: []*psp.TierData{ + { + Id: 1, + Name: "Explorer", + Description: "Explorer tier", + IsActive: true, + IsHiddenTier: false, + }, + }, + }, nil + }).MinTimes(1) + + req := pb.RpcMembershipTiersGetRequest{ + NoCache: false, + Locale: "EN_us", + PaymentMethod: 0, + } + out, err := fx.GetTiers(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, 1, len(out.Tiers)) + + assert.Equal(t, uint32(1), out.Tiers[0].Id) + assert.Equal(t, "Explorer", out.Tiers[0].Name) + assert.Equal(t, "Explorer tier", out.Tiers[0].Description) + assert.Equal(t, true, out.Tiers[0].IsActive) + assert.Equal(t, false, out.Tiers[0].IsHiddenTier) + }) + + t.Run("success if full status is in cache", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + sr := psp.GetSubscriptionResponse{ + Tier: int32(psp.SubscriptionTier_TierExplorer), + Status: psp.SubscriptionStatus_StatusActive, + DateStarted: uint64(timeNow.Unix()), + DateEnds: uint64(subsExpire.Unix()), + IsAutoRenew: true, + PaymentMethod: psp.PaymentMethod_MethodCrypto, + RequestedAnyName: "something.any", + } + + psgsr := pb.RpcMembershipGetStatusResponse{ + Data: &model.Membership{ + Tier: int32(sr.Tier), + Status: model.MembershipStatus(sr.Status), + DateStarted: sr.DateStarted, + DateEnds: sr.DateEnds, + IsAutoRenew: sr.IsAutoRenew, + PaymentMethod: model.MembershipPaymentMethod(sr.PaymentMethod), + RequestedAnyName: sr.RequestedAnyName, + }, + } + + tgr := pb.RpcMembershipTiersGetResponse{ + Tiers: []*model.MembershipTierData{ + { + Id: 1, + Name: "Explorer", + Description: "Explorer tier", + IsActive: true, + IsHiddenTier: false, + }, + }, + } + fx.cache.EXPECT().CacheGet().Return(&psgsr, &tgr, nil) + + req := pb.RpcMembershipTiersGetRequest{ + NoCache: false, + Locale: "EN_us", + PaymentMethod: 0, + } + out, err := fx.GetTiers(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, 1, len(out.Tiers)) + + assert.Equal(t, uint32(1), out.Tiers[0].Id) + assert.Equal(t, "Explorer", out.Tiers[0].Name) + assert.Equal(t, "Explorer tier", out.Tiers[0].Description) + assert.Equal(t, true, out.Tiers[0].IsActive) + assert.Equal(t, false, out.Tiers[0].IsHiddenTier) + }) +} + +func TestIsNameValid(t *testing.T) { + t.Run("success if reading from cache", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + tgr := pb.RpcMembershipTiersGetResponse{ + Tiers: []*model.MembershipTierData{ + { + Id: 1, + Name: "Explorer", + Description: "Explorer tier", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 1, + AnyNameMinLength: 5, + }, + { + Id: 2, + Name: "Suppa", + Description: "Suppa tieren", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 2, + AnyNameMinLength: 7, + }, + { + Id: 3, + Name: "NoNamme", + Description: "Nicht Suppa tieren", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 0, + AnyNameMinLength: 0, + }, + }, + } + fx.cache.EXPECT().CacheGet().Return(nil, &tgr, nil) + + req := pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 0, + RequestedAnyName: "something.any", + } + resp, err := fx.IsNameValid(ctx, &req) + assert.Error(t, err) + assert.Equal(t, (*pb.RpcMembershipIsNameValidResponse)(nil), resp) + + // 2 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 1, + RequestedAnyName: "something.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, (*pb.RpcMembershipIsNameValidResponseError)(nil), resp.Error) + + // 3 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 2, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, pb.RpcMembershipIsNameValidResponseError_TOO_SHORT, resp.Error.Code) + + // 4 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 3, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, pb.RpcMembershipIsNameValidResponseError_TIER_FEATURES_NO_NAME, resp.Error.Code) + + // 5 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 4, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.Error(t, err) + }) + + t.Run("success if asking directly from node", func(t *testing.T) { + fx := newFixture(t) + defer fx.finish(t) + + fx.ppclient.EXPECT().GetAllTiers(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in interface{}) (*psp.GetTiersResponse, error) { + return &psp.GetTiersResponse{ + + Tiers: []*psp.TierData{ + { + Id: 1, + Name: "Explorer", + Description: "Explorer tier", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 1, + AnyNameMinLength: 5, + }, + { + Id: 2, + Name: "Suppa", + Description: "Suppa tieren", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 2, + AnyNameMinLength: 7, + }, + { + Id: 3, + Name: "NoNamme", + Description: "Nicht Suppa tieren", + IsActive: true, + IsHiddenTier: false, + AnyNamesCountIncluded: 0, + AnyNameMinLength: 0, + }, + }, + }, nil + }).MinTimes(1) + + fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired) + fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipTiersGetResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipTiersGetResponse, expire time.Time) (err error) { + return nil + }) + + req := pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 0, + RequestedAnyName: "something.any", + } + resp, err := fx.IsNameValid(ctx, &req) + assert.Error(t, err) + assert.Equal(t, (*pb.RpcMembershipIsNameValidResponse)(nil), resp) + + // 2 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 1, + RequestedAnyName: "something.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, (*pb.RpcMembershipIsNameValidResponseError)(nil), resp.Error) + + // 3 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 2, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, pb.RpcMembershipIsNameValidResponseError_TOO_SHORT, resp.Error.Code) + + // 4 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 3, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.NoError(t, err) + assert.Equal(t, pb.RpcMembershipIsNameValidResponseError_TIER_FEATURES_NO_NAME, resp.Error.Code) + + // 5 + req = pb.RpcMembershipIsNameValidRequest{ + RequestedTier: 4, + RequestedAnyName: "somet.any", + } + resp, err = fx.IsNameValid(ctx, &req) + assert.Error(t, err) + }) +} diff --git a/core/payments/utils.go b/core/payments/utils.go new file mode 100644 index 000000000..82b687fd8 --- /dev/null +++ b/core/payments/utils.go @@ -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: + // ::=