From 1840212d74578be34de11638ed2faa882f10e970 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Apr 2026 15:26:36 -0400 Subject: [PATCH 1/5] feat: bring back pgv validation rules with protovalidate-kt Signed-off-by: Brandon McAnsh --- definitions/flipcash/models/build.gradle.kts | 8 +- .../account/v1/flipcash_account_service.proto | 46 +- .../activity/v1/activity_feed_service.proto | 51 +- .../src/main/proto/activity/v1/model.proto | 39 +- .../src/main/proto/common/v1/common.proto | 85 ++- .../email/v1/email_verification_service.proto | 34 +- .../src/main/proto/email/v1/model.proto | 11 +- .../event/v1/event_streaming_service.proto | 69 ++ .../src/main/proto/event/v1/model.proto | 73 ++ .../src/main/proto/iap/v1/iap_service.proto | 37 +- .../moderation/v1/moderation_service.proto | 37 +- .../src/main/proto/phone/v1/model.proto | 11 +- .../phone/v1/phone_verification_service.proto | 33 +- .../src/main/proto/profile/v1/model.proto | 51 +- .../proto/profile/v1/profile_service.proto | 51 +- .../protos/src/main/proto/push/v1/model.proto | 8 + .../src/main/proto/push/v1/push_service.proto | 28 +- .../proto/settings/v1/settings_service.proto | 10 +- .../src/main/proto/thirdparty/v1/model.proto | 21 +- .../thirdparty/v1/third_party_service.proto | 29 +- definitions/opencode/models/build.gradle.kts | 8 +- .../proto/account/v1/account_service.proto | 20 +- .../src/main/proto/common/v1/model.proto | 101 ++- .../proto/currency/v1/currency_service.proto | 459 ++++++++++-- .../messaging/v1/messaging_service.proto | 131 +++- .../transaction/v1/transaction_service.proto | 689 +++++++++++++++--- gradle/libs.versions.toml | 3 + scripts/fetch-protos.sh | 28 +- scripts/strip-proto-validation.sh | 47 -- settings.gradle.kts | 2 + 30 files changed, 1835 insertions(+), 385 deletions(-) create mode 100644 definitions/flipcash/protos/src/main/proto/event/v1/event_streaming_service.proto create mode 100644 definitions/flipcash/protos/src/main/proto/event/v1/model.proto delete mode 100755 scripts/strip-proto-validation.sh diff --git a/definitions/flipcash/models/build.gradle.kts b/definitions/flipcash/models/build.gradle.kts index 1383d28e3..08bee9525 100644 --- a/definitions/flipcash/models/build.gradle.kts +++ b/definitions/flipcash/models/build.gradle.kts @@ -1,8 +1,10 @@ +import dev.bmcreations.protovalidate.gradle.ProtoVariant import org.apache.tools.ant.taskdefs.condition.Os plugins { alias(libs.plugins.flipcash.android.library) - id("com.google.protobuf") + alias(libs.plugins.protobuf) + alias(libs.plugins.protobuf.validate) } val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" @@ -64,3 +66,7 @@ protobuf { } } } + +protovalidate { + variant.set(ProtoVariant.PGV) +} \ No newline at end of file diff --git a/definitions/flipcash/protos/src/main/proto/account/v1/flipcash_account_service.proto b/definitions/flipcash/protos/src/main/proto/account/v1/flipcash_account_service.proto index cc0dea785..8840fd901 100644 --- a/definitions/flipcash/protos/src/main/proto/account/v1/flipcash_account_service.proto +++ b/definitions/flipcash/protos/src/main/proto/account/v1/flipcash_account_service.proto @@ -1,30 +1,39 @@ syntax = "proto3"; + package flipcash.account.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/account/v1;acountpb"; option java_package = "com.codeinc.flipcash.gen.account.v1"; option objc_class_prefix = "FPBAccountV1"; + import "common/v1/common.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; service Account { // Register registers a new user, bound to the provided PublicKey. // If the PublicKey is already in use, the previous user account is returned. rpc Register(RegisterRequest) returns (RegisterResponse); + // Login retrieves the UserId (and in the future, potentially other information) // required for 'recovering' an account. rpc Login(LoginRequest) returns (LoginResponse); + // GetUserFlags gets user-specific flags. rpc GetUserFlags(GetUserFlagsRequest) returns (GetUserFlagsResponse); + // GetUserFlags gets user flags for unauthenticated users rpc GetUnauthenticatedUserFlags(GetUnauthenticatedUserFlagsRequest) returns (GetUnauthenticatedUserFlagsResponse); } + message RegisterRequest { // PublicKey the public key that is authorized to perform actions on the // registered users behalf. - common.v1.PublicKey public_key = 1; + common.v1.PublicKey public_key = 1 [(validate.rules).message.required = true]; + // Signature of this message (without the signature), using the provided keypair. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; } message RegisterResponse { Result result = 1; @@ -33,16 +42,19 @@ message RegisterResponse { INVALID_SIGNATURE = 1; DENIED = 2; } + // The UserId associated with the account. common.v1.UserId user_id = 2; } + message LoginRequest { // Timestamp is the timestamp the request was generated // // The server may reject the request if the timestamp is too far off // the current (server) time. This is to prevent replay attacks. - google.protobuf.Timestamp timestamp = 1; - common.v1.Auth auth = 2; + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } message LoginResponse { Result result = 1; @@ -51,12 +63,17 @@ message LoginResponse { INVALID_TIMESTAMP = 1; DENIED = 2; } + common.v1.UserId user_id = 2; } + message GetUserFlagsRequest { - common.v1.UserId user_id = 1; - common.v1.Auth auth = 2; + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; + common.v1.Platform platform = 3; + common.v1.CountryCode country_code = 4; } message GetUserFlagsResponse { @@ -65,10 +82,13 @@ message GetUserFlagsResponse { OK = 0; DENIED = 1; } + UserFlags user_flags = 2; } + message GetUnauthenticatedUserFlagsRequest { common.v1.Platform platform = 1; + common.v1.CountryCode country_code = 2; } message GetUnauthenticatedUserFlagsResponse { @@ -76,15 +96,20 @@ message GetUnauthenticatedUserFlagsResponse { enum Result { OK = 0; } + UserFlags user_flags = 2; } + message UserFlags { // Is this a fully registered account using IAP for account creation? bool is_registered_account = 1; + // Is this user associated with a Flipcash staff member? bool is_staff = 2; + // Does this user require IAP for registration in the account creation flow? bool requires_iap_for_registration = 3; + enum OnRampProvider { UNKNOWN = 0; COINBASE_VIRTUAL = 1; @@ -96,18 +121,25 @@ message UserFlags { BACKPACK = 7; BASE = 8; } + // The set of supported on ramp providers for the user, based on their platform // and locale if provided - repeated OnRampProvider supported_on_ramp_providers = 4 ; + repeated OnRampProvider supported_on_ramp_providers = 4 [(validate.rules).repeated = { + min_items: 0 + max_items: 256 + }]; // The preferred on ramp provider for this user. If the value is UNKNOWN, client // should show the list of all supported providers. OnRampProvider preferred_on_ramp_provider = 5; + // The minumum build number for this user. If their build number is less than the // provided value, client should show a forced upgrade screen. uint32 min_build_number = 6; + // Exchange data timeout for sequential give/grabs for bills google.protobuf.Duration bill_exchange_data_timeout = 7; + // USDF amount, in quarks, that must be purchased when launching a new currency uint64 new_currency_purchase_amount = 8; } diff --git a/definitions/flipcash/protos/src/main/proto/activity/v1/activity_feed_service.proto b/definitions/flipcash/protos/src/main/proto/activity/v1/activity_feed_service.proto index 3f385874f..8d847d9d8 100644 --- a/definitions/flipcash/protos/src/main/proto/activity/v1/activity_feed_service.proto +++ b/definitions/flipcash/protos/src/main/proto/activity/v1/activity_feed_service.proto @@ -1,53 +1,79 @@ syntax = "proto3"; + package flipcash.activity.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/activity/v1;activitypb"; option java_package = "com.codeinc.flipcash.gen.activity.v1"; option objc_class_prefix = "FPBActivityV1"; + import "activity/v1/model.proto"; import "common/v1/common.proto"; +import "validate/validate.proto"; service ActivityFeed { // GetLatestNotifications gets the latest N notifications in a user's // activity feed. Results will be ordered by descending timestamp. rpc GetLatestNotifications(GetLatestNotificationsRequest) returns (GetLatestNotificationsResponse); + // GetPagedNotifications gets all notifications using a paging API. rpc GetPagedNotifications(GetPagedNotificationsRequest) returns (GetPagedNotificationsResponse); + // GetBatchNotifications gets a batch of notifications by ID. rpc GetBatchNotifications(GetBatchNotificationsRequest) returns (GetBatchNotificationsResponse); } + message GetLatestNotificationsRequest { // The activity feed to fetch notifications from - ActivityFeedType type = 1; + ActivityFeedType type = 1 [(validate.rules).enum.in = 1]; + // Maximum number of notifications to return. If <= 0, the server default is used - int32 max_items = 2; - common.v1.Auth auth = 3; + int32 max_items = 2 [(validate.rules).int32.lte = 1024]; + + common.v1.Auth auth = 3 [(validate.rules).message.required = true]; } + message GetLatestNotificationsResponse { Result result = 1; enum Result { OK = 0; DENIED = 1; } - repeated Notification notifications = 2 ; + + repeated Notification notifications = 2 [(validate.rules).repeated = { + max_items: 1024 + }]; } + message GetPagedNotificationsRequest { // The activity feed to fetch notifications from - ActivityFeedType type = 1; - common.v1.QueryOptions query_options = 2; - common.v1.Auth auth = 3; + ActivityFeedType type = 1 [(validate.rules).enum.in = 1]; + + common.v1.QueryOptions query_options = 2 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 3 [(validate.rules).message.required = true]; } + message GetPagedNotificationsResponse { Result result = 1; enum Result { OK = 0; DENIED = 1; } - repeated Notification notifications = 2 ; + + repeated Notification notifications = 2 [(validate.rules).repeated = { + max_items: 1024 + }]; } + message GetBatchNotificationsRequest { - repeated NotificationId ids = 1 ; - common.v1.Auth auth = 2; + repeated NotificationId ids = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 + }]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message GetBatchNotificationsResponse { Result result = 1; enum Result { @@ -55,5 +81,8 @@ message GetBatchNotificationsResponse { DENIED = 1; NOT_FOUND = 2; } - repeated Notification notifications = 2 ; + + repeated Notification notifications = 2 [(validate.rules).repeated = { + max_items: 1024 + }]; } diff --git a/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto b/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto index c38a6834d..f0d0a876e 100644 --- a/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto @@ -1,27 +1,43 @@ syntax = "proto3"; + package flipcash.activity.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/activity/v1;activitypb"; option java_package = "com.codeinc.flipcash.gen.activity.v1"; option objc_class_prefix = "FCPBActivityV1"; + import "common/v1/common.proto"; import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; // The ID of the notification message NotificationId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; } + // Notification is a message that is displayed in an activity feed message Notification { // The ID of this notification - NotificationId id = 1; + NotificationId id = 1 [(validate.rules).message.required = true]; + // The localized title text for the notification - string localized_text = 2 ; + string localized_text = 2 [(validate.rules).string = { + min_len: 1 + max_len: 256 + }]; + // If a payment applies, the amount that was paid common.v1.CryptoPaymentAmount payment_amount = 3; + // The timestamp of this notification - google.protobuf.Timestamp ts = 4; + google.protobuf.Timestamp ts = 4 [(validate.rules).timestamp.required = true]; + // The state of this notification - NotificationState state = 5; + NotificationState state = 5 [(validate.rules).enum.not_in = 0]; + // Additional metadata for this notification specific to the notification oneof additional_metadata { GaveCryptoNotificationMetadata gave_crypto = 7; @@ -32,32 +48,43 @@ message Notification { BoughtCryptoNotificationMetadata bought_crypto = 12; SoldCryptoNotificationMetadata sold_crypto = 13; } + reserved 6; // Deprecated WelcomeBonusNotificationMetadata } + message GaveCryptoNotificationMetadata { } + message ReceivedCryptoNotificationMetadata { } + message WithdrewCryptoNotificationMetadata { } + message SentCryptoNotificationMetadata { // The vault of the gift card account that was created for the cash link - common.v1.PublicKey vault = 1; + common.v1.PublicKey vault = 1 [(validate.rules).message.required = true]; + // Whether the cancel action can be initiated by the user bool can_initiate_cancel_action = 2; } + message DepositedCryptoNotificationMetadata { } + message BoughtCryptoNotificationMetadata { } + message SoldCryptoNotificationMetadata { } + // ActivityFeedType enables multiple activity feeds, where notifications may be // split across different parts of the app enum ActivityFeedType { UNKNOWN = 0; TRANSACTION_HISTORY = 1; // Activity feed displayed under the Balance tab } + // NotificationState determines the mutability of a notification, and whether // client should attempt to refetch state. enum NotificationState { diff --git a/definitions/flipcash/protos/src/main/proto/common/v1/common.proto b/definitions/flipcash/protos/src/main/proto/common/v1/common.proto index 53003bb15..752669f6b 100644 --- a/definitions/flipcash/protos/src/main/proto/common/v1/common.proto +++ b/definitions/flipcash/protos/src/main/proto/common/v1/common.proto @@ -1,77 +1,114 @@ syntax = "proto3"; + package flipcash.common.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/common/v1;commonpb"; option java_package = "com.codeinc.flipcash.gen.common.v1"; option objc_class_prefix = "FPBCommonV1"; +import "validate/validate.proto"; + message PublicKey { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; } + message Signature { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 64 + max_len: 64 + }]; } + // Auth provides an authentication information for RPCs/messages. // // Currently, only a single form is supported, but it may be useful in // the future to rely on session tokens instead. message Auth { oneof kind { + option (validate.required) = true; + // KeyPair uses pub key cryptography to verify. KeyPair key_pair = 1; } + // KeyPair uses a keypair to verify a message. // // The signature should be of the encapsulating proto message, // _without_ the Auth section being set. message KeyPair { - PublicKey pub_key = 1; - Signature signature = 2; + PublicKey pub_key = 1 [(validate.rules).message.required = true]; + Signature signature = 2 [(validate.rules).message.required = true]; } } + message UserId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 1 + max_len: 32 + }]; } + // AppInstallId is a unque ID tied to a client app installation. It does not // identify a device. Value should remain private and not be shared across // installs. message AppInstallId { - string value = 1 ; + string value = 1 [(validate.rules).string = { + min_len: 1 + max_len: 256 // todo: What's a reasonable size + }]; } + enum Platform { UNKNOWN = 0; APPLE = 1; GOOGLE = 2; } + // CryptoPaymentAmount defines an amount of crypto with currency exchange data message CryptoPaymentAmount { // ISO 4217 alpha-3 currency code the payment was made in - string currency = 1; + string currency = 1 [(validate.rules).string = { pattern: "^[a-z]{3,4}$" }]; + // The amount in the native currency that was paid - double native_amount = 2; + double native_amount = 2 [(validate.rules).double.gte = 0]; + // The amount in quarks of crypto that was paid uint64 quarks = 3; + // The crypto mint that was paid - PublicKey mint = 4; + PublicKey mint = 4 [(validate.rules).message.required = true]; } + // FiatPaymentAmount defines an amount of fiat message FiatPaymentAmount { // ISO 4217 alpha-3 currency code the payment was made in - string currency = 1; + string currency = 1 [(validate.rules).string = { pattern: "^[a-z]{3,4}$" }]; + // The amount in the native currency that was paid - double native_amount = 2; + double native_amount = 2 [(validate.rules).double.gte = 0]; } + message PagingToken { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 1 + max_len: 128 + }]; } + message QueryOptions { // PageSize limits the maximum page size of a response. // // Server may choose to return less items. If <= 0, // server may select an arbitrary default page size. - int32 page_size = 1; + int32 page_size = 1 [(validate.rules).int32.lte = 1024]; + // PagingToken is a token that can be extracted from the // identifier of a collection. PagingToken paging_token = 2; + // Order is the order of elements, if applicable. Order order = 3; enum Order { @@ -79,6 +116,7 @@ message QueryOptions { DESC = 1; } } + // Request is a generic wrapper for gRPC requests message Request { string version = 1; @@ -86,25 +124,40 @@ message Request { string method = 3; bytes body = 4; } + // Response is a generic wrapper for gRPC responses message Response { Result result = 1; + bytes body = 2; string message = 3; + enum Result { OK = 0; ERROR = 1; } } + message CountryCode { // ISO 3166-1 Alpha-2 - string value = 1 ; + string value = 1 [(validate.rules).string = { + min_len: 2 + max_len: 2 + }]; } + // Locale represents an IETF BCP 47 language tag (e.g. "en", "en-US", "zh-Hans-CN") message Locale { - string value = 1 ; + string value = 1 [(validate.rules).string = { + pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*$" + min_len: 2 + max_len: 35 + }]; } + // Region represents a fiat currency region identified by its ISO 4217 alpha-3 currency code (e.g. "usd", "eur") message Region { - string value = 1 ; + string value = 1 [(validate.rules).string = { + pattern: "^[a-z]{3,4}$" + }]; } diff --git a/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto b/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto index b24764d47..6a3870148 100644 --- a/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto +++ b/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto @@ -1,16 +1,21 @@ syntax = "proto3"; + package flipcash.email.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/email/v1;emailpb"; option java_package = "com.codeinc.flipcash.gen.email.v1"; option objc_class_prefix = "FPBEmailV1"; + import "common/v1/common.proto"; import "email/v1/model.proto"; +import "validate/validate.proto"; service EmailVerification { // SendVerificationCode sends a verification code to the provided email address. // If an active verification is already taking place, the existing code will be // resent. rpc SendVerificationCode(SendVerificationCodeRequest) returns (SendVerificationCodeResponse); + // CheckVerificationCode validates a verification code. On success, the email // address is linked to the user. Any previous links are overwritten. rpc CheckVerificationCode(CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse); @@ -18,13 +23,19 @@ service EmailVerification { // Unlink removes the link of an email address from a user. rpc Unlink(UnlinkRequest) returns (UnlinkResponse); } + message SendVerificationCodeRequest { // The email address to send a verification code to - EmailAddress email_address = 1; - common.v1.Auth auth = 2; + EmailAddress email_address = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; + // Additional client data that is sent in the deep link - string client_data = 3 ; + string client_data = 3 [(validate.rules).string = { + max_len: 1024 + }]; } + message SendVerificationCodeResponse { Result result = 1; enum Result { @@ -37,13 +48,17 @@ message SendVerificationCodeResponse { INVALID_EMAIL_ADDRESS = 3; } } + message CheckVerificationCodeRequest { // The email address being verified - EmailAddress email_address = 1; + EmailAddress email_address = 1 [(validate.rules).message.required = true]; + // The verification code received via email - VerificationCode code = 2; - common.v1.Auth auth = 3; + VerificationCode code = 2 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 3 [(validate.rules).message.required = true]; } + message CheckVerificationCodeResponse { Result result = 1; enum Result { @@ -63,11 +78,14 @@ message CheckVerificationCodeResponse { NO_VERIFICATION = 4; } } + message UnlinkRequest { // The email address to unlink - EmailAddress email_address = 1; - common.v1.Auth auth = 2; + EmailAddress email_address = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message UnlinkResponse { Result result = 1; enum Result { diff --git a/definitions/flipcash/protos/src/main/proto/email/v1/model.proto b/definitions/flipcash/protos/src/main/proto/email/v1/model.proto index 2183719f1..8efb34449 100644 --- a/definitions/flipcash/protos/src/main/proto/email/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/email/v1/model.proto @@ -1,14 +1,21 @@ syntax = "proto3"; + package flipcash.email.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/email/v1;emailpb"; option java_package = "com.codeinc.flipcash.gen.email.v1"; option objc_class_prefix = "FPBEmailV1"; +import "validate/validate.proto"; + // EmailAddress is an email address message EmailAddress { - string value = 1; + string value = 1 [(validate.rules).string.pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"]; } + // VerificationCode is a 4-10 digit numerical code for verification message VerificationCode { - string value = 2 ; + string value = 2 [(validate.rules).string = { + pattern: "^[0-9]{4,10}$" + }]; } diff --git a/definitions/flipcash/protos/src/main/proto/event/v1/event_streaming_service.proto b/definitions/flipcash/protos/src/main/proto/event/v1/event_streaming_service.proto new file mode 100644 index 000000000..bc7f0ed2f --- /dev/null +++ b/definitions/flipcash/protos/src/main/proto/event/v1/event_streaming_service.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package flipcash.event.v1; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/event/v1;eventpb"; +option java_package = "com.codeinc.flipcash.gen.events.v1"; +option objc_class_prefix = "FPBEventV1"; + +import "event/v1/model.proto"; +import "common/v1/common.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +service EventStreaming { + // StreamEvents streams events for the requesting user. + rpc StreamEvents(stream StreamEventsRequest) returns (stream StreamEventsResponse); + + // ForwardEvents is an internal RPC for forwarding events to another server. + rpc ForwardEvents(ForwardEventsRequest) returns (ForwardEventsResponse); +} + +message StreamEventsRequest { + oneof type { + option (validate.required) = true; + + Params params = 1; + ClientPong pong = 2; + } + + message Params { + common.v1.Auth auth = 1 [(validate.rules).message.required = true]; + + // ts contains the time for stream open. + // + // It is used primarily as a nonce for auth. Server may reject + // timestamps that are too far in the future or past. + google.protobuf.Timestamp ts = 2 [(validate.rules).timestamp.required = true]; + } +} + +message StreamEventsResponse { + oneof type { + option (validate.required) = true; + + ServerPing ping = 1; + StreamError error = 2; + EventBatch events = 3; + } + + message StreamError { + Code code = 1; + enum Code { + DENIED = 0; + INVALID_TIMESTAMP = 1; + } + } +} + +message ForwardEventsRequest { + UserEventBatch user_events = 1 [(validate.rules).message.required = true]; +} + +message ForwardEventsResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + } +} \ No newline at end of file diff --git a/definitions/flipcash/protos/src/main/proto/event/v1/model.proto b/definitions/flipcash/protos/src/main/proto/event/v1/model.proto new file mode 100644 index 000000000..3b1bfaa10 --- /dev/null +++ b/definitions/flipcash/protos/src/main/proto/event/v1/model.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package flipcash.event.v1; + +option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/event/v1;eventpb"; +option java_package = "com.codeinc.flipcash.gen.events.v1"; +option objc_class_prefix = "FPBEventV1"; + +import "common/v1/common.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +message EventId { + bytes id = 1 [(validate.rules).bytes = { + min_len: 16 + max_len: 16 + }]; +} + + // todo: define additional events +message Event { + EventId id = 1 [(validate.rules).message.required = true]; + + google.protobuf.Timestamp ts = 2 [(validate.rules).timestamp.required = true];; + + oneof type { + option (validate.required) = true; + + TestEvent test = 3; + } +} + +message EventBatch { + repeated Event events = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; +} + +message UserEvent { + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + + Event event = 2 [(validate.rules).message.required = true]; +} + +message UserEventBatch { + repeated UserEvent events = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; +} + +message TestEvent { + repeated string hops = 1; + + uint64 nonce = 2; +} + +message ServerPing { + // Timestamp the ping was sent on the stream, for client to get a sense + // of potential network latency + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; + + // The delay server will apply before sending the next ping + google.protobuf.Duration ping_delay = 2 [(validate.rules).duration.required = true]; +} + +message ClientPong { + // Timestamp the Pong was sent on the stream, for server to get a sense + // of potential network latency + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; +} diff --git a/definitions/flipcash/protos/src/main/proto/iap/v1/iap_service.proto b/definitions/flipcash/protos/src/main/proto/iap/v1/iap_service.proto index df325e068..fbd0b99cc 100644 --- a/definitions/flipcash/protos/src/main/proto/iap/v1/iap_service.proto +++ b/definitions/flipcash/protos/src/main/proto/iap/v1/iap_service.proto @@ -1,20 +1,29 @@ syntax = "proto3"; + package flipcash.iap.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/iap/v1;iappb"; option java_package = "com.codeinc.flipcash.gen.iap.v1"; option objc_class_prefix = "FPBIapV1"; + import "common/v1/common.proto"; +import "validate/validate.proto"; service Iap { // OnPurchaseCompleted is called when an IAP has been completed rpc OnPurchaseCompleted(OnPurchaseCompletedRequest) returns (OnPurchaseCompletedResponse); } + message OnPurchaseCompletedRequest { - common.v1.Platform platform = 1; - Receipt receipt = 2; - Metadata metadata = 3; - common.v1.Auth auth = 4; + common.v1.Platform platform = 1 [(validate.rules).enum = {in: [1,2]}]; + + Receipt receipt = 2 [(validate.rules).message.required = true]; + + Metadata metadata = 3 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 4 [(validate.rules).message.required = true]; } + message OnPurchaseCompletedResponse { Result result = 1; enum Result { @@ -24,13 +33,25 @@ message OnPurchaseCompletedResponse { INVALID_METADATA = 3; // Returned if the at least one field in the payment metadata is invalid } } + message Receipt { - string value = 1 ; + string value = 1 [(validate.rules).string = { + min_len: 1 + // todo: what's a reasonable max length? + }]; } + // Additional IAP metadata, which can be trusted given a verified receipt (they can // only be generated by production-signed apps). message Metadata { - string product = 1 ; - string currency = 2 ; - double amount = 3; + string product = 1 [(validate.rules).string = { + min_len: 1 + max_len: 128 + }]; + + string currency = 2 [(validate.rules).string = { + pattern: "^[a-z]{3}$" + }]; + + double amount = 3 [(validate.rules).double.gt = 0]; } diff --git a/definitions/flipcash/protos/src/main/proto/moderation/v1/moderation_service.proto b/definitions/flipcash/protos/src/main/proto/moderation/v1/moderation_service.proto index bf290fabb..b4bd7f348 100644 --- a/definitions/flipcash/protos/src/main/proto/moderation/v1/moderation_service.proto +++ b/definitions/flipcash/protos/src/main/proto/moderation/v1/moderation_service.proto @@ -1,40 +1,60 @@ syntax = "proto3"; + package flipcash.moderation.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/moderation/v1;moderationpb"; option java_package = "com.codeinc.flipcash.gen.moderation.v1"; option objc_class_prefix = "FPBModerationV1"; + import "common/v1/common.proto"; import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; service Moderation { // ModerateText checks text content against moderation policies rpc ModerateText(ModerateTextRequest) returns (ModerateTextResponse); + // ModerateImage checks image content against moderation policies rpc ModerateImage(ModerateImageRequest) returns (ModerateImageResponse); } + message ModerateTextRequest { // The text content to moderate - string text = 1 ; - common.v1.Auth auth = 2; + string text = 1 [(validate.rules).string = { + min_len: 1 + max_len: 4096 + }]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message ModerateTextResponse { Result result = 1; enum Result { OK = 0; DENIED = 1; } + // Whether the text content is allowed bool is_allowed = 2; + // Signed attestation of the moderation result when content is allowed ModerationAttestation attestation = 3; + // The best fit flagged category when content is not allowed FlaggedCategory flagged_category = 4; } + message ModerateImageRequest { // The raw image data to moderate - bytes image_data = 1 ; - common.v1.Auth auth = 2; + bytes image_data = 1 [(validate.rules).bytes = { + min_len: 1 + max_len: 1048576 // 1 MB + }]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message ModerateImageResponse { Result result = 1; enum Result { @@ -42,27 +62,36 @@ message ModerateImageResponse { DENIED = 1; UNSUPPORTED_FORMAT = 2; } + // Whether the image content is allowed bool is_allowed = 2; + // Signed attestation of the moderation result when content is allowed ModerationAttestation attestation = 3; + // The best fit flagged category when content is not allowed FlaggedCategory flagged_category = 4; } + // ModerationAttestation is a signed proof of the moderation result. // The signature is computed over this message without the signature field set. message ModerationAttestation { // SHA-256 hash of the moderated content to be allowed bytes content_hash = 1; + // Timestamp of the moderation google.protobuf.Timestamp timestamp = 2; + // The user who submitted the content common.v1.UserId user_id = 3; + // Public key of the attestor that signed this message common.v1.PublicKey attestor = 4; + // Attestor signature over this message common.v1.Signature signature = 5; } + enum FlaggedCategory { NONE = 0; OTHER = 1; // Fallback category when flagged content does not fit into a well-defined FlaggedCategory diff --git a/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto b/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto index 5b3517fc6..9a87c95f6 100644 --- a/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto @@ -1,15 +1,22 @@ syntax = "proto3"; + package flipcash.phone.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/phone/v1;phonepb"; option java_package = "com.codeinc.flipcash.gen.phone.v1"; option objc_class_prefix = "FPBPhoneV1"; +import "validate/validate.proto"; + // PhoneNumber is an E.164 phone number message PhoneNumber { // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - string value = 1; + string value = 1 [(validate.rules).string.pattern = "^\\+[1-9]\\d{1,14}$"]; } + // VerificationCode is a 4-10 digit numerical code for verification message VerificationCode { - string value = 2 ; + string value = 2 [(validate.rules).string = { + pattern: "^[0-9]{4,10}$" + }]; } diff --git a/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto b/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto index 58d5fb545..a4a50c445 100644 --- a/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto +++ b/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto @@ -1,29 +1,39 @@ syntax = "proto3"; + package flipcash.phone.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/phone/v1;phonepb"; option java_package = "com.codeinc.flipcash.gen.phone.v1"; option objc_class_prefix = "FPBPhoneV1"; + import "common/v1/common.proto"; import "phone/v1/model.proto"; +import "validate/validate.proto"; service PhoneVerification { // SendVerificationCode sends a verification code to the provided phone number // over SMS. If an active verification is already taking place, the existing code // will be resent. rpc SendVerificationCode(SendVerificationCodeRequest) returns (SendVerificationCodeResponse); + // CheckVerificationCode validates a verification code. On success, the phone number // is linked to the user. Any previous links are overwritten. rpc CheckVerificationCode(CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse); + // Unlink removes the link of a phone number from a user. rpc Unlink(UnlinkRequest) returns (UnlinkResponse); } + message SendVerificationCodeRequest { // The phone number to send a verification code over SMS to - PhoneNumber phone_number = 1; + PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + // The app platform that's making this request - common.v1.Platform platform = 2; - common.v1.Auth auth = 3; + common.v1.Platform platform = 2 [(validate.rules).enum = {in: [1,2]}]; + + common.v1.Auth auth = 3 [(validate.rules).message.required = true]; } + message SendVerificationCodeResponse { Result result = 1; enum Result { @@ -39,13 +49,17 @@ message SendVerificationCodeResponse { UNSUPPORTED_PHONE_TYPE = 4; } } + message CheckVerificationCodeRequest { // The phone number being verified - PhoneNumber phone_number = 1; + PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + // The verification code received via SMS - VerificationCode code = 2; - common.v1.Auth auth = 3; + VerificationCode code = 2 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 3 [(validate.rules).message.required = true]; } + message CheckVerificationCodeResponse { Result result = 1; enum Result { @@ -65,11 +79,14 @@ message CheckVerificationCodeResponse { NO_VERIFICATION = 4; } } + message UnlinkRequest { // The phone number to unlink - PhoneNumber phone_number = 1; - common.v1.Auth auth = 2; + PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message UnlinkResponse { Result result = 1; enum Result { diff --git a/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto b/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto index 9bab88e46..562b8432c 100644 --- a/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto @@ -1,39 +1,75 @@ syntax = "proto3"; + package flipcash.profile.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/profile/v1;profilepb"; option java_package = "com.codeinc.flipcash.gen.profile.v1"; option objc_class_prefix = "FPBProfileV1"; + import "email/v1/model.proto"; import "phone/v1/model.proto"; +import "validate/validate.proto"; message UserProfile { // Display name is the display name of the user (if found). - string display_name = 1 ; + string display_name = 1 [(validate.rules).string = { + min_len: 0 + max_len: 64 + }]; + // Social profiles are links to external social accounts - repeated SocialProfile social_profiles = 2 ; + repeated SocialProfile social_profiles = 2 [(validate.rules).repeated = { + min_items: 0 + max_items: 1 + }]; + + // Phone number linked to this user. This is private and will only be returned // when the requesting user asks for their own profile phone.v1.PhoneNumber phone_number = 3; + // Email address linked to this user. This is private and will only be returned // when the requesting user asks for their own profile email.v1.EmailAddress email_address = 4; } + message SocialProfile { oneof type { + option (validate.required) = true; + XProfile x = 1; } } + message XProfile { // The user's ID on X - string id = 1 ; + string id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 32 + }]; + // The user's username on X - string username = 2 ; + string username = 2 [(validate.rules).string = { + min_len: 1 + max_len: 15 + }]; + // The user's friendly name on X - string name = 3 ; + string name = 3 [(validate.rules).string = { + max_len: 256 + }]; + // The user's description on X - string description = 4 ; + string description = 4 [(validate.rules).string = { + max_len: 4096 // todo: arbitrary + }]; + // URL to the user's X profile picture - string profile_pic_url = 5 ; + string profile_pic_url = 5 [(validate.rules).string = { + min_len: 1 + max_len: 2048 // todo: arbitrary + }]; + // The type of X verification associated with the user VerifiedType verified_type = 6; enum VerifiedType { @@ -42,6 +78,7 @@ message XProfile { BUSINESS = 2; GOVERNMENT = 3; } + // The number of followers the user has on X uint32 follower_count = 7; } diff --git a/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto b/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto index 600331c9a..7e6e02c56 100644 --- a/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto +++ b/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto @@ -1,41 +1,58 @@ syntax = "proto3"; + package flipcash.profile.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/profile/v1;profilepb"; option java_package = "com.codeinc.flipcash.gen.profile.v1"; option objc_class_prefix = "FPBProfileV1"; + import "common/v1/common.proto"; import "profile/v1/model.proto"; +import "validate/validate.proto"; service Profile { rpc GetProfile(GetProfileRequest) returns (GetProfileResponse); + rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse); + // LinkSocialAccount links a social account to a user rpc LinkSocialAccount(LinkSocialAccountRequest) returns (LinkSocialAccountResponse); + // UnlinkSocialAccount removes a social account link from a user rpc UnlinkSocialAccount(UnlinkSocialAccountRequest) returns (UnlinkSocialAccountResponse); } + message GetProfileRequest { - common.v1.UserId user_id = 1; + common.v1.UserId user_id = 1 [(validate.rules).message.required = true]; + // Optional auth to retrieve private profile information for self common.v1.Auth auth = 2; } + message GetProfileResponse { Result result = 1; enum Result { OK = 0; NOT_FOUND = 1; } + // UserProfile, if found. // // Some fields may or may not be set, depending on the scope of request // in the future. UserProfile user_profile = 2; } + message SetDisplayNameRequest { // DisplayName is the new name to set. - string display_name = 1 ; - common.v1.Auth auth = 10; + string display_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 64 + }]; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } + message SetDisplayNameResponse { Result result = 1; enum Result { @@ -44,19 +61,29 @@ message SetDisplayNameResponse { DENIED = 2; } } + message LinkSocialAccountRequest { - LinkingToken linking_token = 1; + LinkingToken linking_token = 1 [(validate.rules).message.required = true]; + message LinkingToken { oneof type { + option (validate.required) = true; + XLinkingToken x = 1; } + message XLinkingToken { // X access token from the OAuth 2.0 flow - string access_token = 1; + string access_token = 1[(validate.rules).string = { + min_len: 1 + max_len: 4096 // todo: arbitrary + }]; } } - common.v1.Auth auth = 10; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } + message LinkSocialAccountResponse { Result result = 1; enum Result { @@ -65,14 +92,22 @@ message LinkSocialAccountResponse { EXISTING_LINK = 2; DENIED = 3; } + SocialProfile social_profile = 2; } + message UnlinkSocialAccountRequest { oneof social_identifier { - string x_user_id = 1 ; + option (validate.required) = true; + + string x_user_id = 1 [(validate.rules).string = { + max_len: 32 + }]; } - common.v1.Auth auth = 10; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } + message UnlinkSocialAccountResponse { Result result = 1; enum Result { diff --git a/definitions/flipcash/protos/src/main/proto/push/v1/model.proto b/definitions/flipcash/protos/src/main/proto/push/v1/model.proto index adf2c63ba..ae040fc32 100644 --- a/definitions/flipcash/protos/src/main/proto/push/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/push/v1/model.proto @@ -1,9 +1,13 @@ syntax = "proto3"; + package flipcash.push.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/push/v1;pushpb"; option java_package = "com.codeinc.flipcash.gen.push.v1"; option objc_class_prefix = "FPBPushV1"; + import "common/v1/common.proto"; +import "validate/validate.proto"; enum TokenType { UNKNOWN = 0; @@ -12,14 +16,18 @@ enum TokenType { // FCM registration token or an iOS device FCM_APNS = 2; } + // Payload provided as extra data in a push message Payload { // If present, where the app should navigate to after clicking the push Navigation navigation = 1; } + // Navigation within the app upon clicking the push message Navigation { oneof type { + option (validate.required) = true; + // Currency info page for the provided mint common.v1.PublicKey currency_info = 1; } diff --git a/definitions/flipcash/protos/src/main/proto/push/v1/push_service.proto b/definitions/flipcash/protos/src/main/proto/push/v1/push_service.proto index 562b970ac..fa1b9597e 100644 --- a/definitions/flipcash/protos/src/main/proto/push/v1/push_service.proto +++ b/definitions/flipcash/protos/src/main/proto/push/v1/push_service.proto @@ -1,23 +1,36 @@ syntax = "proto3"; + package flipcash.push.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/push/v1;pushpb"; option java_package = "com.codeinc.flipcash.gen.push.v1"; option objc_class_prefix = "FPBPushV1"; + import "common/v1/common.proto"; import "push/v1/model.proto"; +import "validate/validate.proto"; service Push { // AddToken adds a push token associated with a user. rpc AddToken(AddTokenRequest) returns (AddTokenResponse); + // DeleteTokens removes all push tokens within an app install for a user rpc DeleteTokens(DeleteTokensRequest) returns (DeleteTokensResponse); } + message AddTokenRequest { - TokenType token_type = 1; - string push_token = 2 ; - common.v1.AppInstallId app_install = 3; - common.v1.Auth auth = 4; + TokenType token_type = 1 [(validate.rules).enum = {in: [1,2]}]; + + string push_token = 2 [(validate.rules).string = { + min_len: 1 + max_len: 4096 + }]; + + common.v1.AppInstallId app_install = 3 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 4 [(validate.rules).message.required = true]; } + message AddTokenResponse { Result result = 1; enum Result { @@ -25,10 +38,13 @@ message AddTokenResponse { INVALID_PUSH_TOKEN = 1; } } + message DeleteTokensRequest { - common.v1.AppInstallId app_install = 1; - common.v1.Auth auth = 2; + common.v1.AppInstallId app_install = 1 [(validate.rules).message.required = true]; + + common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } + message DeleteTokensResponse { Result result = 1; enum Result { diff --git a/definitions/flipcash/protos/src/main/proto/settings/v1/settings_service.proto b/definitions/flipcash/protos/src/main/proto/settings/v1/settings_service.proto index d70fcebba..3fdf614da 100644 --- a/definitions/flipcash/protos/src/main/proto/settings/v1/settings_service.proto +++ b/definitions/flipcash/protos/src/main/proto/settings/v1/settings_service.proto @@ -1,20 +1,28 @@ syntax = "proto3"; + package flipcash.settings.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/settings/v1;settingspb"; option java_package = "com.codeinc.flipcash.gen.settings.v1"; option objc_class_prefix = "FPBSettingsV1"; + import "common/v1/common.proto"; +import "validate/validate.proto"; service Settings { rpc UpdateSettings(UpdateSettingsRequest) returns (UpdateSettingsResponse); } + message UpdateSettingsRequest { // Locale setting, only updated if present common.v1.Locale locale = 1; + // Region setting, only updated if present common.v1.Region region = 2; - common.v1.Auth auth = 10; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; } + message UpdateSettingsResponse { Result result = 1; enum Result { diff --git a/definitions/flipcash/protos/src/main/proto/thirdparty/v1/model.proto b/definitions/flipcash/protos/src/main/proto/thirdparty/v1/model.proto index 56bb43b32..5eab691fb 100644 --- a/definitions/flipcash/protos/src/main/proto/thirdparty/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/thirdparty/v1/model.proto @@ -1,17 +1,30 @@ syntax = "proto3"; + package flipcash.thirdparty.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/thirdparty/v1;thirdpartypb"; option java_package = "com.codeinc.flipcash.gen.thirdparty.v1"; option objc_class_prefix = "FPBThirdPartyV1"; +import "validate/validate.proto"; + enum Provider { UNKNOWN = 0; COINBASE = 1; } + message ApiKey { - Provider provider = 1; - string value = 2 ; + Provider provider = 1 [(validate.rules).enum = {in: [1]}]; + + string value = 2 [(validate.rules).string = { + min_len: 36 + max_len: 36 + }]; } + message Jwt { - string value = 1 ; -} + string value = 1 [(validate.rules).string = { + min_len: 1 + max_len: 1024 // Arbitrary + }]; +} \ No newline at end of file diff --git a/definitions/flipcash/protos/src/main/proto/thirdparty/v1/third_party_service.proto b/definitions/flipcash/protos/src/main/proto/thirdparty/v1/third_party_service.proto index df6f77a36..ee3144263 100644 --- a/definitions/flipcash/protos/src/main/proto/thirdparty/v1/third_party_service.proto +++ b/definitions/flipcash/protos/src/main/proto/thirdparty/v1/third_party_service.proto @@ -1,23 +1,41 @@ syntax = "proto3"; + package flipcash.thirdparty.v1; + option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/thirdparty/v1;thirdpartypb"; option java_package = "com.codeinc.flipcash.gen.thirdparty.v1"; option objc_class_prefix = "FPBThirdPartyV1"; + import "common/v1/common.proto"; import "thirdparty/v1/model.proto"; +import "validate/validate.proto"; service ThirdParty { // GetJwt gets a JWT for auth against a third part rpc GetJwt(GetJwtRequest) returns (GetJwtResponse); } + message GetJwtRequest { - ApiKey api_key = 1; + ApiKey api_key = 1 [(validate.rules).message.required = true]; - string method = 2 ; - string host = 3 ; - string path = 4 ; - common.v1.Auth auth = 5; + string method = 2 [(validate.rules).string = { + min_len: 3 + max_len: 4 + }]; + + string host = 3 [(validate.rules).string = { + min_len: 1 + max_len: 1024 + }]; + + string path = 4 [(validate.rules).string = { + min_len: 1 + max_len: 1024 + }]; + + common.v1.Auth auth = 5 [(validate.rules).message.required = true]; } + message GetJwtResponse { Result result = 1; enum Result { @@ -28,5 +46,6 @@ message GetJwtResponse { PHONE_VERIFICATION_REQUIRED = 4; EMAIL_VERIFICATION_REQUIRED = 5; } + Jwt jwt = 2; } diff --git a/definitions/opencode/models/build.gradle.kts b/definitions/opencode/models/build.gradle.kts index 0f05a80a1..10f9aea90 100644 --- a/definitions/opencode/models/build.gradle.kts +++ b/definitions/opencode/models/build.gradle.kts @@ -1,8 +1,10 @@ +import dev.bmcreations.protovalidate.gradle.ProtoVariant import org.apache.tools.ant.taskdefs.condition.Os plugins { alias(libs.plugins.flipcash.android.library) - id("com.google.protobuf") + alias(libs.plugins.protobuf) + alias(libs.plugins.protobuf.validate) } val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" @@ -64,3 +66,7 @@ protobuf { } } } + +protovalidate { + variant.set(ProtoVariant.PGV) +} \ No newline at end of file diff --git a/definitions/opencode/protos/src/main/proto/account/v1/account_service.proto b/definitions/opencode/protos/src/main/proto/account/v1/account_service.proto index febb515e9..4586e24a4 100644 --- a/definitions/opencode/protos/src/main/proto/account/v1/account_service.proto +++ b/definitions/opencode/protos/src/main/proto/account/v1/account_service.proto @@ -10,7 +10,7 @@ import "common/v1/model.proto"; import "currency/v1/currency_service.proto"; import "transaction/v1/transaction_service.proto"; import "google/protobuf/timestamp.proto"; - +import "validate/validate.proto"; service Account { // IsOcpAccount returns whether an owner account is a OCP account. This hints @@ -25,13 +25,15 @@ service Account { message IsOcpAccountRequest { // The owner account to check against. - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + // The signature is of serialize(IsOcpAccountRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + } @@ -50,13 +52,15 @@ message IsOcpAccountResponse { message GetTokenAccountInfosRequest { // The owner account to fetch balances for, which can also be thought of as a // parent account for this RPC that links to one or more token accounts. - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + // The signature is of serialize(GetTokenAccountInfosRequest) without signature // fields set using the private key of the owner account. This provides // an authentication mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + // A requesting owner account that is requesting the balance for owner. Additional @@ -93,7 +97,8 @@ message GetTokenAccountInfosResponse { message TokenAccountInfo { // The token account's address - common.v1.SolanaAccountId address = 1; + common.v1.SolanaAccountId address = 1 [(validate.rules).message.required = true]; + // The owner of the token account, which can also be thought of as a parent @@ -107,7 +112,8 @@ message TokenAccountInfo { common.v1.SolanaAccountId authority = 3; // The type of token account, which infers its intended use. - common.v1.AccountType account_type = 4; + common.v1.AccountType account_type = 4 [(validate.rules).enum.not_in = 0]; + // The account's derivation index for applicable account types. When this field diff --git a/definitions/opencode/protos/src/main/proto/common/v1/model.proto b/definitions/opencode/protos/src/main/proto/common/v1/model.proto index 295da3272..301e5ae4a 100644 --- a/definitions/opencode/protos/src/main/proto/common/v1/model.proto +++ b/definitions/opencode/protos/src/main/proto/common/v1/model.proto @@ -1,10 +1,14 @@ syntax = "proto3"; + package ocp.common.v1; + option go_package = "github.com/code-payments/ocp-protobuf-api/generated/go/common/v1;common"; option java_package = "com.codeinc.opencode.gen.common.v1"; option objc_class_prefix = "CPBCommonV1"; + import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; // AccountType associates a type to an account, which infers how an account is used // within the OCP ecosystem. @@ -16,45 +20,103 @@ enum AccountType { ASSOCIATED_TOKEN_ACCOUNT = 4; POOL = 5; } + // SolanaAccountId is a raw binary Ed25519 public key for a Solana account message SolanaAccountId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // A Solana address lookup table used in versioned transactions message SolanaAddressLookupTable { - common.v1.SolanaAccountId address = 1; - repeated common.v1.SolanaAccountId entries = 2 ; + common.v1.SolanaAccountId address = 1 [(validate.rules).message.required = true]; + + + + repeated common.v1.SolanaAccountId entries = 2 [(validate.rules).repeated = { + min_items: 1, + max_items: 256, + }]; + + } + // Transaction is a raw binary Solana transaction message Transaction { // Maximum size taken from: https://github.com/solana-labs/solana/blob/39b3ac6a8d29e14faa1de73d8b46d390ad41797b/sdk/src/packet.rs#L9-L13 - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 1 + max_len: 1232 + }]; + + } + // Blockhash is a raw binary Solana blockchash message Blockhash { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // Signature is a raw binary Ed25519 signature message Signature { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 64 + max_len: 64 + }]; + + } + // IntentId is a client-side generated ID that maps to an intent to perform actions // on the blockchain fulfilled by the OCP sequencer. message IntentId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // SwapId is a client-side generated ID that maps to a swap. message SwapId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // Hash is a raw binary 32 byte hash value message Hash { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // UUID is a 16 byte UUID value message UUID { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 16 + max_len: 16 + }]; + + } + // Request is a generic wrapper for gRPC requests message Request { string version = 1; @@ -62,28 +124,41 @@ message Request { string method = 3; bytes body = 4; } + // Response is a generic wrapper for gRPC responses message Response { Result result = 1; + bytes body = 2; string message = 3; + enum Result { OK = 0; ERROR = 1; } } + message ServerPing { // Timestamp the ping was sent on the stream, for client to get a sense // of potential network latency - google.protobuf.Timestamp timestamp = 1; + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; + + + // The delay server will apply before sending the next ping - google.protobuf.Duration ping_delay = 2; + google.protobuf.Duration ping_delay = 2 [(validate.rules).duration.required = true]; + + } + message ClientPong { // Timestamp the Pong was sent on the stream, for server to get a sense // of potential network latency - google.protobuf.Timestamp timestamp = 1; + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; + + } + enum Interval { RAW = 0; SECOND = 1; diff --git a/definitions/opencode/protos/src/main/proto/currency/v1/currency_service.proto b/definitions/opencode/protos/src/main/proto/currency/v1/currency_service.proto index 74da68a0d..84f5b2153 100644 --- a/definitions/opencode/protos/src/main/proto/currency/v1/currency_service.proto +++ b/definitions/opencode/protos/src/main/proto/currency/v1/currency_service.proto @@ -1,257 +1,514 @@ syntax = "proto3"; + package ocp.currency.v1; + option go_package = "github.com/code-payments/ocp-protobuf-api/generated/go/currency/v1;currency"; option java_package = "com.codeinc.opencode.gen.currency.v1"; option objc_class_prefix = "CPBCurrencyV1"; -import "common/v1/model.proto"; +import "common/v1/model.proto"; +import "validate/validate.proto"; import "google/protobuf/timestamp.proto"; + service Currency { // GetMints gets mint account metadata by address rpc GetMints(GetMintsRequest) returns (GetMintsResponse); + // GetHistoricalMintData returns historical market data for a mint rpc GetHistoricalMintData(GetHistoricalMintDataRequest) returns (GetHistoricalMintDataResponse); + // StreamLiveMintData streams live mint data for a set of mints rpc StreamLiveMintData(stream StreamLiveMintDataRequest) returns (stream StreamLiveMintDataResponse); + // Launch launches a new currency on the launchpad rpc Launch(LaunchRequest) returns (LaunchResponse); + // UpdateIcon uploads and updates the icon for a currency rpc UpdateIcon(UpdateIconRequest) returns (UpdateIconResponse); + // UpdateMetadata updates mutable metadata for a currency rpc UpdateMetadata(UpdateMetadataRequest) returns (UpdateMetadataResponse); + // Discover returns a set of currencies to discover rpc Discover(DiscoverRequest) returns (stream DiscoverResponse); + // CheckAvailability checks whether a currency name is available for launch rpc CheckAvailability(CheckAvailabilityRequest) returns (CheckAvailabilityResponse); } + message GetMintsRequest { - repeated common.v1.SolanaAccountId addresses = 1 ; + repeated common.v1.SolanaAccountId addresses = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; + + } + message GetMintsResponse { Result result = 1; enum Result { OK = 0; NOT_FOUND = 1; } + map metadata_by_address = 2; } + message GetHistoricalMintDataRequest { // The mint address to get historical data for - common.v1.SolanaAccountId address = 1; + common.v1.SolanaAccountId address = 1 [(validate.rules).message.required = true]; + + + // The currency code for the returned market data (e.g., "usd") - string currency_code = 2 ; + string currency_code = 2 [(validate.rules).string = { + pattern: "^[a-z]{3,4}$" + }]; + + + oneof range { + option (validate.required) = true; + PredefinedRange predefined_range = 3; } } + message GetHistoricalMintDataResponse { Result result = 1; enum Result { OK = 0; + // The requested mint or currency was not found NOT_FOUND = 1; + // No data available for the requested time range MISSING_DATA = 2; } + repeated HistoricalMintData data = 2; } + message StreamLiveMintDataRequest { oneof type { + option (validate.required) = true; + Request request = 1; common.v1.ClientPong pong = 2; } + message Request { // The set of mints to receive live data against. To update the set of mints, // close the current stream and open a new one with the new set. - repeated common.v1.SolanaAccountId mints = 1 ; + repeated common.v1.SolanaAccountId mints = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; + + } } + message StreamLiveMintDataResponse { oneof type { + option (validate.required) = true; + LiveData data = 1; common.v1.ServerPing ping = 2; } + message LiveData { oneof type { + option (validate.required) = true; + VerifiedCoreMintFiatExchangeRateBatch core_mint_fiat_exchange_rates = 1; VerifiedLaunchapdCurrencyReserveStateBatch launchpad_currency_reserve_states = 2; } } } + message Mint { // Token mint address - common.v1.SolanaAccountId address = 1; + common.v1.SolanaAccountId address = 1 [(validate.rules).message.required = true]; + + + // The number of decimals configured for the mint uint32 decimals = 2; + // Currency name - string name = 3 ; + string name = 3 [(validate.rules).string = { + min_len: 1, + max_len: 32, + }]; + + + // Currency ticker symbol - string symbol = 4 ; + string symbol = 4 [(validate.rules).string = { + min_len: 1, + max_len: 8, + }]; + + + // Currency description - string description = 5 ; + string description = 5 [(validate.rules).string = { + min_len: 1, + max_len: 4096, + }]; + + + // URL to currency image - string image_url = 6 ; + string image_url = 6 [(validate.rules).string = { + min_len: 1, + max_len: 1024, + }]; + + + // Available when a VM exists for the given mint, and can be used for deriving // VM deposit PDAs // // Note: Only currencies with a VM are useable for payments VmMetadata vm_metadata = 7; + // Available when created by the launchpad via the currency creator program, and // can be used for calculating price, market cap, etc. based on the exponential // bonding curve LaunchpadMetadata launchpad_metadata = 8; + // Timestamp the currency was created - google.protobuf.Timestamp created_at = 9; + google.protobuf.Timestamp created_at = 9 [(validate.rules).timestamp.required = true]; + + + // Social links for this currency - repeated SocialLink social_links = 10 ; + repeated SocialLink social_links = 10 [(validate.rules).repeated = { + min_items: 0 + max_items: 32 // Arbitrary + }]; + + + // Bill customization for this currency. Use the default if not provided BillCustomization bill_customization = 11; + // Holder metrics. This is surfaced where needed (e.g. only in the Discover RPC) HolderMetrics holder_metrics = 12; } + message VmMetadata { // VM address - common.v1.SolanaAccountId vm = 1; + common.v1.SolanaAccountId vm = 1 [(validate.rules).message.required = true]; + + + // Authority that subsidizes and authorizes all transactions against the VM - common.v1.SolanaAccountId authority = 2; + common.v1.SolanaAccountId authority = 2 [(validate.rules).message.required = true]; + + + // Lock duration of Virtual Timelock Accounts on the VM, currently hardcoded // to 21 days - uint32 lock_duration_in_days = 3; + uint32 lock_duration_in_days = 3 [(validate.rules).uint32.const = 21]; + + + // VM omnibus address - common.v1.SolanaAccountId omnibus = 4; + common.v1.SolanaAccountId omnibus = 4 [(validate.rules).message.required = true]; + + } + message LaunchpadMetadata { // The address of the currency config - common.v1.SolanaAccountId currency_config = 1; + common.v1.SolanaAccountId currency_config = 1 [(validate.rules).message.required = true]; + + + // The address of the liquidity pool - common.v1.SolanaAccountId liquidity_pool = 2; + common.v1.SolanaAccountId liquidity_pool = 2 [(validate.rules).message.required = true]; + + + // The random seed used during currency creation - common.v1.SolanaAccountId seed = 3; + common.v1.SolanaAccountId seed = 3 [(validate.rules).message.required = true]; + + + // The address of the authority for the currency - common.v1.SolanaAccountId authority = 4; + common.v1.SolanaAccountId authority = 4 [(validate.rules).message.required = true]; + + + // The address where this mint's tokens are locked against the liquidity pool - common.v1.SolanaAccountId mint_vault = 5; + common.v1.SolanaAccountId mint_vault = 5 [(validate.rules).message.required = true]; + + + // The address where core mint tokens are locked against the liquidity pool - common.v1.SolanaAccountId core_mint_vault = 6; + common.v1.SolanaAccountId core_mint_vault = 6 [(validate.rules).message.required = true]; + + + // Current circulating mint token supply in quarks uint64 supply_from_bonding = 7; + // Precent fee for sells in basis points, currently hardcoded to 1% - uint32 sell_fee_bps = 8; + uint32 sell_fee_bps = 8 [(validate.rules).uint32.const = 100]; + + + // The current price in USD double price = 9; + // The current market capitalization in USD double market_cap = 10; } + message HistoricalMintData { // Timestamp for this data point - google.protobuf.Timestamp timestamp = 1; + google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true]; + + + // Market capitalization at this point in time double market_cap = 2; } + message CoreMintFiatExchangeRate { // The currency code for the fiat exchange rate - string currency_code = 1 ; + string currency_code = 1 [(validate.rules).string = { + pattern: "^[a-z]{3,4}$" + }]; + + + // The exchange rate against the core mint double exchange_rate = 2; + // Timestamp for this data point - google.protobuf.Timestamp timestamp = 3; + google.protobuf.Timestamp timestamp = 3 [(validate.rules).timestamp.required = true]; + + } + // CoreMintFiatExchangeRate with a server signature for proof for use in a payment message VerifiedCoreMintFiatExchangeRate { - CoreMintFiatExchangeRate exchange_rate = 1; - common.v1.Signature signature = 2; + CoreMintFiatExchangeRate exchange_rate = 1 [(validate.rules).message.required = true]; + + + + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + } + message VerifiedCoreMintFiatExchangeRateBatch { - repeated VerifiedCoreMintFiatExchangeRate exchange_rates = 2 ; + repeated VerifiedCoreMintFiatExchangeRate exchange_rates = 2 [(validate.rules).repeated = { + min_items: 1 + max_items: 256 // Arbitrary + }]; + + } + message LaunchpadCurrencyReserveState { // Launchpad currency mint address - common.v1.SolanaAccountId mint = 1; + common.v1.SolanaAccountId mint = 1 [(validate.rules).message.required = true]; + + + // Current circulating mint token supply in quarks uint64 supply_from_bonding = 2; + // Timestamp for this data point - google.protobuf.Timestamp timestamp = 3; + google.protobuf.Timestamp timestamp = 3 [(validate.rules).timestamp.required = true]; + + } + // LaunchpadCurrencyReserveState with a server signature for proof for use in a payment message VerifiedLaunchpadCurrencyReserveState { - LaunchpadCurrencyReserveState reserve_state = 1; - common.v1.Signature signature = 2; + LaunchpadCurrencyReserveState reserve_state = 1 [(validate.rules).message.required = true]; + +; + + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + +; } + message VerifiedLaunchapdCurrencyReserveStateBatch { - repeated VerifiedLaunchpadCurrencyReserveState reserve_states = 2 ; + repeated VerifiedLaunchpadCurrencyReserveState reserve_states = 2 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; + + } + message SocialLink { oneof type { + option (validate.required) = true; + Website website = 1; X x = 2; Telegram telegram = 3; Discord discord = 4; } + message Website { - string url = 1 ; + string url = 1 [(validate.rules).string = { + uri: true, + max_len: 2048, + }]; + + } + message X { - string username = 1 ; + string username = 1 [(validate.rules).string = { + min_len: 1, + max_len: 15, + pattern: "^[a-zA-Z0-9_]+$", + }]; + + } + message Telegram { // Telegram username (without the @ prefix) - string username = 1 ; + string username = 1 [(validate.rules).string = { + min_len: 1, + max_len: 32, + pattern: "^[a-zA-Z0-9_]+$", + }]; + + } + message Discord { // Discord invite code (e.g. "abc123" from discord.gg/abc123) - string invite_code = 1 ; + string invite_code = 1 [(validate.rules).string = { + min_len: 1, + max_len: 32, + pattern: "^[a-zA-Z0-9]+$", + }]; + + } } + message BillCustomization { // Bill background colors (from top to bottom) - repeated Color colors = 1 ; + repeated Color colors = 1 [(validate.rules).repeated = { + min_items: 2 + max_items: 3 + }]; + + } + message Color { // Hex colour value (e.g. "#19191A") - string hex = 1 ; + string hex = 1 [(validate.rules).string = { + pattern: "^#[0-9a-fA-F]{6}$" + }]; + + } + message HolderMetrics { // The current number of holders for a currency uint64 current_holders = 1; - repeated DeltaHolders holder_deltas = 2 ; + + repeated DeltaHolders holder_deltas = 2 [(validate.rules).repeated = { + min_items: 0 + max_items: 4 + }]; + + + message DeltaHolders { // Predefined range where delta is calculated from PredefinedRange range = 1; + // Net holders within the time range int64 delta = 2; } } + message LaunchRequest { // The owner account launching the currency - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(LaunchRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + + // The name of the currency to launch. Must be printable ASCII with no // leading or trailing spaces. - string name = 3; + string name = 3 [(validate.rules).string = { + min_len: 1, + max_len: 32, + // [!-~] = printable ASCII excluding space; [ -~] = printable ASCII including space + pattern: "^[!-~]([ -~]*[!-~])?$", + }]; + // The ticker symbol for the currency. Must be printable ASCII with no // spaces. If not provided, a default will be generated using the currency // name. - string symbol = 4 ; + string symbol = 4 [(validate.rules).string = { + max_len: 8, + // [!-~] = printable ASCII excluding space + pattern: "^[!-~]*$", + }]; + + + // Optional description - string description = 5 ; + string description = 5 [(validate.rules).string = { + max_len: 4096, + }]; + + + // Optional bill customization. If not provided, a default will be set. BillCustomization bill_customization = 6; + // The raw image data for the icon. If not provided, a default will be set. - bytes icon = 7 ; + bytes icon = 7 [(validate.rules).bytes = { + max_len: 1048576, // 1 MB + }]; + + + // Attestation that the name passed moderation - ModerationAttestation name_moderation_attestation = 8; + ModerationAttestation name_moderation_attestation = 8 [(validate.rules).message.required = true]; + + + // Attestation that the symbol, if provided, passed moderation ModerationAttestation symbol_moderation_attestation = 9; + // Attestation that the descritpion, if provided, passed moderation ModerationAttestation description_moderation_attestation = 10; + // Attestation that the icon image, if provided, passed moderation ModerationAttestation icon_moderation_attestation = 11; } + message LaunchResponse { Result result = 1; enum Result { @@ -263,23 +520,43 @@ message LaunchResponse { // Provided icon is invalid INVALID_ICON = 3; } + // The mint address of the launched currency on success common.v1.SolanaAccountId mint = 2; } + message UpdateIconRequest { // The owner account of the currency - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(UpdateIconRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + + // The mint address of the currency to update - common.v1.SolanaAccountId mint = 3; + common.v1.SolanaAccountId mint = 3 [(validate.rules).message.required = true]; + + + // The raw image data for the icon - bytes icon = 4 ; + bytes icon = 4 [(validate.rules).bytes = { + min_len: 1, + max_len: 1048576, // 1 MB + }]; + + + // Attestation that the icon image passed moderation - ModerationAttestation moderation_attestation = 5; + ModerationAttestation moderation_attestation = 5 [(validate.rules).message.required = true]; + + } + message UpdateIconResponse { Result result = 1; enum Result { @@ -289,34 +566,65 @@ message UpdateIconResponse { INVALID_ICON = 3; } } + message UpdateMetadataRequest { // The owner account of the currency - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(UpdateMetadataRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + + // The mint address of the currency to update - common.v1.SolanaAccountId mint = 3; + common.v1.SolanaAccountId mint = 3 [(validate.rules).message.required = true]; + + + // Updated currency description. If not provided, description is not updated. DescriptionUpdate new_description = 4; + // Updated bill customization. If not provided, bill customization is not updated. BillCustomizationUpdate new_bill_customization = 5; + // Updated social links. This replaces the entire set of social links. If not // provided, social links are not updated. SocialLinksUpdate new_social_links = 6; + message DescriptionUpdate { - string value = 1 ; + string value = 1 [(validate.rules).string = { + min_len: 1, + max_len: 4096, + }]; + + + // Attestation that the description passed moderation - ModerationAttestation moderation_attestation = 2; + ModerationAttestation moderation_attestation = 2 [(validate.rules).message.required = true]; + + } + message BillCustomizationUpdate { - BillCustomization value = 1; + BillCustomization value = 1 [(validate.rules).message.required = true]; + + } + message SocialLinksUpdate { - repeated SocialLink value = 1 ; + repeated SocialLink value = 1 [(validate.rules).repeated = { + min_items: 0 + max_items: 32 // Arbitrary + }]; + + } } + message UpdateMetadataResponse { Result result = 1; enum Result { @@ -325,6 +633,7 @@ message UpdateMetadataResponse { DENIED = 2; } } + message DiscoverRequest { Category category = 1; enum Category { @@ -332,26 +641,44 @@ message DiscoverRequest { NEW = 1; } } + message DiscoverResponse { Result result = 1; enum Result { OK = 0; NOT_FOUND = 1; } - repeated Mint mints = 2 ; + + repeated Mint mints = 2 [(validate.rules).repeated = { + min_items: 0 + max_items: 1024 // Arbitrary + }]; + + } + message CheckAvailabilityRequest { // The currency name to check availability for - string name = 1; + string name = 1 [(validate.rules).string = { + min_len: 1, + max_len: 32, + // [!-~] = printable ASCII excluding space; [ -~] = printable ASCII including space + pattern: "^[!-~]([ -~]*[!-~])?$", + }]; + + } + message CheckAvailabilityResponse { Result result = 1; enum Result { OK = 0; } + // Whether the name is available for use bool is_available = 2; } + enum PredefinedRange { ALL_TIME = 0; LAST_DAY = 1; @@ -359,6 +686,12 @@ enum PredefinedRange { LAST_MONTH = 3; LAST_YEAR = 4; } + message ModerationAttestation { - bytes raw_value = 1 ; + bytes raw_value = 1 [(validate.rules).bytes = { + min_len: 1, + max_len: 4096, + }]; + + } diff --git a/definitions/opencode/protos/src/main/proto/messaging/v1/messaging_service.proto b/definitions/opencode/protos/src/main/proto/messaging/v1/messaging_service.proto index b0db406a0..5d0158445 100644 --- a/definitions/opencode/protos/src/main/proto/messaging/v1/messaging_service.proto +++ b/definitions/opencode/protos/src/main/proto/messaging/v1/messaging_service.proto @@ -1,11 +1,15 @@ syntax = "proto3"; + package ocp.messaging.v1; + option go_package = "github.com/code-payments/ocp-protobuf-api/generated/go/messaging/v1;messaging"; option java_package = "com.codeinc.opencode.gen.messaging.v1"; option objc_class_prefix = "CPBMessagingV1"; + import "common/v1/model.proto"; import "currency/v1/currency_service.proto"; import "transaction/v1/transaction_service.proto"; +import "validate/validate.proto"; service Messaging { // OpenMessageStream opens a stream of messages. Messages are routed using the @@ -28,6 +32,7 @@ service Messaging { // 7. The payment sender receives the RequestToGrabBill message in real time, submits the intent // for the payment to the provided destination, and then closes the stream. rpc OpenMessageStream(OpenMessageStreamRequest) returns (stream OpenMessageStreamResponse); + // OpenMessageStreamWithKeepAlive is like OpenMessageStream, but enables a ping/pong // keepalive to determine the health of the stream at both the client and server. // @@ -54,6 +59,7 @@ service Messaging { // Note: This API will enforce OpenMessageStreamRequest.signature is set as part of migration // to this newer protocol rpc OpenMessageStreamWithKeepAlive(stream OpenMessageStreamWithKeepAliveRequest) returns (stream OpenMessageStreamWithKeepAliveResponse); + // PollMessages is like OpenMessageStream, but uses a polling flow for receiving // messages. Updates are not real-time and depedent on the polling interval. // This RPC supports all message types. @@ -61,103 +67,179 @@ service Messaging { // This is a temporary RPC until OpenMessageStream can be built out generically on // both client and server, while supporting things like multiple listeners. rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse); + // AckMessages acks one or more messages that have been successfully delivered to // the client. rpc AckMessages(AckMessagesRequest) returns (AckMesssagesResponse); + // SendMessage sends a message. rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); } + message OpenMessageStreamRequest { - RendezvousKey rendezvous_key = 1; + RendezvousKey rendezvous_key = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(OpenMessageStreamRequest) using rendezvous_key. // // todo: Make required once clients migrate - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = false]; + + } + message OpenMessageStreamResponse { - repeated Message messages = 1 ; + repeated Message messages = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 + }]; + + } + message OpenMessageStreamWithKeepAliveRequest { oneof request_or_pong { + option (validate.required) = true; + OpenMessageStreamRequest request = 1; common.v1.ClientPong pong = 2; } } + message OpenMessageStreamWithKeepAliveResponse { oneof response_or_ping { + option (validate.required) = true; + OpenMessageStreamResponse response = 1; common.v1.ServerPing ping = 2; } } + message PollMessagesRequest { - RendezvousKey rendezvous_key = 1; + RendezvousKey rendezvous_key = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(PollMessagesRequest) using rendezvous_key. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + } + message PollMessagesResponse { - repeated Message messages = 1 ; + repeated Message messages = 1 [(validate.rules).repeated = { + min_items: 0 + max_items: 1024 + }]; + + } + message AckMessagesRequest { - RendezvousKey rendezvous_key = 1; - repeated MessageId message_ids = 2 ; + RendezvousKey rendezvous_key = 1 [(validate.rules).message.required = true]; + + + + repeated MessageId message_ids = 2 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 + }]; + + } + message AckMesssagesResponse { Result result = 1; enum Result { OK = 0; } } + message SendMessageRequest { // The message to send. Types of messages clients can send are restricted. - Message message = 1; + Message message = 1 [(validate.rules).message.required = true]; + + + // The rendezvous key that the message should be routed to. - RendezvousKey rendezvous_key = 2; + RendezvousKey rendezvous_key = 2 [(validate.rules).message.required = true]; + + + // The signature is of serialize(Message) using the PrivateKey of the keypair. - common.v1.Signature signature = 3; + common.v1.Signature signature = 3 [(validate.rules).message.required = true]; + + } + message SendMessageResponse { Result result = 1; enum Result { OK = 0; NO_ACTIVE_STREAM = 1; } + // Set if result == OK. MessageId message_id = 2; } + // RendezvousKey is a unique key pair, typically derived from a scan code payload, // which is used to establish a secure communication channel anonymously to coordinate // a flow using messages. message RendezvousKey { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 32 + max_len: 32 + }]; + + } + // MessageId identifies a message. It is only guaranteed to be unique when // paired with a destination (i.e. the rendezvous public key). message MessageId { - bytes value = 1 ; + bytes value = 1 [(validate.rules).bytes = { + min_len: 16 + max_len: 16 + }]; + + } + // Request that a pulled out bill be sent to the requested address. // // This message type is only initiated by clients. message RequestToGrabBill { // Requestor is the virtual token account on the VM to which a payment // should be sent. - common.v1.SolanaAccountId requestor_account = 1; + common.v1.SolanaAccountId requestor_account = 1 [(validate.rules).message.required = true]; + + } + message RequestToGiveBillServerContext { // Mint metadata for the bill's mint - currency.v1.Mint mint_metadata = 1; + currency.v1.Mint mint_metadata = 1 [(validate.rules).message.required = true]; + + } + // Request that a bill be given in the desired mint // // This message type is only initiated by clients. message RequestToGiveBill { // The mint that the bill will be received in - common.v1.SolanaAccountId mint = 1; + common.v1.SolanaAccountId mint = 1 [(validate.rules).message.required = true]; + + + // The validated exchange data that was used to compute the fiat value of the give // to support subsequent gives. Clients should be aware of timeouts and dismiss a // bill if the threshold is met. transaction.v1.VerifiedExchangeData exchange_data = 2; } + message Message { // MessageId is the Id of the message. This ID is generated by the // server, and will _always_ be set when receiving a message. @@ -165,23 +247,36 @@ message Message { // Server generates the message to: // 1. Reserve the ability for any future ID changes // 2. Prevent clients attempting to collide message IDs. - MessageId id = 1; + MessageId id = 1 [(validate.rules).message.required = false]; + + + // The signature sent from SendMessageRequest, which will be injected by server. // This enables clients to ensure no MITM attacks were performed to hijack contents // of the typed message. This is only applicable for messages not generated by server. - common.v1.Signature send_message_request_signature = 2; + common.v1.Signature send_message_request_signature = 2 [(validate.rules).message.required = false]; + + + oneof kind { + option (validate.required) = true; + // // Section: Cash // + RequestToGrabBill request_to_grab_bill = 3; RequestToGiveBill request_to_give_bill = 4; } + // Additional server-provided context for messages sent by client AdditionalServerContext additional_context = 5; } + message AdditionalServerContext { oneof type { + option (validate.required) = true; + RequestToGiveBillServerContext request_to_give_bill = 1; } } diff --git a/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto b/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto index dc34565ce..c750093e0 100644 --- a/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto +++ b/definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto @@ -1,11 +1,15 @@ syntax = "proto3"; + package ocp.transaction.v1; + option go_package = "github.com/code-payments/ocp-protobuf-api/generated/go/transaction/v1;transaction"; option java_package = "com.codeinc.opencode.gen.transaction.v1"; option objc_class_prefix = "APBTransactionV1"; + import "common/v1/model.proto"; import "currency/v1/currency_service.proto"; import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; service Transaction { // SubmitIntent is the mechanism for client and server to agree upon a set of @@ -41,16 +45,20 @@ service Transaction { // * Server will return SubmitIntentResponse.Error and close the stream // * Client will close the stream rpc SubmitIntent(stream SubmitIntentRequest) returns (stream SubmitIntentResponse); + // GetIntentMetadata gets basic metadata on an intent. It can also be used // to fetch the status of submitted intents. Metadata exists only for intents // that have been successfully submitted. rpc GetIntentMetadata(GetIntentMetadataRequest) returns (GetIntentMetadataResponse); + // GetLimits gets limits for money moving intents for an owner account in an // identity-aware manner rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse); + // CanWithdrawToAccount provides hints to clients for submitting withdraw intents. // The RPC indicates if a withdrawal is possible, and how it should be performed. rpc CanWithdrawToAccount(CanWithdrawToAccountRequest) returns (CanWithdrawToAccountResponse); + // VoidGiftCard voids a gift card account by returning the funds to the funds back // to the issuer via the auto-return action if it hasn't been claimed or already // returned. @@ -58,6 +66,7 @@ service Transaction { // Note: The RPC is idempotent. If the user already claimed/voided the gift card, or // it is close to or is auto-returned, then OK will be returned. rpc VoidGiftCard(VoidGiftCardRequest) returns (VoidGiftCardResponse); + // StatefulSwap swaps tokens using a non-custodial state-management system. // The high-level flow mirrors SubmitIntent closely. However, due to the // unreliability of swaps, they do not fit within the broader intent system. @@ -71,57 +80,97 @@ service Transaction { // Swap transaction signatures are collected up-front. They are executed once the // swap is funded. rpc StatefulSwap(stream StatefulSwapRequest) returns (stream StatefulSwapResponse); + // GetSwap gets metadata for a swap rpc GetSwap(GetSwapRequest) returns (GetSwapResponse); + // GetPendingSwaps gets swaps that are pending client actions which include: // * Swaps that need a call to SubmitIntent to fund the VM swap PDA rpc GetPendingSwaps(GetPendingSwapsRequest) returns (GetPendingSwapsResponse); } + // // Request and Response Definitions // + message SubmitIntentRequest { oneof request { + option (validate.required) = true; + SubmitActions submit_actions = 1; SubmitSignatures submit_signatures = 2; } + message SubmitActions { // The globally unique client generated intent ID. Use the original intent // ID when operating on actions that mutate the intent. - common.v1.IntentId id = 1; + common.v1.IntentId id = 1 [(validate.rules).message.required = true]; + + + // The verified owner account public key - common.v1.SolanaAccountId owner = 2; + common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true]; + + + // Additional metadata that describes the high-level intention - Metadata metadata = 3; + Metadata metadata = 3 [(validate.rules).message.required = true]; + + + // The set of all ordered actions required to fulfill the intent - repeated Action actions = 4 ; + repeated Action actions = 4 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary + }]; + + + // The signature is of serialize(SubmitActions) without this field set using the // private key of the owner account. This provides an authentication mechanism // to the RPC. - common.v1.Signature signature = 5; + common.v1.Signature signature = 5 [(validate.rules).message.required = true]; + + } + message SubmitSignatures { // The set of all signatures for each transaction or virtual instruction requiring // signature from the authority accounts. // // The signature for a transaction is for the marshalled transaction. // The signature for a virtual instruction is the hash of the marshalled instruction. - repeated common.v1.Signature signatures = 1 ; + repeated common.v1.Signature signatures = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Assumes at most 1 client signatures per action + }]; + + } } + message SubmitIntentResponse { oneof response { + option (validate.required) = true; + ServerParameters server_parameters = 1; Success success = 2; Error error = 3; } + message ServerParameters { // The set of all server paremeters required to fill missing transaction // or virtual instruction details. Server guarantees to provide a message // for each client action in an order consistent with the received action // list. - repeated ServerParameter server_parameters = 1 ; + repeated ServerParameter server_parameters = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 1024 // Arbitrary, but must match SubmitActions.actions.max_items + }]; + + } + message Success { Code code = 1; enum Code { @@ -129,6 +178,7 @@ message SubmitIntentResponse { OK = 0; } } + message Error { Code code = 1; enum Code { @@ -141,20 +191,29 @@ message SubmitIntentResponse { // Server detected client has stale state. STALE_STATE = 3; } + repeated ErrorDetails error_details = 2; } } + message GetIntentMetadataRequest { // The intent ID to query - common.v1.IntentId intent_id = 1; + common.v1.IntentId intent_id = 1 [(validate.rules).message.required = true]; + + + // The verified owner account public key when not signing with the rendezvous // key. Only owner accounts involved in the intent can access the metadata. common.v1.SolanaAccountId owner = 2; + // The signature is of serialize(GetIntentStatusRequest) without this field set // using the private key of the rendezvous or owner account. This provides an // authentication mechanism to the RPC. - common.v1.Signature signature = 3; + common.v1.Signature signature = 3 [(validate.rules).message.required = true]; + + } + message GetIntentMetadataResponse { Result result = 1; enum Result { @@ -162,38 +221,60 @@ message GetIntentMetadataResponse { NOT_FOUND = 1; DENIED = 2; } + Metadata metadata = 2; } + message GetLimitsRequest { // The owner account whose limits will be calculated. Any other owner accounts // linked with the same identity of the owner will also be applied. - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(GetLimitsRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + + // All transactions starting at this time will be incorporated into the consumed // limit calculation. Clients should set this to the start of the current day in // the client's current time zone (because server has no knowledge of this atm). - google.protobuf.Timestamp consumed_since = 3; + google.protobuf.Timestamp consumed_since = 3 [(validate.rules).timestamp.required = true]; + + } + message GetLimitsResponse { Result result = 1; enum Result { OK = 0; } + // Send limits keyed by currency map send_limits_by_currency = 2; + // The amount of USD transacted since the consumption timestamp - double usd_transacted = 3; + double usd_transacted = 3 [(validate.rules).double.gte = 0]; + +; } + message CanWithdrawToAccountRequest { // The destination account attempted to be withdrawn to. Can be an owner or // token account. - common.v1.SolanaAccountId account = 1; + common.v1.SolanaAccountId account = 1 [(validate.rules).message.required = true]; + + + // The mint that the withdraw will be operating against - common.v1.SolanaAccountId mint = 2; + common.v1.SolanaAccountId mint = 2 [(validate.rules).message.required = true]; + +; } + message CanWithdrawToAccountResponse { // Server-controlled flag to indicate if the account can be withdrawn to. // There are several reasons server may deny it, including: @@ -201,6 +282,7 @@ message CanWithdrawToAccountResponse { // - Unsupported external account type (eg. token account but of the wrong mint) // This is guaranteed to be false when account_type = Unknown. bool is_valid_payment_destination = 1; + // Metadata so the client knows how to withdraw to the account. Server cannot // provide precalculated addresses in this response to maintain non-custodial // status. @@ -210,9 +292,11 @@ message CanWithdrawToAccountResponse { TokenAccount = 1; // Client uses the address as is in SubmitIntent OwnerAccount = 2; // Client locally derives the ATA to use in SubmitIntent } + // ATA requires initialization before the withdrawal can occur. Server may not // subsidize the account creation, so a fee may be required. bool requires_initialization = 3; + // The CREATE_ON_SEND_WITHDRAWAL fee, in USD, that must be paid in order to // submit a withdrawal to subsidize the creation of the account at time of // send. The user must explicitly agree to this fee amount before submitting @@ -224,16 +308,26 @@ message CanWithdrawToAccountResponse { // Note: The fee is always paid in the target mint. ExchangeDataWithoutRate fee_amount = 4; } + message VoidGiftCardRequest { // The owner account that issued the gift card account - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The vault of the gift card account to void - common.v1.SolanaAccountId gift_card_vault = 2; + common.v1.SolanaAccountId gift_card_vault = 2 [(validate.rules).message.required = true]; + + + // The signature is of serialize(VoidGiftCardRequest) without this field set using // the private key of the owner account. This provides an authentication mechanism // to the RPC. - common.v1.Signature signature = 3; + common.v1.Signature signature = 3 [(validate.rules).message.required = true]; + + } + message VoidGiftCardResponse { Result result = 1; enum Result { @@ -246,65 +340,119 @@ message VoidGiftCardResponse { NOT_FOUND = 3; } } + message StatefulSwapRequest { oneof request { + option (validate.required) = true; + Initiate initiate = 1; SubmitSignatures submit_signatures = 2; } + message Initiate { oneof kind { + option (validate.required) = true; + ReserveSwapClientParameters reserve = 1; } + // Client parameters for starting swaps against the Reserve contract message ReserveSwapClientParameters { // The unique ID for this swap randomly generated on client - common.v1.SwapId id = 1; + common.v1.SwapId id = 1 [(validate.rules).message.required = true]; + + + // The source mint that will be swapped from - common.v1.SolanaAccountId from_mint = 2; + common.v1.SolanaAccountId from_mint = 2 [(validate.rules).message.required = true]; + + + // The destination mint that will be swapped to - common.v1.SolanaAccountId to_mint = 3; + common.v1.SolanaAccountId to_mint = 3 [(validate.rules).message.required = true]; + + + // The amount to swap from the source mint in quarks. - uint64 amount = 4; + uint64 amount = 4 [(validate.rules).uint64.gt = 0]; + + + // Where "amount" of "from_mint" will be sent from to the VM swap PDA - FundingSource funding_source = 5 ; + FundingSource funding_source = 5 [(validate.rules).enum = { + in: [1, 2] // FUNDING_SOURCE_SUBMIT_INTENT, FUNDING_SOURCE_EXTERNAL_WALLET + }]; + + + // The ID of the "transaction" to lookup funding state. // // For FUNDING_SOURCE_SUBMIT_INTENT, this value is the base58 encoded intent ID. // For FUNDING_SOURCE_EXTERNAL_WALLET, this value is the base58 encoded transaction signature. - string funding_id = 6 ; + string funding_id = 6 [(validate.rules).string = { + min_len: 32, + max_len: 88, + }]; + + } + // The owner account starting the swap - common.v1.SolanaAccountId owner = 9; + common.v1.SolanaAccountId owner = 9 [(validate.rules).message.required = true]; + + + // The user authority account that will sign to authorize the swap. // // For Reserve contract buy/sell flows against existing currencies, this must be a random one-time use account. // For Reserve contract buy flows against new currencies, this must be the owner account that is the currency creator. - common.v1.SolanaAccountId swap_authority = 10; + common.v1.SolanaAccountId swap_authority = 10 [(validate.rules).message.required = true]; + + + // The signature of serialize(VerifiedSwapMetadata) for the swap being initiated. - common.v1.Signature proof_signature = 11; + common.v1.Signature proof_signature = 11 [(validate.rules).message.required = true]; + + + // The signature is of serialize(StatefulSwapRequest.Initiate) without this field // set using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 12; + common.v1.Signature signature = 12 [(validate.rules).message.required = true]; + + } + message SubmitSignatures { // The signatures for the locally constructed swap transaction: // - owner is at index 0 // - swap_authority is at index 1 - repeated common.v1.Signature transaction_signatures = 1 ; + repeated common.v1.Signature transaction_signatures = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 2 + }]; + + } } + message StatefulSwapResponse { oneof response { + option (validate.required) = true; + ServerParameters server_parameters = 1; Success success = 2; Error error = 3; } + message ServerParameters { oneof kind { + option (validate.required) = true; + ReserveExistingCurrencyServerParameters reserve_existing_currency = 1; ReserveNewCurrencyServerParameter reserve_new_currency = 2; } + // Server parameters when executing stateful buy/sell flows against the // Reserve contract against an existing currency // @@ -349,27 +497,46 @@ message StatefulSwapResponse { // 12. VM::CloseSwapAccountIfEmpty (closes from_mint VM swap ATA if empty) message ReserveExistingCurrencyServerParameters { // Subisdizer account that will be paying for the swap - common.v1.SolanaAccountId payer = 1; + common.v1.SolanaAccountId payer = 1 [(validate.rules).message.required = true]; + + + // The nonce that is reserved for use in the swap transaction - common.v1.SolanaAccountId nonce = 2; + common.v1.SolanaAccountId nonce = 2 [(validate.rules).message.required = true]; + + + // The blockhash that is reserved for use in the swap transaction - common.v1.Blockhash blockhash = 3; + common.v1.Blockhash blockhash = 3 [(validate.rules).message.required = true]; + + + // ALTs that should be used when constructing the versioned transaction repeated common.v1.SolanaAddressLookupTable alts = 4; + // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit // instruction. If the value is 0, then the instruction can be omitted. uint32 compute_unit_limit = 5; + // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice // instruction. If the value is 0, then the instruction can be omitted. uint64 compute_unit_price = 6; + // Value provided into the Memo::Memo instruction. If the value length is 0, // then the instruction can be omitted. - string memo_value = 7; + string memo_value = 7 [(validate.rules).string.max_len = 64]; + + + // The memory account where the destination virtual Timelock account lives - common.v1.SolanaAccountId memory_account = 8; + common.v1.SolanaAccountId memory_account = 8 [(validate.rules).message.required = true]; + + + // The memory index where the destination virtual Timelock account lives uint32 memory_index = 9; } + // Server parameters when executing stateful buy flows against the // Reserve contract against a new currency. Only the creator of the // currency will be able to execute this flow. @@ -394,42 +561,82 @@ message StatefulSwapResponse { // from using these server parameters. message ReserveNewCurrencyServerParameter { // Subisdizer account that will be paying for the swap - common.v1.SolanaAccountId payer = 1; + common.v1.SolanaAccountId payer = 1 [(validate.rules).message.required = true]; + + + // The nonce that is reserved for use in the swap transaction - common.v1.SolanaAccountId nonce = 2; + common.v1.SolanaAccountId nonce = 2 [(validate.rules).message.required = true]; + + + // The blockhash that is reserved for use in the swap transaction - common.v1.Blockhash blockhash = 3; + common.v1.Blockhash blockhash = 3 [(validate.rules).message.required = true]; + + + // ALTs that should be used when constructing the versioned transaction repeated common.v1.SolanaAddressLookupTable alts = 4; + // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit // instruction. If the value is 0, then the instruction can be omitted. uint32 compute_unit_limit = 5; + // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice // instruction. If the value is 0, then the instruction can be omitted. uint64 compute_unit_price = 6; + // Value provided into the Memo::Memo instruction. If the value length is 0, // then the instruction can be omitted. - string memo_value = 7; - // The VM and currency authority - common.v1.SolanaAccountId authority = 8; + string memo_value = 7 [(validate.rules).string.max_len = 64]; + + + // The VM and currency authority - string name = 9 ; + common.v1.SolanaAccountId authority = 8 [(validate.rules).message.required = true]; + + + + // The currency name + string name = 9 [(validate.rules).string = { + min_len: 1, + max_len: 32, + }]; + + + // The currency symbol - string symbol = 10 ; + string symbol = 10 [(validate.rules).string = { + min_len: 1, + max_len: 8, + }]; + + + // The random seed value used to generate a unique currency of the given name - common.v1.SolanaAccountId seed = 11; + common.v1.SolanaAccountId seed = 11 [(validate.rules).message.required = true]; + + + // Liquidity pool's percent sell fee in basis points - uint32 sell_fee_bps = 12; + uint32 sell_fee_bps = 12 [(validate.rules).uint32.const = 100]; + + + // The VM lock duration - uint32 vm_lock_duration_in_days = 13; + uint32 vm_lock_duration_in_days = 13 [(validate.rules).uint32.const = 21]; + + } } + message Success { Code code = 1; enum Code { OK = 0; } } + message Error { Code code = 1; enum Code { @@ -440,17 +647,28 @@ message StatefulSwapResponse { // The swap metadata failed server-side validation INVALID_SWAP = 2; } + repeated ErrorDetails error_details = 2; } } + message GetSwapRequest { - common.v1.SwapId id = 1; - common.v1.SolanaAccountId owner = 2; + common.v1.SwapId id = 1 [(validate.rules).message.required = true]; + + + + common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true]; + + + // The signature is of serialize(GetSwapRequest) without this field set using the // private key of the owner account. This provides an authentication mechanism // to the RPC. - common.v1.Signature signature = 3; + common.v1.Signature signature = 3 [(validate.rules).message.required = true]; + + } + message GetSwapResponse { Result result = 1; enum Result { @@ -458,35 +676,53 @@ message GetSwapResponse { NOT_FOUND = 1; DENIED = 2; } + SwapMetadata swap = 2; } + message GetPendingSwapsRequest{ - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1 [(validate.rules).message.required = true]; + + + // The signature is of serialize(GetPendingSwapsRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2 [(validate.rules).message.required = true]; + + } + message GetPendingSwapsResponse { Result result = 1; enum Result { OK = 0; NOT_FOUND = 1; } - repeated SwapMetadata swaps = 2 ; + + repeated SwapMetadata swaps = 2 [(validate.rules).repeated = { + max_items: 1024 // Arbitrary + }]; + + } + // // Metadata definitions // + // Metadata describes the high-level details of an intent message Metadata { oneof type { + option (validate.required) = true; + OpenAccountsMetadata open_accounts = 1; SendPublicPaymentMetadata send_public_payment = 2; ReceivePaymentsPubliclyMetadata receive_payments_publicly = 3; PublicDistributionMetadata public_distribution = 4; } } + // Open a set of accounts // // Action Spec (User): @@ -499,14 +735,21 @@ message Metadata { // for account in [POOL] // actions.push_back(OpenAccountAction(account)) message OpenAccountsMetadata { - AccountSet account_set = 1; + AccountSet account_set = 1 [(validate.rules).enum.defined_only = true]; + + enum AccountSet { USER = 0; // Opens a set of user accounts POOL = 1; // Opens a pool account } + + // The mint that this action will be operating against - common.v1.SolanaAccountId mint = 2; + common.v1.SolanaAccountId mint = 2 [(validate.rules).message.required = true]; + +; } + // Send a payment to a destination account publicly. // // Action Spec (Payment): @@ -531,26 +774,42 @@ message OpenAccountsMetadata { message SendPublicPaymentMetadata { // The source account where funds will be sent from. Currently, this is always // the user's primary account. - common.v1.SolanaAccountId source = 1; + common.v1.SolanaAccountId source = 1 [(validate.rules).message.required = true]; + + + // The destination token account to send funds to. - common.v1.SolanaAccountId destination = 2; + common.v1.SolanaAccountId destination = 2 [(validate.rules).message.required = true]; + + + // Destination owner account, which is required for withdrawals that intend // to create an ATA. Every other variation of this intent can omit this field. common.v1.SolanaAccountId destination_owner = 3; + // The exchange data of total funds being sent to the destination oneof exchange_data { + option (validate.required) = true; + // Provided by server for submitted intents ExchangeData server_exchange_data = 4; + // Provided by clients when submitting new intents VerifiedExchangeData client_exchange_data = 8; } + // Is the payment a withdrawal? bool is_withdrawal = 5; + // Is the payment going to a new gift card? Note is_withdrawal must be false. bool is_remote_send = 6; + // The mint that this intent will be operating against - common.v1.SolanaAccountId mint = 7; + common.v1.SolanaAccountId mint = 7 [(validate.rules).message.required = true]; + +; } + // Receive funds into a user-owned account publicly. All use cases of this intent // close the account, so all funds must be moved. // @@ -559,19 +818,33 @@ message SendPublicPaymentMetadata { // actions = [NoPrivacyWithdrawAction(REMOTE_SEND_GIFT_CARD, PRIMARY, quarks)] message ReceivePaymentsPubliclyMetadata { // The remote send gift card to receive funds from - common.v1.SolanaAccountId source = 1; + common.v1.SolanaAccountId source = 1 [(validate.rules).message.required = true]; + + + // The exact amount of quarks being received - uint64 quarks = 2; + uint64 quarks = 2 [(validate.rules).uint64.gt = 0]; + + + // Is the receipt of funds from a remote send gift card? Currently, this is // the only use case for this intent and validation enforces the flag to true. - bool is_remote_send = 3; + bool is_remote_send = 3 [(validate.rules).bool.const = true]; + + + // If is_remote_send is true, the original exchange data that was provided as // part of creating the gift card account. This is purely a server-provided value. // SubmitIntent will disallow this being set. ExchangeData exchange_data = 4; + + // The mint that this intent will be operating against - common.v1.SolanaAccountId mint = 5; + common.v1.SolanaAccountId mint = 5 [(validate.rules).message.required = true]; + +; } + // Distribute funds from a pool account publicly to one or more user-owned accounts. // // Action Spec: @@ -585,22 +858,41 @@ message ReceivePaymentsPubliclyMetadata { // - The pool is closed at the end of the intent via a NoPrivacyWithdrawAction message PublicDistributionMetadata { // The pool account to distribute from - common.v1.SolanaAccountId source = 1; + common.v1.SolanaAccountId source = 1 [(validate.rules).message.required = true]; + + + // The set of distributions - repeated Distribution distributions = 2 ; + repeated Distribution distributions = 2 [(validate.rules).repeated = { + min_items: 1, + // todo: max-items? + }]; + +; message Distribution { // Destination where a portion of the pool's funds will be distributed. // This must always be a primary account. - common.v1.SolanaAccountId destination = 1; + common.v1.SolanaAccountId destination = 1 [(validate.rules).message.required = true]; + + + // The amount of funds to distribute to the destination - uint64 quarks = 2; + uint64 quarks = 2 [(validate.rules).uint64.gt = 0]; + + } + + // The mint that this intent will be operating against - common.v1.SolanaAccountId mint = 3; + common.v1.SolanaAccountId mint = 3 [(validate.rules).message.required = true]; + +; } + // // Action Definitions // + // Action is a well-defined, ordered and small set of transactions or virtual instructions // for a unit of work that the client wants to perform on the blockchain. Clients provide // parameters known to them in the action. @@ -608,88 +900,165 @@ message Action { // The ID of this action, which is unique within an intent. It must match // the index of the action's location in the SubmitAction's actions field. uint32 id = 1; + // The type of action to perform. oneof type { + option (validate.required) = true; + OpenAccountAction open_account = 2; NoPrivacyTransferAction no_privacy_transfer = 3; NoPrivacyWithdrawAction no_privacy_withdraw = 4; FeePaymentAction fee_payment = 5; } } + // No client signature required message OpenAccountAction { // The type of account, which will dictate its intended use - common.v1.AccountType account_type = 1; + common.v1.AccountType account_type = 1 [(validate.rules).enum.not_in = 0]; + + + // The owner of the account. For accounts liked to a user's 12 words, this is // the verified parent owner account public key. All other account types should // set this to the authority value. - common.v1.SolanaAccountId owner = 2; + common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true]; + + + // The index used to for accounts that are derived from owner uint64 index = 3; + // The public key of the private key that has authority over the opened token account - common.v1.SolanaAccountId authority = 4; + common.v1.SolanaAccountId authority = 4 [(validate.rules).message.required = true]; + + + // The token account being opened - common.v1.SolanaAccountId token = 5; + common.v1.SolanaAccountId token = 5 [(validate.rules).message.required = true]; + + + // The signature is of serialize(OpenAccountAction) without this field set // using the private key of the authority account. This provides a proof // of authorization to link authority to owner. - common.v1.Signature authority_signature = 6; + common.v1.Signature authority_signature = 6 [(validate.rules).message.required = true]; + + + + // The mint that this action will be operating against - common.v1.SolanaAccountId mint = 7; + common.v1.SolanaAccountId mint = 7 [(validate.rules).message.required = true]; + +; } + // Compact message signature required message NoPrivacyTransferAction { // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; + common.v1.SolanaAccountId authority = 1 [(validate.rules).message.required = true]; + + + // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; + common.v1.SolanaAccountId source = 2 [(validate.rules).message.required = true]; + + + // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; + common.v1.SolanaAccountId destination = 3 [(validate.rules).message.required = true]; + + + // The quark amount to transfer - uint64 amount = 4; + uint64 amount = 4 [(validate.rules).uint64.gt = 0]; + + + + // The mint that this action will be operating against - common.v1.SolanaAccountId mint = 5; + common.v1.SolanaAccountId mint = 5 [(validate.rules).message.required = true]; + +; } + // Compact message signature required message NoPrivacyWithdrawAction { // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; + common.v1.SolanaAccountId authority = 1 [(validate.rules).message.required = true]; + + + // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; + common.v1.SolanaAccountId source = 2 [(validate.rules).message.required = true]; + + + // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; + common.v1.SolanaAccountId destination = 3 [(validate.rules).message.required = true]; + + + // The quark amount to withdraw - uint64 amount = 4; + uint64 amount = 4 [(validate.rules).uint64.gt = 0]; + + + // Whether the account is closed afterwards. This is always true, since there // are no current se cases to leave it open. - bool should_close = 5; + bool should_close = 5 [(validate.rules).bool.const = true]; + + + // Whether this action is for an auto-return, which client allows server to defer // scheduling at its own discretion to return funds back to the owner (to their primary // account) that funded source. bool is_auto_return = 6; + + // The mint that this action will be operating against - common.v1.SolanaAccountId mint = 7; + common.v1.SolanaAccountId mint = 7 [(validate.rules).message.required = true]; + +; } + // Compact message signature required message FeePaymentAction { // The type of fee being operated on - FeeType type = 1; + FeeType type = 1 [(validate.rules).enum.not_in = 0]; + + enum FeeType { UNKNOWN = 0; CREATE_ON_SEND_WITHDRAWAL = 1; // Server-defined fee for creating an external ATA on withdrawals on send } + // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 2; + common.v1.SolanaAccountId authority = 2 [(validate.rules).message.required = true]; + + + // The source account where funds are transferred from - common.v1.SolanaAccountId source = 3; + common.v1.SolanaAccountId source = 3 [(validate.rules).message.required = true]; + + + // The quark amount to transfer - uint64 amount = 4; + uint64 amount = 4 [(validate.rules).uint64.gt = 0]; + + + + // The mint that this action will be operating against - common.v1.SolanaAccountId mint = 5; + common.v1.SolanaAccountId mint = 5 [(validate.rules).message.required = true]; + +; } + // // Server Parameter Definitions // + // ServerParameter are a set of parameters known and returned by server that // enables clients to complete transaction construction. Any necessary proofs, // which are required to be locally verifiable, are also provided to ensure @@ -697,150 +1066,251 @@ message FeePaymentAction { message ServerParameter { // The action the server parameters belong to uint32 action_id = 1; + // The set of nonces used for the action. Server will only provide values // for transactions requiring client signatures. - repeated NoncedTransactionMetadata nonces = 2 ; + repeated NoncedTransactionMetadata nonces = 2 [(validate.rules).repeated = { + max_items: 1 + }]; + + + // The type of server parameter which maps to the type of action requested oneof type { + option (validate.required) = true; + OpenAccountServerParameter open_account = 3; NoPrivacyTransferServerParameter no_privacy_transfer = 4; NoPrivacyWithdrawServerParameter no_privacy_withdraw = 5; FeePaymentServerParameter fee_payment = 6; } } + // For transactions, the nonce is a standard nonce on Solana // For virtual instructions, the nonce is a virtual nonce on the Code VM message NoncedTransactionMetadata { // The nonce account to use in the system::AdvanceNonce instruction - common.v1.SolanaAccountId nonce = 1; + common.v1.SolanaAccountId nonce = 1 [(validate.rules).message.required = true]; + + + // The blockhash to set in the transaction or virtual instruction - common.v1.Blockhash blockhash = 2; + common.v1.Blockhash blockhash = 2 [(validate.rules).message.required = true]; + + } + message OpenAccountServerParameter { // There are no transactions requiring client signatures } + message NoPrivacyTransferServerParameter { // There are no action-specific server parameters } + message NoPrivacyWithdrawServerParameter { // There are no action-specific server parameters } + message FeePaymentServerParameter { // The destination account where OCP fee payments should be sent. This will // only be set when the corresponding FeePaymentAction.Type: // - CREATE_ON_SEND_WITHDRAWAL - common.v1.SolanaAccountId destination = 1; + common.v1.SolanaAccountId destination = 1 [(validate.rules).message.required = true]; + + } + // // Structured Error Definitions // + message ErrorDetails { oneof type { + option (validate.required) = true; + ReasonStringErrorDetails reason_string = 1; InvalidSignatureErrorDetails invalid_signature = 2; DeniedErrorDetails denied = 3; } } + message ReasonStringErrorDetails { // Human readable string indicating the failure. - string reason = 1 ; + string reason = 1 [(validate.rules).string = { + min_len: 1, + max_len: 2048, // Arbitrary + }]; + + } + message InvalidSignatureErrorDetails { // The action whose signature mismatched uint32 action_id = 1; + oneof expected_blob { + option (validate.required) = true; + // The transaction the server expected to have signed. common.v1.Transaction expected_transaction = 2; + // The virtual ixn hash the server expected to have signed. common.v1.Hash expected_vixn_hash = 4; } + // The signature that was provided by the client. - common.v1.Signature provided_signature = 3; + common.v1.Signature provided_signature = 3 [(validate.rules).message.required = true]; + + } + message DeniedErrorDetails { Code code = 1; enum Code { // Reason code not yet defined UNSPECIFIED = 0; } + // Human readable string indicating the failure. - string reason = 2 ; + string reason = 2 [(validate.rules).string = { + min_len: 1, + max_len: 2048, // Arbitrary + }]; + + } + // // Other Model Definitions // + // VerifiedExchangeData defines an amount of crypto to use in a payment flow // with verified server-state for provable fiat exchange data message VerifiedExchangeData { // The crypto mint that is being operated against for the payment flow. - common.v1.SolanaAccountId mint = 1; + common.v1.SolanaAccountId mint = 1 [(validate.rules).message.required = true]; + + + // The exact amount of quarks being operated in a payment flow. // This will be used as the source of truth for validating transfer amounts. - uint64 quarks = 2; + uint64 quarks = 2 [(validate.rules).uint64.gt = 0]; + + + // The agreed upon fiat amount in a payment flow. - double native_amount = 3; + double native_amount = 3 [(validate.rules).double.gt = 0]; + + + // Verified core mint fiat exchange rate used to compute the exchange data // // Required when operating against: // - Core mint // - Launchpad currency - currency.v1.VerifiedCoreMintFiatExchangeRate core_mint_fiat_exchange_rate = 4; + currency.v1.VerifiedCoreMintFiatExchangeRate core_mint_fiat_exchange_rate = 4 [(validate.rules).message.required = true]; + + + // Verified launchpad currency reserve state used to compute the exchange data // // Required when operating against: // - Launchpad currency currency.v1.VerifiedLaunchpadCurrencyReserveState launchpad_currency_reserve_state = 5; } + // ExchangeData defines an amount of crypto to use in a payment flow with // fiat exchange data message ExchangeData { // ISO 4217 alpha-3 currency code. - string currency = 1; + string currency = 1 [(validate.rules).string = { pattern: "^[a-z]{3,4}$" }]; + + + // The agreed upon exchange rate. This might not be the same as the // actual exchange rate at the time of intent or fund transfer. - double exchange_rate = 2; + double exchange_rate = 2 [(validate.rules).double.gt = 0]; + + + // The agreed upon fiat amount in a payment flow. - double native_amount = 3; + double native_amount = 3 [(validate.rules).double.gt = 0]; + + + // The exact amount of quarks being operated in a payment flow. // This will be used as the source of truth for validating transfer amounts. - uint64 quarks = 4; + uint64 quarks = 4 [(validate.rules).uint64.gt = 0]; + + + // The crypto mint that is being operated against for the payment flow. - common.v1.SolanaAccountId mint = 5; + common.v1.SolanaAccountId mint = 5 [(validate.rules).message.required = true]; + +; } + message ExchangeDataWithoutRate { // ISO 4217 alpha-3 currency code. - string currency = 1; + string currency = 1 [(validate.rules).string = { pattern: "^[a-z]{3,4}$" }]; + + + // The agreed upon fiat amount in a payment flow. - double native_amount = 2; + double native_amount = 2 [(validate.rules).double.gt = 0]; + + } + message SendLimit { // Remaining limit to apply on the next transaction float next_transaction = 1; + // Maximum allowed on a per-transaction basis float max_per_transaction = 2; + // Maximum allowed on a per-day basis float max_per_day = 3; } + // VerifiedSwapMetadata defines verifiable swap metadata for non-custodial swap // state management using client signature verification. message VerifiedSwapMetadata { oneof kind { + option (validate.required) = true; + VerifiedReserveSwapMetadata reserve = 1; } } + // VerifiedReserveSwapMetadata is verified metadata for swaps against the // Currency Creator program message VerifiedReserveSwapMetadata { // Verifiable client-side parameters that were provided during the StatefulSwap RPC - StatefulSwapRequest.Initiate.ReserveSwapClientParameters client_parameters = 1; + StatefulSwapRequest.Initiate.ReserveSwapClientParameters client_parameters = 1 [(validate.rules).message.required = true]; + + } + message SwapMetadata { - VerifiedSwapMetadata verified_metadata = 1; - State state = 2 ; + VerifiedSwapMetadata verified_metadata = 1 [(validate.rules).message.required = true]; + + + + State state = 2 [(validate.rules).enum = { + not_in: [0] // UNKNOWN + }]; + + + // The signature is of serialize(VerifiedSwapMetadata) using the private // key of the owner account. Use this to guarantee that VerifiedSwapMetadata // has not been tampered with. - common.v1.Signature signature = 3; + common.v1.Signature signature = 3 [(validate.rules).message.required = true]; + + + enum State { UNKNOWN = 0; CREATED = 1; // Swap state has been created and is pending funding @@ -853,6 +1323,7 @@ message SwapMetadata { CANCELLED = 8; // The swap transaction is cancelled. Funds have been deposited back into the VM } } + enum FundingSource { FUNDING_SOURCE_UNKNOWN = 0; FUNDING_SOURCE_SUBMIT_INTENT = 1; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 876228c5c..612e99532 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,6 +59,7 @@ grpc-okhttp = "1.79.0" grpc-kotlin = "1.5.0" protobuf = "4.34.0" protobuf-plugin = "0.9.6" +protovalidate-kt = "0.1.0" lib-phone-number-port = "9.0.28" lib-phone-number-google = "9.0.28" @@ -211,6 +212,7 @@ grpc-protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grp grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } +protobuf-validate-runtime = { module = "dev.bmcreations:protovalidate-runtime", version.ref = "protovalidate-kt" } # Retrofit retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } @@ -332,3 +334,4 @@ protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } androidx-room = { id = "androidx.room", version.ref = "androidx-room" } screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +protobuf-validate = { id = "dev.bmcreations.protovalidate", version.ref = "protovalidate-kt" } diff --git a/scripts/fetch-protos.sh b/scripts/fetch-protos.sh index dd15bc4ff..ed158b717 100755 --- a/scripts/fetch-protos.sh +++ b/scripts/fetch-protos.sh @@ -3,19 +3,15 @@ root=$(pwd) REPO_URL="git@github.com:code-payments/ocp-protobuf-api.git" # Default repo URL COMMIT_SHA="" -RUN_STRIP_PROTO_VALIDATION=false # Default to not running the script TEMP_DIR=$(mktemp -d) TARGET="code" # Parse options -while getopts ":r:t:x" opt; do +while getopts ":r:t:" opt; do case ${opt} in r ) REPO_URL=$OPTARG ;; - x ) - RUN_STRIP_PROTO_VALIDATION=true - ;; t ) TARGET=$OPTARG if [ "$TARGET" == "flipchat" ]; then @@ -67,21 +63,7 @@ fi cd ../.. rm -rf "$TEMP_DIR" -# Conditionally run the strip proto validation script -if [ "$RUN_STRIP_PROTO_VALIDATION" = true ]; then - SCRIPT_PATH="${root}/scripts/strip-proto-validation.sh" - - # Ensure the script exists and is executable - if [ -f "$SCRIPT_PATH" ]; then - if [ -x "$SCRIPT_PATH" ]; then - echo "Running strip-proto-validation.sh" - "$SCRIPT_PATH" - else - echo "Error: strip-proto-validation.sh is not executable. Run 'chmod +x $SCRIPT_PATH' to fix this." - exit 1 - fi - else - echo "Error: strip-proto-validation.sh not found at $SCRIPT_PATH" - exit 1 - fi -fi +# Preserve custom opencode namespacing +if [ "$TARGET" = "opencode" ]; then + find "${root}/definitions/$TARGET/protos/src/main/proto" -name "*.proto" -type f -exec sh -c "awk '{gsub(/];/, \"];\n\n\"); gsub(/option java_package = \"com\.codeinc\.gen\./, \"option java_package = \\\"com.codeinc.opencode.gen.\"); print}' {} > tmp && mv tmp {}" \; +fi \ No newline at end of file diff --git a/scripts/strip-proto-validation.sh b/scripts/strip-proto-validation.sh deleted file mode 100755 index ec4c48c12..000000000 --- a/scripts/strip-proto-validation.sh +++ /dev/null @@ -1,47 +0,0 @@ -# -# For all .proto files, strip the validation parameters & replace inline -# - -root=$(pwd) - -target=$1 - -# 1. hack: first add a couple newlines after all "];" -find "${root}"/definitions/$target/protos/src/main/proto -name "*.proto" -type f -exec sh -c "awk '{gsub(/];/, \"];\n\n\"); print}' {} > tmp && mv tmp {}" \; - -# 2. strip everything between square brackets [...] ignoring lines starting with // -find "${root}"/definitions/$target/protos/src/main/proto -name "*.proto" -type f -exec sh -c "awk '!/^[[:space:]]*\/\// {gsub(/ \[.*\]/, \"\");} {print}' {} > tmp && mv tmp {}" \; - -find "${root}"/definitions/$target/protos/src/main/proto -name "*.proto" -type f | while read -r file; do - awk ' - BEGIN { in_repeated = 0; buffer = "" } - { - if ($0 ~ /^[[:space:]]*(repeated|bytes|[A-Za-z0-9_]+).*=.*\[/) { - in_repeated = 1 - buffer = $0 - } else if (in_repeated) { - buffer = buffer " " $0 - } - - if (in_repeated && $0 ~ /;/) { - gsub(/\[.*\]/, "", buffer) - print buffer - in_repeated = 0 - buffer = "" - } else if (!in_repeated && $0 !~ /^[[:space:]]*option[[:space:]]*\(validate\.required\)[[:space:]]*=[[:space:]]*true;/) { - print $0 - } - } - ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" -done - -# 3. strip validate import statement -find "${root}"/definitions/$target/protos/src/main/proto -name "*.proto" -type f -exec sh -c "awk -v RS='' '{gsub(/import \"validate\/validate.proto\";/, \"\"); print}' {} > tmp && mv tmp {}" \; - -# 4. Preserve custom opencode namespacing -if [ "$target" = "opencode" ]; then - find "${root}/definitions/$target/protos/src/main/proto" -name "*.proto" -type f -exec sh -c "awk '{gsub(/];/, \"];\n\n\"); gsub(/option java_package = \"com\.codeinc\.gen\./, \"option java_package = \\\"com.codeinc.opencode.gen.\"); print}' {} > tmp && mv tmp {}" \; -fi - -# 5. Remove lines containing only ; -find "${root}/definitions/$target/protos/src/main/proto" -name "*.proto" -type f -exec sh -c "awk '!/^[[:space:]]*;[[:space:]]*$/ {print}' {} > tmp && mv tmp {}" \; \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f233b4b9..f1d0caa07 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ pluginManagement { includeBuild("build-logic") repositories { + mavenLocal() google() mavenCentral() gradlePluginPortal() @@ -18,6 +19,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode = RepositoriesMode.PREFER_SETTINGS repositories { + mavenLocal() google() mavenCentral() maven(url = "https://plugins.gradle.org/m2/") From a6106e0d21926bc3ef49567a210542ec5278fa44 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 21 Apr 2026 15:27:17 -0400 Subject: [PATCH 2/5] feat(services): add runtime RPC request validation Signed-off-by: Brandon McAnsh --- services/flipcash/build.gradle.kts | 1 + .../internal/network/api/AccountApi.kt | 8 + .../internal/network/api/ActivityFeedApi.kt | 8 + .../network/api/EmailVerificationApi.kt | 8 + .../internal/network/api/ModerationApi.kt | 6 + .../network/api/PhoneVerificationApi.kt | 8 + .../internal/network/api/ProfileApi.kt | 4 + .../internal/network/api/PurchaseApi.kt | 4 + .../services/internal/network/api/PushApi.kt | 6 + .../internal/network/api/SettingsApi.kt | 4 + .../internal/network/api/ThirdPartyApi.kt | 4 + .../network/services/AccountService.kt | 7 +- .../network/services/ActivityFeedService.kt | 7 +- .../services/EmailVerificationService.kt | 7 +- .../network/services/ModerationService.kt | 5 +- .../services/PhoneVerificationService.kt | 7 +- .../network/services/ProfileService.kt | 9 +- .../network/services/PurchaseService.kt | 3 +- .../internal/network/services/PushService.kt | 5 +- .../network/services/SettingsService.kt | 3 +- .../network/services/ThirdPartyService.kt | 3 +- .../services/PhoneVerificationServiceTest.kt | 163 ++++++++++++++++++ services/opencode/build.gradle.kts | 2 + .../InternalCurrencyRepository.kt | 2 +- .../internal/network/api/AccountApi.kt | 8 +- .../internal/network/api/CurrencyApi.kt | 16 +- .../internal/network/api/MessagingApi.kt | 10 ++ .../internal/network/api/TransactionApi.kt | 14 ++ .../network/services/AccountService.kt | 5 +- .../network/services/CurrencyService.kt | 17 +- .../network/services/MessagingService.kt | 6 +- .../internal/network/services/SwapService.kt | 6 +- .../network/services/TransactionService.kt | 10 +- .../model/core/errors/ValidationException.kt | 16 ++ .../com/getcode/opencode/utils/Throwable.kt | 15 ++ .../network/services/CurrencyServiceTest.kt | 29 ++++ .../opencode/utils/ThrowableExtensionsTest.kt | 60 +++++++ 37 files changed, 453 insertions(+), 43 deletions(-) create mode 100644 services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationServiceTest.kt create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/ValidationException.kt create mode 100644 services/opencode/src/main/kotlin/com/getcode/opencode/utils/Throwable.kt create mode 100644 services/opencode/src/test/kotlin/com/getcode/opencode/utils/ThrowableExtensionsTest.kt diff --git a/services/flipcash/build.gradle.kts b/services/flipcash/build.gradle.kts index 281940fdf..f41be8388 100644 --- a/services/flipcash/build.gradle.kts +++ b/services/flipcash/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.grpc.okhttp) implementation(libs.grpc.kotlin) implementation(libs.grpc.protobuf.lite) + implementation(libs.protobuf.validate.runtime) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/AccountApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/AccountApi.kt index 3040f0967..fd6c0b52d 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/AccountApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/AccountApi.kt @@ -1,6 +1,7 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.account.v1.AccountGrpcKt +import com.codeinc.flipcash.gen.account.v1.validate import com.codeinc.flipcash.gen.common.v1.Common import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.asCountryCode @@ -12,6 +13,7 @@ import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.network.core.GrpcApi import com.getcode.opencode.model.core.ID import com.google.protobuf.Timestamp +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,6 +39,8 @@ internal class AccountApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.register(request) } @@ -52,6 +56,8 @@ internal class AccountApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.login(request) } @@ -72,6 +78,8 @@ internal class AccountApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getUserFlags(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ActivityFeedApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ActivityFeedApi.kt index d65d7bedc..27c4815e4 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ActivityFeedApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ActivityFeedApi.kt @@ -3,6 +3,7 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.activity.v1.ActivityFeedGrpcKt import com.codeinc.flipcash.gen.activity.v1.ActivityFeedService import com.codeinc.flipcash.gen.activity.v1.Model +import com.codeinc.flipcash.gen.activity.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.asQueryOptions import com.flipcash.services.internal.network.extensions.authenticate @@ -12,6 +13,7 @@ import com.flipcash.services.models.QueryOptions import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.network.core.GrpcApi import com.getcode.opencode.model.core.ID +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -45,6 +47,8 @@ internal class ActivityFeedApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getLatestNotifications(request) } @@ -68,6 +72,8 @@ internal class ActivityFeedApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getPagedNotifications(request) } @@ -90,6 +96,8 @@ internal class ActivityFeedApi @Inject constructor( setAuth(authenticate(owner)) }.build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getBatchNotifications(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt index bee2015d9..7cf6fce13 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt @@ -8,6 +8,8 @@ import com.flipcash.services.internal.network.extensions.authenticate import com.flipcash.services.models.ContactMethod import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.core.GrpcApi +import com.codeinc.flipcash.gen.email.v1.validate +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -38,6 +40,8 @@ internal class EmailVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.sendVerificationCode(request) } @@ -57,6 +61,8 @@ internal class EmailVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.checkVerificationCode(request) } @@ -71,6 +77,8 @@ internal class EmailVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.unlink(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ModerationApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ModerationApi.kt index 7b87e8362..d5a28fb07 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ModerationApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ModerationApi.kt @@ -2,11 +2,13 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.moderation.v1.ModerationGrpcKt import com.codeinc.flipcash.gen.moderation.v1.ModerationService +import com.codeinc.flipcash.gen.moderation.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.authenticate import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.core.GrpcApi import com.getcode.utils.toByteString +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -27,6 +29,8 @@ internal class ModerationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.moderateText(request) } @@ -38,6 +42,8 @@ internal class ModerationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.moderateImage(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt index fa6fbecbc..606d3e4fc 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt @@ -9,6 +9,8 @@ import com.flipcash.services.internal.network.extensions.authenticate import com.flipcash.services.models.ContactMethod import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.core.GrpcApi +import com.codeinc.flipcash.gen.phone.v1.validate +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,6 +41,8 @@ internal class PhoneVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.sendVerificationCode(request) } @@ -58,6 +62,8 @@ internal class PhoneVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.checkVerificationCode(request) } @@ -75,6 +81,8 @@ internal class PhoneVerificationApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.unlink(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt index 2ec025f26..89dca6956 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt @@ -11,6 +11,8 @@ import com.flipcash.services.models.SocialAccountUnlinkRequest import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.core.GrpcApi import com.getcode.opencode.model.core.ID +import com.codeinc.flipcash.gen.profile.v1.validate +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -52,6 +54,8 @@ internal class ProfileApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.setDisplayName(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PurchaseApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PurchaseApi.kt index 1fb0292fc..e35791785 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PurchaseApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PurchaseApi.kt @@ -3,12 +3,14 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.iap.v1.IapGrpcKt import com.codeinc.flipcash.gen.iap.v1.IapService +import com.codeinc.flipcash.gen.iap.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.model.billing.IapMetadata import com.flipcash.services.internal.model.billing.Receipt import com.flipcash.services.internal.network.extensions.authenticate import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.network.core.GrpcApi +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -40,6 +42,8 @@ internal class PurchaseApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.onPurchaseCompleted(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PushApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PushApi.kt index 881e56421..3ee2494b5 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PushApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PushApi.kt @@ -4,10 +4,12 @@ import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.push.v1.Model import com.codeinc.flipcash.gen.push.v1.PushGrpcKt import com.codeinc.flipcash.gen.push.v1.PushService +import com.codeinc.flipcash.gen.push.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.authenticate import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.network.core.GrpcApi +import dev.bmcreations.protovalidate.orThrow import io.grpc.Deadline import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers @@ -41,6 +43,8 @@ internal class PushApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.addToken(request) } @@ -59,6 +63,8 @@ internal class PushApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.deleteTokens(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/SettingsApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/SettingsApi.kt index 431cf94f2..13db7db1d 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/SettingsApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/SettingsApi.kt @@ -3,10 +3,12 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.settings.v1.SettingsGrpcKt import com.codeinc.flipcash.gen.settings.v1.SettingsService +import com.codeinc.flipcash.gen.settings.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.authenticate import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.core.GrpcApi +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,6 +41,8 @@ internal class SettingsApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.updateSettings(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ThirdPartyApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ThirdPartyApi.kt index 571628ae4..157c7b8a8 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ThirdPartyApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ThirdPartyApi.kt @@ -2,12 +2,14 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.thirdparty.v1.ThirdPartyGrpcKt import com.codeinc.flipcash.gen.thirdparty.v1.ThirdPartyService +import com.codeinc.flipcash.gen.thirdparty.v1.validate import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.asApiKey import com.flipcash.services.internal.network.extensions.authenticate import com.getcode.ed25519.Ed25519 import com.getcode.network.jwt.JwtSecuredEndpoint import com.getcode.opencode.internal.network.core.GrpcApi +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,6 +39,8 @@ internal class ThirdPartyApi @Inject constructor( .apply { setAuth(authenticate(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getJwt(request) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/AccountService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/AccountService.kt index b743b9215..31ff0cd3b 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/AccountService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/AccountService.kt @@ -1,6 +1,7 @@ package com.flipcash.services.internal.network.services import com.flipcash.services.internal.network.api.AccountApi +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.flipcash.services.models.GetUserFlagsError import com.flipcash.services.models.LoginError @@ -39,7 +40,7 @@ internal class AccountService @Inject constructor( } }, onFailure = { cause -> - Result.failure(RegisterError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { RegisterError.Other(cause = it) }) } ) } @@ -70,7 +71,7 @@ internal class AccountService @Inject constructor( } }, onFailure = { cause -> - Result.failure(LoginError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { LoginError.Other(cause = it) }) } ) } @@ -99,7 +100,7 @@ internal class AccountService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetUserFlagsError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetUserFlagsError.Other(cause = it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ActivityFeedService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ActivityFeedService.kt index ee7e39a28..0760eae2b 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ActivityFeedService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ActivityFeedService.kt @@ -3,6 +3,7 @@ package com.flipcash.services.internal.network.services import com.codeinc.flipcash.gen.activity.v1.ActivityFeedService import com.codeinc.flipcash.gen.activity.v1.Model import com.flipcash.services.internal.network.api.ActivityFeedApi +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.flipcash.services.models.ActivityFeedType import com.flipcash.services.models.GetActivityFeedMessagesError @@ -31,7 +32,7 @@ internal class ActivityFeedService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetActivityFeedMessagesError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetActivityFeedMessagesError.Other(cause = it) }) } ) } @@ -53,7 +54,7 @@ internal class ActivityFeedService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetActivityFeedMessagesError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetActivityFeedMessagesError.Other(cause = it) }) } ) } @@ -75,7 +76,7 @@ internal class ActivityFeedService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetActivityFeedMessagesError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetActivityFeedMessagesError.Other(cause = it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/EmailVerificationService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/EmailVerificationService.kt index 4494fbd01..662a69ee2 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/EmailVerificationService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/EmailVerificationService.kt @@ -1,6 +1,7 @@ package com.flipcash.services.internal.network.services import com.flipcash.services.internal.network.api.EmailVerificationApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.ContactMethod import com.flipcash.services.models.EmailVerificationError import com.getcode.ed25519.Ed25519 @@ -39,7 +40,7 @@ internal class EmailVerificationService @Inject constructor( }, onFailure = { cause -> - Result.failure(EmailVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { EmailVerificationError.Other(it) }) } ) @@ -76,7 +77,7 @@ internal class EmailVerificationService @Inject constructor( }, onFailure = { cause -> - Result.failure(EmailVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { EmailVerificationError.Other(it) }) } ) } @@ -101,7 +102,7 @@ internal class EmailVerificationService @Inject constructor( } }, onFailure = { cause -> - Result.failure(EmailVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { EmailVerificationError.Other(it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ModerationService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ModerationService.kt index 0dca59c32..93234de65 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ModerationService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ModerationService.kt @@ -2,6 +2,7 @@ package com.flipcash.services.internal.network.services import com.codeinc.flipcash.gen.moderation.v1.ModerationService import com.flipcash.services.internal.network.api.ModerationApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.ImageModerationError import com.flipcash.services.models.TextModerationError import com.getcode.ed25519.Ed25519 @@ -25,7 +26,7 @@ internal class ModerationService @Inject constructor( } }, onFailure = { cause -> - Result.failure(TextModerationError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { TextModerationError.Other(cause = it) }) } ) } @@ -45,7 +46,7 @@ internal class ModerationService @Inject constructor( } }, onFailure = { cause -> - Result.failure(ImageModerationError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { ImageModerationError.Other(cause = it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationService.kt index bef235483..33a1a8c6a 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationService.kt @@ -1,6 +1,7 @@ package com.flipcash.services.internal.network.services import com.flipcash.services.internal.network.api.PhoneVerificationApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.ContactMethod import com.flipcash.services.models.PhoneVerificationError import com.getcode.ed25519.Ed25519 @@ -42,7 +43,7 @@ internal class PhoneVerificationService @Inject constructor( }, onFailure = { cause -> - Result.failure(PhoneVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { PhoneVerificationError.Other(it) }) } ) @@ -79,7 +80,7 @@ internal class PhoneVerificationService @Inject constructor( }, onFailure = { cause -> - Result.failure(PhoneVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { PhoneVerificationError.Other(it) }) } ) } @@ -106,7 +107,7 @@ internal class PhoneVerificationService @Inject constructor( } }, onFailure = { cause -> - Result.failure(PhoneVerificationError.Other(cause)) + Result.failure(cause.toValidationOrElse { PhoneVerificationError.Other(it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt index 432d8fe36..faa2da840 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt @@ -3,6 +3,7 @@ package com.flipcash.services.internal.network.services import com.codeinc.flipcash.gen.profile.v1.Model import com.codeinc.flipcash.gen.profile.v1.ProfileService import com.flipcash.services.internal.network.api.ProfileApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.GetUserProfileError import com.flipcash.services.models.LinkSocialAccountError import com.flipcash.services.models.SetDisplayNameError @@ -31,7 +32,7 @@ internal class ProfileService @Inject constructor( ProfileService.GetProfileResponse.Result.UNRECOGNIZED -> Result.failure(GetUserProfileError.Unrecognized()) } }, - onFailure = { Result.failure(it) } + onFailure = { Result.failure(it.toValidationOrElse { cause -> GetUserProfileError.Other(cause) }) } ) } @@ -50,7 +51,7 @@ internal class ProfileService @Inject constructor( ProfileService.SetDisplayNameResponse.Result.UNRECOGNIZED -> Result.failure(SetDisplayNameError.Unrecognized()) } }, - onFailure = { Result.failure(it) } + onFailure = { Result.failure(it.toValidationOrElse { cause -> SetDisplayNameError.Other(cause) }) } ) } @@ -70,7 +71,7 @@ internal class ProfileService @Inject constructor( ProfileService.LinkSocialAccountResponse.Result.UNRECOGNIZED -> Result.failure(LinkSocialAccountError.Unrecognized()) } }, - onFailure = { Result.failure(it) } + onFailure = { Result.failure(it.toValidationOrElse { cause -> LinkSocialAccountError.Other(cause) }) } ) } @@ -88,7 +89,7 @@ internal class ProfileService @Inject constructor( ProfileService.UnlinkSocialAccountResponse.Result.UNRECOGNIZED -> Result.failure(UnlinkSocialAccountError.Unrecognized()) } }, - onFailure = { Result.failure(it) } + onFailure = { Result.failure(it.toValidationOrElse { cause -> UnlinkSocialAccountError.Other(cause) }) } ) } } \ No newline at end of file diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PurchaseService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PurchaseService.kt index 339a6ca0d..1cded8a70 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PurchaseService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PurchaseService.kt @@ -4,6 +4,7 @@ import com.codeinc.flipcash.gen.iap.v1.IapService import com.flipcash.services.internal.model.billing.IapMetadata import com.flipcash.services.internal.model.billing.Receipt import com.flipcash.services.internal.network.api.PurchaseApi +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.flipcash.services.models.PurchaseAckError import com.getcode.ed25519.Ed25519.KeyPair @@ -27,7 +28,7 @@ internal class PurchaseService @Inject constructor( } }, onFailure = { cause -> - Result.failure(PurchaseAckError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { PurchaseAckError.Other(cause = it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PushService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PushService.kt index 6efbb5ad2..acefecced 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PushService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PushService.kt @@ -1,6 +1,7 @@ package com.flipcash.services.internal.network.services import com.flipcash.services.internal.network.api.PushApi +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.flipcash.services.models.AddTokenError import com.flipcash.services.models.DeleteTokenError @@ -28,7 +29,7 @@ internal class PushService @Inject constructor( } }, onFailure = { cause -> - Result.failure(AddTokenError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { AddTokenError.Other(cause = it) }) } ) } @@ -48,7 +49,7 @@ internal class PushService @Inject constructor( } }, onFailure = { cause -> - Result.failure(DeleteTokenError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { DeleteTokenError.Other(cause = it) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/SettingsService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/SettingsService.kt index 4556a3359..bc1aef9cc 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/SettingsService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/SettingsService.kt @@ -2,6 +2,7 @@ package com.flipcash.services.internal.network.services import com.codeinc.flipcash.gen.settings.v1.SettingsService import com.flipcash.services.internal.network.api.SettingsApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.UpdateSettingsError import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.network.extensions.foldWithSuppression @@ -28,7 +29,7 @@ internal class SettingsService @Inject constructor( } }, onFailure = { - Result.failure(UpdateSettingsError.Other(it)) + Result.failure(it.toValidationOrElse { cause -> UpdateSettingsError.Other(cause) }) } ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ThirdPartyService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ThirdPartyService.kt index 0607f3f7e..949abe761 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ThirdPartyService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ThirdPartyService.kt @@ -2,6 +2,7 @@ package com.flipcash.services.internal.network.services import com.codeinc.flipcash.gen.thirdparty.v1.ThirdPartyService import com.flipcash.services.internal.network.api.ThirdPartyApi +import com.getcode.opencode.utils.toValidationOrElse import com.flipcash.services.models.GetJwtError import com.flipcash.services.models.Jwt import com.getcode.ed25519.Ed25519 @@ -32,7 +33,7 @@ internal class ThirdPartyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetJwtError.Other(cause)) + Result.failure(cause.toValidationOrElse { GetJwtError.Other(it) }) } ) } diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationServiceTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationServiceTest.kt new file mode 100644 index 000000000..5fe017e87 --- /dev/null +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/services/PhoneVerificationServiceTest.kt @@ -0,0 +1,163 @@ +package com.flipcash.services.internal.network.services + +import com.flipcash.services.internal.network.api.PhoneVerificationApi +import com.flipcash.services.models.ContactMethod +import com.flipcash.services.models.PhoneVerificationError +import com.getcode.ed25519.Ed25519 +import com.getcode.opencode.model.core.errors.ValidationException +import dev.bmcreations.protovalidate.FieldViolation +import dev.bmcreations.protovalidate.ProtoValidationException +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import com.codeinc.flipcash.gen.phone.v1.PhoneVerificationService as RpcPhoneService + +class PhoneVerificationServiceTest { + + private val api = mockk() + private val service = PhoneVerificationService(api) + private val owner = mockk(relaxed = true) + private val phone = ContactMethod.Phone("+15551234567") + + // region sendVerificationCode + + @Test + fun `sendVerificationCode OK returns success`() = runTest { + coEvery { api.sendVerificationCode(any(), any()) } returns + sendCodeResponse(RpcPhoneService.SendVerificationCodeResponse.Result.OK) + + val result = service.sendVerificationCode(phone, owner) + + assertTrue(result.isSuccess) + } + + @Test + fun `sendVerificationCode DENIED returns Denied error`() = runTest { + coEvery { api.sendVerificationCode(any(), any()) } returns + sendCodeResponse(RpcPhoneService.SendVerificationCodeResponse.Result.DENIED) + + val result = service.sendVerificationCode(phone, owner) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + @Test + fun `sendVerificationCode exception returns Other error with cause`() = runTest { + coEvery { api.sendVerificationCode(any(), any()) } throws RuntimeException("network failure") + + val result = service.sendVerificationCode(phone, owner) + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertIs(error) + assertNotNull(error.cause) + } + + @Test + fun `sendVerificationCode ProtoValidationException returns ValidationException`() = runTest { + val violation = FieldViolation(field = "phone_number", rule = "string.pattern", message = "invalid format") + coEvery { api.sendVerificationCode(any(), any()) } throws ProtoValidationException(listOf(violation)) + + val result = service.sendVerificationCode(phone, owner) + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertIs(error) + assertEquals("phone_number", error.violations.first().field) + assertEquals("invalid format", error.violations.first().message) + } + + // endregion + + // region checkVerificationCode + + @Test + fun `checkVerificationCode OK returns success`() = runTest { + coEvery { api.checkVerificationCode(any(), any(), any()) } returns + checkCodeResponse(RpcPhoneService.CheckVerificationCodeResponse.Result.OK) + + val result = service.checkVerificationCode(phone, "123456", owner) + + assertTrue(result.isSuccess) + } + + @Test + fun `checkVerificationCode INVALID_CODE returns InvalidVerificationCode error`() = runTest { + coEvery { api.checkVerificationCode(any(), any(), any()) } returns + checkCodeResponse(RpcPhoneService.CheckVerificationCodeResponse.Result.INVALID_CODE) + + val result = service.checkVerificationCode(phone, "000000", owner) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + @Test + fun `checkVerificationCode ProtoValidationException returns ValidationException`() = runTest { + val violation = FieldViolation(field = "code", rule = "string.len", message = "must be 6 digits") + coEvery { api.checkVerificationCode(any(), any(), any()) } throws ProtoValidationException(listOf(violation)) + + val result = service.checkVerificationCode(phone, "bad", owner) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + // endregion + + // region unlink + + @Test + fun `unlink OK returns success`() = runTest { + coEvery { api.unlink(any(), any()) } returns + unlinkResponse(RpcPhoneService.UnlinkResponse.Result.OK) + + val result = service.unlink(phone, owner) + + assertTrue(result.isSuccess) + } + + @Test + fun `unlink ProtoValidationException returns ValidationException`() = runTest { + val violation = FieldViolation(field = "phone_number", rule = "required", message = "required") + coEvery { api.unlink(any(), any()) } throws ProtoValidationException(listOf(violation)) + + val result = service.unlink(phone, owner) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + // endregion + + // region helpers + + private fun sendCodeResponse( + result: RpcPhoneService.SendVerificationCodeResponse.Result, + ): RpcPhoneService.SendVerificationCodeResponse = + RpcPhoneService.SendVerificationCodeResponse.newBuilder() + .setResult(result) + .build() + + private fun checkCodeResponse( + result: RpcPhoneService.CheckVerificationCodeResponse.Result, + ): RpcPhoneService.CheckVerificationCodeResponse = + RpcPhoneService.CheckVerificationCodeResponse.newBuilder() + .setResult(result) + .build() + + private fun unlinkResponse( + result: RpcPhoneService.UnlinkResponse.Result, + ): RpcPhoneService.UnlinkResponse = + RpcPhoneService.UnlinkResponse.newBuilder() + .setResult(result) + .build() + + // endregion +} diff --git a/services/opencode/build.gradle.kts b/services/opencode/build.gradle.kts index bfd860180..22f55c04a 100644 --- a/services/opencode/build.gradle.kts +++ b/services/opencode/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { api(project(":vendor:kik:scanner")) + implementation(libs.protobuf.validate.runtime) + implementation(libs.javax.inject) implementation(libs.kotlinx.serialization.json) diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalCurrencyRepository.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalCurrencyRepository.kt index 99fe4d84f..e046c1679 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalCurrencyRepository.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/domain/repositories/InternalCurrencyRepository.kt @@ -51,7 +51,7 @@ internal class InternalCurrencyRepository @Inject constructor( } override suspend fun checkTokenAvailability(name: String): Result = - service.checkTokenAvailability(name.trim()) + service.checkTokenAvailability(name) .onFailure { ErrorUtils.handleError(it) } override suspend fun launchToken( diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/AccountApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/AccountApi.kt index 68749f06b..d6c362887 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/AccountApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/AccountApi.kt @@ -2,12 +2,14 @@ package com.getcode.opencode.internal.network.api import com.codeinc.opencode.gen.account.v1.AccountGrpcKt import com.codeinc.opencode.gen.account.v1.AccountService +import com.codeinc.opencode.gen.account.v1.validate import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel import com.getcode.opencode.internal.network.core.GrpcApi import com.getcode.opencode.internal.network.extensions.asSolanaAccountId import com.getcode.opencode.internal.network.extensions.sign import com.getcode.opencode.model.accounts.AccountFilter +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -29,7 +31,7 @@ internal class AccountApi @Inject constructor( * etc. * * @param owner The owner account to check against. - * @return The [AccountService.IsCodeAccountResponse] + * @return The [AccountService.IsOcpAccountResponse] */ suspend fun isCodeAccount( owner: KeyPair, @@ -39,6 +41,8 @@ internal class AccountApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.isOcpAccount(request) } @@ -88,6 +92,8 @@ internal class AccountApi @Inject constructor( } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getTokenAccountInfos(request) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/CurrencyApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/CurrencyApi.kt index 0af043fa4..a78792ffa 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/CurrencyApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/CurrencyApi.kt @@ -2,6 +2,7 @@ package com.getcode.opencode.internal.network.api import com.codeinc.opencode.gen.currency.v1.CurrencyGrpcKt import com.codeinc.opencode.gen.currency.v1.CurrencyService +import com.codeinc.opencode.gen.currency.v1.validate import com.getcode.ed25519.Ed25519 import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel import com.getcode.opencode.internal.annotations.OpenCodeManagedStreamingChannel @@ -19,6 +20,7 @@ import com.getcode.opencode.model.ui.TokenBillCustomizations import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import com.getcode.utils.toByteString +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -58,6 +60,8 @@ internal class CurrencyApi @Inject constructor( } }.build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getMints(request) } @@ -81,6 +85,8 @@ internal class CurrencyApi @Inject constructor( } ).build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getHistoricalMintData(request) } @@ -94,9 +100,11 @@ internal class CurrencyApi @Inject constructor( suspend fun checkTokenAvailability(name: String): CurrencyService.CheckAvailabilityResponse { val request = CurrencyService.CheckAvailabilityRequest.newBuilder() - .setName(name) + .setName(name.trim()) .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.checkAvailability(request) } @@ -133,6 +141,8 @@ internal class CurrencyApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.launch(request) } @@ -150,6 +160,8 @@ internal class CurrencyApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.updateIcon(request) } @@ -196,6 +208,8 @@ internal class CurrencyApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.updateMetadata(request) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/MessagingApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/MessagingApi.kt index 371c452fb..053f84439 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/MessagingApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/MessagingApi.kt @@ -3,6 +3,7 @@ package com.getcode.opencode.internal.network.api import com.codeinc.opencode.gen.common.v1.Model import com.codeinc.opencode.gen.messaging.v1.MessagingGrpcKt import com.codeinc.opencode.gen.messaging.v1.MessagingService +import com.codeinc.opencode.gen.messaging.v1.validate import com.getcode.ed25519.Ed25519 import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel @@ -11,6 +12,7 @@ import com.getcode.opencode.internal.network.extensions.asRendezvousKey import com.getcode.opencode.internal.network.extensions.sign import com.getcode.utils.trace import com.google.protobuf.ByteString +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -60,6 +62,8 @@ internal class MessagingApi @Inject constructor( .apply { setSignature(sign(rendezvous)) } .build() + request.validate().orThrow() + return api.openMessageStream(request) } @@ -126,6 +130,8 @@ internal class MessagingApi @Inject constructor( .apply { setSignature(sign(rendezvous)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.pollMessages(request) } } @@ -141,6 +147,8 @@ internal class MessagingApi @Inject constructor( .addAllMessageIds(messageIds) .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.ackMessages(request) } } @@ -163,6 +171,8 @@ internal class MessagingApi @Inject constructor( .setSignature(signature) .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.sendMessage(request) } } } \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt index 0858c5f7c..7707d9fa4 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/TransactionApi.kt @@ -14,6 +14,7 @@ import com.codeinc.opencode.gen.transaction.v1.TransactionService.SubmitIntentRe import com.codeinc.opencode.gen.transaction.v1.TransactionService.SubmitIntentResponse import com.codeinc.opencode.gen.transaction.v1.TransactionService.VoidGiftCardRequest import com.codeinc.opencode.gen.transaction.v1.TransactionService.VoidGiftCardResponse +import com.codeinc.opencode.gen.transaction.v1.validate import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.annotations.OpenCodeManagedChannel import com.getcode.opencode.internal.network.core.GrpcApi @@ -25,6 +26,7 @@ import com.getcode.opencode.internal.network.extensions.sign import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey +import dev.bmcreations.protovalidate.orThrow import io.grpc.ManagedChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -100,6 +102,8 @@ class TransactionApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getIntentMetadata(request) } @@ -125,6 +129,8 @@ class TransactionApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getLimits(request) } @@ -143,6 +149,8 @@ class TransactionApi @Inject constructor( .setMint(mint.asSolanaAccountId()) .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.canWithdrawToAccount(request) } @@ -166,6 +174,8 @@ class TransactionApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.voidGiftCard(request) } @@ -187,6 +197,8 @@ class TransactionApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getSwap(request) } @@ -206,6 +218,8 @@ class TransactionApi @Inject constructor( .apply { setSignature(sign(owner)) } .build() + request.validate().orThrow() + return withContext(Dispatchers.IO) { api.getPendingSwaps(request) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/AccountService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/AccountService.kt index 66c95ce8b..b1acefdd1 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/AccountService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/AccountService.kt @@ -9,6 +9,7 @@ import com.getcode.opencode.model.accounts.AccountInfo import com.getcode.opencode.model.accounts.AccountResponse import com.getcode.opencode.model.core.errors.CodeAccountCheckError import com.getcode.opencode.model.core.errors.GetAccountsError +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.PublicKey import javax.inject.Inject @@ -32,7 +33,7 @@ internal class AccountService @Inject constructor( } }, onFailure = { cause -> - Result.failure(CodeAccountCheckError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { CodeAccountCheckError.Other(cause = it) }) } ) } @@ -73,7 +74,7 @@ internal class AccountService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetAccountsError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetAccountsError.Other(cause = it) }) } ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/CurrencyService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/CurrencyService.kt index 51e3a09e6..0e3094273 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/CurrencyService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/CurrencyService.kt @@ -16,9 +16,11 @@ import com.getcode.opencode.internal.network.streamers.LiveMintDataStreamer import com.getcode.opencode.internal.network.streamers.ManagedMintStream import com.getcode.opencode.model.core.errors.CheckTokenAvailabilityError import com.getcode.opencode.model.core.errors.DiscoverTokensError +import com.getcode.opencode.model.core.errors.GetAccountsError import com.getcode.opencode.model.core.errors.GetHistoricalMintDataError import com.getcode.opencode.model.core.errors.GetMintsError import com.getcode.opencode.model.core.errors.LaunchTokenError +import com.getcode.opencode.model.core.errors.SendMessageError import com.getcode.opencode.model.core.errors.UpdateIconError import com.getcode.opencode.model.core.errors.UpdateMetadataError import com.getcode.opencode.model.financial.CurrencyCode @@ -28,6 +30,7 @@ import com.getcode.opencode.model.financial.TokenCreateRequest import com.getcode.opencode.model.financial.TokenUpdateRequest import com.getcode.opencode.model.moderation.ModerationAttestation import com.getcode.opencode.model.ui.TokenBillCustomizations +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import kotlinx.coroutines.CoroutineScope @@ -60,7 +63,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetMintsError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetMintsError.Other(cause = it) }) } ) } @@ -91,7 +94,7 @@ internal class CurrencyService @Inject constructor( }, onFailure = { cause -> - Result.failure(GetHistoricalMintDataError.Other(cause)) + Result.failure(cause.toValidationOrElse { GetHistoricalMintDataError.Other(cause = it) }) } ) } @@ -130,7 +133,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(CheckTokenAvailabilityError.Other(cause)) + Result.failure(cause.toValidationOrElse { CheckTokenAvailabilityError.Other(cause = it) }) } ) } @@ -152,7 +155,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(LaunchTokenError.Other(cause)) + Result.failure(cause.toValidationOrElse { LaunchTokenError.Other(cause = it) }) } ) } @@ -174,7 +177,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(UpdateIconError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { UpdateIconError.Other(cause = it) }) } ) } @@ -195,7 +198,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(UpdateMetadataError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { UpdateMetadataError.Other(cause = it) }) } ) } @@ -217,7 +220,7 @@ internal class CurrencyService @Inject constructor( } }, onFailure = { cause -> - Result.failure(DiscoverTokensError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { SendMessageError.Other(cause = it) }) } ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/MessagingService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/MessagingService.kt index ecf160203..8dbd791db 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/MessagingService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/MessagingService.kt @@ -13,8 +13,10 @@ import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.getcode.opencode.internal.network.extensions.openMessageStreamRequest import com.getcode.opencode.internal.network.extensions.toPublicKey import com.getcode.opencode.model.core.errors.AckMessagesError +import com.getcode.opencode.model.core.errors.DiscoverTokensError import com.getcode.opencode.model.core.errors.PollMessagesError import com.getcode.opencode.model.core.errors.SendMessageError +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.PublicKey import com.getcode.utils.TraceType import com.getcode.utils.trace @@ -155,7 +157,7 @@ internal class MessagingService @Inject constructor( } }, onFailure = { error -> - Result.failure(PollMessagesError.Other(cause = error)) + Result.failure(error.toValidationOrElse { PollMessagesError.Other(cause = it) }) } ) } @@ -184,7 +186,7 @@ internal class MessagingService @Inject constructor( } }, onFailure = { error -> - Result.failure(SendMessageError.Other(cause = error)) + Result.failure(error.toValidationOrElse { SendMessageError.Other(cause = it) }) } ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/SwapService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/SwapService.kt index eb71b2d55..16edb06a1 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/SwapService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/SwapService.kt @@ -8,7 +8,9 @@ import com.getcode.opencode.internal.network.extensions.foldWithSuppression import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.core.errors.GetPendingSwapsError import com.getcode.opencode.model.core.errors.GetSwapError +import com.getcode.opencode.model.core.errors.SendMessageError import com.getcode.opencode.model.transactions.Swap +import com.getcode.opencode.utils.toValidationOrElse import javax.inject.Inject internal class SwapService @Inject constructor( @@ -38,7 +40,7 @@ internal class SwapService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetSwapError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetSwapError.Other(cause = it) }) } ) } @@ -58,7 +60,7 @@ internal class SwapService @Inject constructor( } }, onFailure = { cause -> - Result.failure(GetPendingSwapsError.Other(cause = cause)) + Result.failure(cause.toValidationOrElse { GetPendingSwapsError.Other(cause = it) }) } ) } \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt index 4c7c3098e..1e1cd0734 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt @@ -16,6 +16,7 @@ import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.model.core.errors.GetIntentMetadataError import com.getcode.opencode.model.core.errors.GetLimitsError +import com.getcode.opencode.model.core.errors.SendMessageError import com.getcode.opencode.model.core.errors.VoidGiftCardError import com.getcode.opencode.model.core.errors.WithdrawalAvailabilityError import com.getcode.opencode.model.financial.Limits @@ -28,6 +29,7 @@ import com.getcode.opencode.model.transactions.SwapStartKind import com.getcode.opencode.model.transactions.TransactionMetadata import com.getcode.opencode.model.transactions.WithdrawalAvailability import com.getcode.opencode.solana.intents.IntentType +import com.getcode.opencode.utils.toValidationOrElse import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import com.getcode.solana.keys.base58 @@ -80,7 +82,7 @@ internal class TransactionService @Inject constructor( } }, onFailure = { error -> - Result.failure(GetIntentMetadataError.Other(cause = error)) + Result.failure(error.toValidationOrElse { GetIntentMetadataError.Other(cause = it) }) } ) } @@ -112,7 +114,7 @@ internal class TransactionService @Inject constructor( } }, onFailure = { error -> - Result.failure(GetLimitsError.Other(cause = error)) + Result.failure(error.toValidationOrElse { GetLimitsError.Other(cause = it) }) } ) } @@ -138,7 +140,7 @@ internal class TransactionService @Inject constructor( Result.success(availability) }, onFailure = { error -> - Result.failure(WithdrawalAvailabilityError.Other(cause = error)) + Result.failure(error.toValidationOrElse { WithdrawalAvailabilityError.Other(cause = it) }) } ) } @@ -161,7 +163,7 @@ internal class TransactionService @Inject constructor( } }, onFailure = { error -> - Result.failure(VoidGiftCardError.Other(cause = error)) + Result.failure(error.toValidationOrElse { VoidGiftCardError.Other(cause = it) }) } ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/ValidationException.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/ValidationException.kt new file mode 100644 index 000000000..3405a43d8 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/ValidationException.kt @@ -0,0 +1,16 @@ +package com.getcode.opencode.model.core.errors + +import com.getcode.utils.CodeServerError + +class ValidationException( + val violations: List, + field: String? = null, +) : CodeServerError( + message = field?.let { "Validation failed for $it" } + ?: "Request validation failed: ${violations.joinToString { "${it.field}: ${it.message}" }}" +) + +data class Violation( + val field: String, + val message: String, +) \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/utils/Throwable.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/utils/Throwable.kt new file mode 100644 index 000000000..6191d1c76 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/utils/Throwable.kt @@ -0,0 +1,15 @@ +package com.getcode.opencode.utils + +import com.getcode.opencode.model.core.errors.ValidationException +import com.getcode.opencode.model.core.errors.Violation +import com.getcode.utils.CodeServerError +import dev.bmcreations.protovalidate.ProtoValidationException + +@Suppress("UNCHECKED_CAST") +fun Throwable.toValidationOrElse(fallback: (Throwable) -> E): E = + when (this) { + is ProtoValidationException -> ValidationException( + violations.map { Violation(field = it.field, message = it.message) } + ) as E + else -> fallback(this) + } diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/network/services/CurrencyServiceTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/network/services/CurrencyServiceTest.kt index 21f338356..444379e97 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/network/services/CurrencyServiceTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/network/services/CurrencyServiceTest.kt @@ -11,6 +11,9 @@ import com.getcode.opencode.internal.network.api.CurrencyApi import com.getcode.opencode.model.core.errors.LaunchTokenError import com.getcode.opencode.model.core.errors.UpdateIconError import com.getcode.opencode.model.core.errors.UpdateMetadataError +import com.getcode.opencode.model.core.errors.ValidationException +import dev.bmcreations.protovalidate.FieldViolation +import dev.bmcreations.protovalidate.ProtoValidationException import com.getcode.opencode.model.financial.TokenCreateRequest import com.getcode.opencode.model.financial.TokenUpdateRequest import com.getcode.opencode.model.moderation.ModerationAttestation @@ -19,6 +22,7 @@ import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -100,6 +104,20 @@ class CurrencyServiceTest { assertNotNull(error.cause) } + @Test + fun `launchNewToken ProtoValidationException returns ValidationException`() = runTest { + val violation = FieldViolation(field = "name", rule = "string.min_len", message = "too short") + coEvery { api.launchToken(any(), any()) } throws ProtoValidationException(listOf(violation)) + + val result = service.launchNewToken(makeCreateRequest(), owner) + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertIs(error) + assertEquals("name", error.violations.first().field) + assertEquals("too short", error.violations.first().message) + } + // endregion // region updateIcon @@ -160,6 +178,17 @@ class CurrencyServiceTest { assertNotNull(error.cause) } + @Test + fun `updateIcon ProtoValidationException returns ValidationException`() = runTest { + val violation = FieldViolation(field = "icon", rule = "bytes.min_len", message = "icon required") + coEvery { api.updateIcon(any(), any()) } throws ProtoValidationException(listOf(violation)) + + val result = service.updateIcon(makeIconRequest(), owner) + + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + // endregion // region updateMetadata diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/utils/ThrowableExtensionsTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/utils/ThrowableExtensionsTest.kt new file mode 100644 index 000000000..b42df5b24 --- /dev/null +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/utils/ThrowableExtensionsTest.kt @@ -0,0 +1,60 @@ +package com.getcode.opencode.utils + +import com.getcode.opencode.model.core.errors.ValidationException +import com.getcode.opencode.model.core.errors.Violation +import com.getcode.utils.CodeServerError +import dev.bmcreations.protovalidate.FieldViolation +import dev.bmcreations.protovalidate.ProtoValidationException +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class ThrowableExtensionsTest { + + private class FallbackError(cause: Throwable? = null) : CodeServerError("fallback", cause) + + @Test + fun `ProtoValidationException maps to ValidationException with violations`() { + val violations = listOf( + FieldViolation(field = "name", rule = "string.min_len", message = "too short"), + FieldViolation(field = "symbol", rule = "string.max_len", message = "too long"), + ) + val proto = ProtoValidationException(violations) + + val result: CodeServerError = proto.toValidationOrElse { FallbackError(it) } + + assertIs(result) + assertEquals(2, result.violations.size) + assertEquals("name", result.violations[0].field) + assertEquals("too short", result.violations[0].message) + assertEquals("symbol", result.violations[1].field) + assertEquals("too long", result.violations[1].message) + } + + @Test + fun `Non-ProtoValidationException falls through to fallback`() { + val exception = RuntimeException("network failure") + + val result: CodeServerError = exception.toValidationOrElse { FallbackError(it) } + + assertIs(result) + } + + @Test + fun `Violations are correctly mapped from FieldViolation to Violation`() { + val fieldViolation = FieldViolation( + field = "phone.value", + rule = "string.pattern", + message = "must match pattern", + ) + val proto = ProtoValidationException(listOf(fieldViolation)) + + val result: CodeServerError = proto.toValidationOrElse { FallbackError(it) } + + assertIs(result) + assertEquals( + listOf(Violation(field = "phone.value", message = "must match pattern")), + result.violations + ) + } +} From 74559fbc78faf764eee1ca3d18b0bdc312887348 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 22 Apr 2026 15:30:49 -0400 Subject: [PATCH 3/5] fix(services/ocp): correct assert for VirtualMachineProgram_TransferForSwapWithFee command resolution Signed-off-by: Brandon McAnsh --- .../internal/solana/programs/VirtualMachineProgramTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/VirtualMachineProgramTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/VirtualMachineProgramTest.kt index 26aab37c7..05134d460 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/VirtualMachineProgramTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/VirtualMachineProgramTest.kt @@ -107,7 +107,7 @@ class VirtualMachineProgramTest { feeDestination = testKey(7), swapAmount = 1000L, feeAmount = 500L, bump = 255 ) - assertEquals(17.toByte(), ix.encode()[0]) + assertEquals(20.toByte(), ix.encode()[0]) } @Test From 9912196392c7ac9abaf7611a7aeed7e3bd3149b3 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 22 Apr 2026 15:53:26 -0400 Subject: [PATCH 4/5] fix(tokens): correct SwapViewModelErrorTest mocking Signed-off-by: Brandon McAnsh --- .../kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt index 512d310bc..92c4341c5 100644 --- a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt +++ b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt @@ -98,7 +98,7 @@ class SwapViewModelErrorTest { fun `buy failure shows buySellFailed error`() = runTest(mainCoroutineRule.dispatcher) { dispatchers = TestDispatchers(testScheduler) transactionController.stub { - onBlocking { buy(any(), any(), anyOrNull(), any(), any(), anyOrNull()) } doReturn + onBlocking { buy(any(), any(), anyOrNull(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } doReturn Result.failure(RuntimeException("buy failed")) } From 585156914c982bc9a471b6828089f48fa94961f9 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 22 Apr 2026 16:09:54 -0400 Subject: [PATCH 5/5] feat(currencycreator): handle proto validation errors the same as flagged or denied Signed-off-by: Brandon McAnsh --- .../app/currencycreator/internal/CurrencyCreatorViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt index 637794124..be40e9520 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt @@ -41,6 +41,7 @@ import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.core.errors.CheckTokenAvailabilityError import com.getcode.opencode.model.core.errors.GetMintsError import com.getcode.opencode.model.core.errors.LaunchTokenError +import com.getcode.opencode.model.core.errors.ValidationException import com.getcode.opencode.model.financial.MintMetadata import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.TokenCreateRequest @@ -280,6 +281,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( onError = { cause -> dispatchEvent(Event.UpdateProcessingState()) when (cause) { + is ValidationException, is TextModerationError.Flagged, is TextModerationError.Denied -> { BottomBarManager.showAlert( @@ -330,6 +332,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( dispatchEvent(Event.UpdateProcessingState()) stateFlow.value.icon.dataOrNull?.let { contentReader.removeFromCache(it) } when (cause) { + is ValidationException, is ImageModerationError.Flagged, is ImageModerationError.Denied -> { BottomBarManager.showAlert( @@ -379,6 +382,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( onError = { cause -> dispatchEvent(Event.UpdateProcessingState()) when (cause) { + is ValidationException, is TextModerationError.Flagged, is TextModerationError.Denied -> { BottomBarManager.showAlert(