From d961cfd3c349bd69bac9bcc1914662a9b8f8dd79 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 14 Oct 2026 16:07:01 -0400 Subject: [PATCH 1/4] chore(protos): adopt PaymentAction, CashContent.receiver, exclude_per_mint_balances DmPayment.Action moved to a top-level PaymentAction. Map CashContent.receiver onto MessageContent.Cash and expose GetBalancesRequest.exclude_per_mint_balances on BalanceApi. --- .../internal/network/extensions/ProtobufToLocal.kt | 1 + .../com/flipcash/services/models/DmPaymentMetadata.kt | 4 ++-- .../com/flipcash/services/models/chat/MessageContent.kt | 2 ++ .../com/flipcash/services/models/DmPaymentMetadataTest.kt | 8 ++++---- .../getcode/opencode/internal/network/api/BalanceApi.kt | 4 ++++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt index ed1ce11b4b..cc00394f93 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt @@ -185,6 +185,7 @@ internal fun MessagingModel.Content.toMessageContent(): MessageContent { MessagingModel.CashContent.Verb.TIPPED -> MessageContent.Cash.Action.TIPPED else -> MessageContent.Cash.Action.SENT }, + receiver = if (cash.hasReceiver()) cash.receiver.value.toByteArray().toList() else null, ) MessagingModel.Content.TypeCase.REPLY -> MessageContent.Reply( repliedMessageId = reply.repliedMessageId.value, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt index aac38cad15..7470b9dad3 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt @@ -78,9 +78,9 @@ fun buildTipDmPaymentMetadata( .setAction( when (action) { TipAction.SEND -> - FlipcashIntentModel.ChatMetadata.DmPayment.Action.SEND + FlipcashIntentModel.PaymentAction.SEND TipAction.TIP -> - FlipcashIntentModel.ChatMetadata.DmPayment.Action.TIP + FlipcashIntentModel.PaymentAction.TIP } ) ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt index d8b86c1d60..659c508f42 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt @@ -14,6 +14,8 @@ sealed interface MessageContent { val tokenName: String = "", val tokenImageUrl: String = "", val action: Action = Action.SENT, + /** The group-payment recipient (`CashContent.receiver`); null for DM cash and older messages. */ + val receiver: ID? = null, ) : MessageContent { enum class Action { SENT, diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt index ffb85ca43a..757bc1d1c5 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt @@ -34,11 +34,11 @@ class DmPaymentMetadataTest { decoded.chat.dmPayment.location, ) assertEquals( - FlipcashIntentModel.ChatMetadata.DmPayment.Action.TIP, + FlipcashIntentModel.PaymentAction.TIP, decoded.chat.dmPayment.action, ) assertNotEquals( - FlipcashIntentModel.ChatMetadata.DmPayment.Action.DEFAULT, + FlipcashIntentModel.PaymentAction.DEFAULT, decoded.chat.dmPayment.action, ) assertEquals(chatId.bytes.toByteString(), decoded.chat.chatId.value) @@ -59,11 +59,11 @@ class DmPaymentMetadataTest { decoded.chat.dmPayment.location, ) assertEquals( - FlipcashIntentModel.ChatMetadata.DmPayment.Action.SEND, + FlipcashIntentModel.PaymentAction.SEND, decoded.chat.dmPayment.action, ) assertNotEquals( - FlipcashIntentModel.ChatMetadata.DmPayment.Action.DEFAULT, + FlipcashIntentModel.PaymentAction.DEFAULT, decoded.chat.dmPayment.action, ) } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt index a832dca409..ee07306725 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/BalanceApi.kt @@ -34,15 +34,19 @@ internal class BalanceApi @Inject constructor( * @param owners The owner accounts to fetch balance data for (min 1, max 1024). * @param mints Optional filter to limit the response to balances for these mints. * When empty, balances for all mints held by each owner are returned. + * @param excludePerMintBalances When true, `OwnerBalance.balances_by_mint` comes back empty; + * owner totals still cover every mint. * @return The [OcpBalanceService.GetBalancesResponse] */ suspend fun getBalances( owners: List, mints: List = emptyList(), + excludePerMintBalances: Boolean = false, ): OcpBalanceService.GetBalancesResponse { val request = OcpBalanceService.GetBalancesRequest.newBuilder() .addAllOwners(owners.map { it.asSolanaAccountId() }) .addAllMints(mints.map { it.asSolanaAccountId() }) + .setExcludePerMintBalances(excludePerMintBalances) .build() request.validate().orThrow() From e30f898c4e2fcaa39025174198f16321e53cf069 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 14 Oct 2026 16:07:02 -0400 Subject: [PATCH 2/4] feat(flipcash): scaffold Poll service (CreatePoll, GetPoll, GetPolls, Vote) --- .../services/controllers/PollController.kt | 40 +++++++ .../services/inject/FlipcashModule.kt | 8 ++ .../services/internal/network/api/PollApi.kt | 91 +++++++++++++++ .../internal/network/services/PollService.kt | 107 ++++++++++++++++++ .../repositories/InternalPollRepository.kt | 44 +++++++ .../com/flipcash/services/models/Errors.kt | 47 ++++++++ .../services/repository/PollRepository.kt | 24 ++++ 7 files changed, 361 insertions(+) create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/controllers/PollController.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PollApi.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PollService.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalPollRepository.kt create mode 100644 services/flipcash/src/main/kotlin/com/flipcash/services/repository/PollRepository.kt diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/PollController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/PollController.kt new file mode 100644 index 0000000000..604010d489 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/PollController.kt @@ -0,0 +1,40 @@ +package com.flipcash.services.controllers + +import com.codeinc.flipcash.gen.poll.v1.Model +import com.flipcash.services.models.chat.ChatId +import com.flipcash.services.repository.PollRepository +import com.flipcash.services.user.UserManager +import javax.inject.Inject + +class PollController @Inject constructor( + private val repository: PollRepository, + private val userManager: UserManager, +) { + suspend fun createPoll( + chatId: ChatId, + definition: Model.PollDefinition, + idempotencyKey: Model.IdempotencyKey, + ): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + return repository.createPoll(owner, chatId, definition, idempotencyKey) + } + + suspend fun getPoll(pollId: Model.PollId): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + return repository.getPoll(owner, pollId) + } + + suspend fun getPolls(pollIds: List): Result> { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + return repository.getPolls(owner, pollIds) + } + + suspend fun vote(pollId: Model.PollId, optionIndices: List): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + return repository.vote(owner, pollId, optionIndices) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt index ee5fefb67d..301f7f1110 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt @@ -27,6 +27,9 @@ import com.flipcash.services.internal.network.services.ChatMessagingService import com.flipcash.services.internal.network.services.EmailVerificationService import com.flipcash.services.internal.network.services.ContactListService import com.flipcash.services.internal.network.services.ModerationService +import com.flipcash.services.internal.network.services.PollService +import com.flipcash.services.internal.repositories.InternalPollRepository +import com.flipcash.services.repository.PollRepository import com.flipcash.services.internal.network.services.PhoneVerificationService import com.flipcash.services.internal.network.services.ProfileService import com.flipcash.services.internal.network.services.PurchaseService @@ -255,6 +258,11 @@ internal object FlipcashModule { ): ProfileRepository = InternalProfileRepository(service, userProfileMapper, socialAccountMapper) + @Provides + internal fun providesPollRepository( + service: PollService, + ): PollRepository = InternalPollRepository(service) + @Provides internal fun providesModerationRepository( service: ModerationService, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PollApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PollApi.kt new file mode 100644 index 0000000000..fa14b13aa5 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PollApi.kt @@ -0,0 +1,91 @@ +package com.flipcash.services.internal.network.api + +import com.codeinc.flipcash.gen.poll.v1.Model +import com.codeinc.flipcash.gen.poll.v1.PollGrpcKt +import com.codeinc.flipcash.gen.poll.v1.PollService +import com.codeinc.flipcash.gen.poll.v1.validate +import com.flipcash.services.internal.annotations.FlipcashManagedChannel +import com.flipcash.services.internal.network.extensions.asChatId +import com.flipcash.services.internal.network.extensions.authenticate +import com.flipcash.services.models.chat.ChatId +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 +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class PollApi @Inject constructor( + @FlipcashManagedChannel + managedChannel: ManagedChannel, +) : GrpcApi(managedChannel) { + private val api = PollGrpcKt.PollCoroutineStub(managedChannel) + .withWaitForReady() + + suspend fun createPoll( + owner: Ed25519.KeyPair, + chatId: ChatId, + definition: Model.PollDefinition, + idempotencyKey: Model.IdempotencyKey, + ): PollService.CreatePollResponse { + val request = PollService.CreatePollRequest.newBuilder() + .setChatId(chatId.asChatId()) + .setDefinition(definition) + .setIdempotencyKey(idempotencyKey) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.createPoll(request) + } + } + + suspend fun getPoll(owner: Ed25519.KeyPair, pollId: Model.PollId): PollService.GetPollResponse { + val request = PollService.GetPollRequest.newBuilder() + .setPollId(pollId) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.getPoll(request) + } + } + + suspend fun getPolls(owner: Ed25519.KeyPair, pollIds: List): PollService.GetPollsResponse { + val request = PollService.GetPollsRequest.newBuilder() + .addAllPollIds(pollIds) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.getPolls(request) + } + } + + suspend fun vote( + owner: Ed25519.KeyPair, + pollId: Model.PollId, + optionIndices: List, + ): PollService.VoteResponse { + val request = PollService.VoteRequest.newBuilder() + .setPollId(pollId) + .addAllOptionIndices(optionIndices) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.vote(request) + } + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PollService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PollService.kt new file mode 100644 index 0000000000..153369ada4 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/PollService.kt @@ -0,0 +1,107 @@ +package com.flipcash.services.internal.network.services + +import com.codeinc.flipcash.gen.poll.v1.Model +import com.codeinc.flipcash.gen.poll.v1.PollService +import com.flipcash.services.internal.network.api.PollApi +import com.flipcash.services.internal.network.extensions.toFlaggedCategory +import com.flipcash.services.models.CreatePollError +import com.flipcash.services.models.GetPollError +import com.flipcash.services.models.GetPollsError +import com.flipcash.services.models.VoteError +import com.flipcash.services.models.chat.ChatId +import com.getcode.ed25519.Ed25519 +import com.getcode.opencode.internal.network.extensions.foldWithSuppression +import com.getcode.opencode.utils.toValidationOrElse +import javax.inject.Inject + +internal class PollService @Inject constructor( + private val api: PollApi, +) { + suspend fun createPoll( + owner: Ed25519.KeyPair, + chatId: ChatId, + definition: Model.PollDefinition, + idempotencyKey: Model.IdempotencyKey, + ): Result { + return runCatching { + api.createPoll(owner, chatId, definition, idempotencyKey) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + PollService.CreatePollResponse.Result.OK -> Result.success(response.poll) + PollService.CreatePollResponse.Result.DENIED -> Result.failure(CreatePollError.Denied()) + PollService.CreatePollResponse.Result.CHAT_NOT_SUPPORTED -> Result.failure(CreatePollError.ChatNotSupported()) + PollService.CreatePollResponse.Result.ENCRYPTION_NOT_ALLOWED -> Result.failure(CreatePollError.EncryptionNotAllowed()) + PollService.CreatePollResponse.Result.ENCRYPTION_REQUIRED -> Result.failure(CreatePollError.EncryptionRequired()) + PollService.CreatePollResponse.Result.OPTION_COUNT_MISMATCH -> Result.failure(CreatePollError.OptionCountMismatch()) + PollService.CreatePollResponse.Result.INVALID_ENDS_AT -> Result.failure(CreatePollError.InvalidEndsAt()) + PollService.CreatePollResponse.Result.INVALID_BALANCE_PARAMETERS -> Result.failure(CreatePollError.InvalidBalanceParameters()) + PollService.CreatePollResponse.Result.TEXT_MODERATED -> Result.failure(CreatePollError.TextModerated(response.flaggedCategory.toFlaggedCategory())) + PollService.CreatePollResponse.Result.UNRECOGNIZED -> Result.failure(CreatePollError.Unrecognized()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { CreatePollError.Other(cause = it) }) + } + ) + } + + suspend fun getPoll(owner: Ed25519.KeyPair, pollId: Model.PollId): Result { + return runCatching { + api.getPoll(owner, pollId) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + PollService.GetPollResponse.Result.OK -> Result.success(response.poll) + PollService.GetPollResponse.Result.DENIED -> Result.failure(GetPollError.Denied()) + PollService.GetPollResponse.Result.NOT_FOUND -> Result.failure(GetPollError.NotFound()) + PollService.GetPollResponse.Result.UNRECOGNIZED -> Result.failure(GetPollError.Unrecognized()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { GetPollError.Other(cause = it) }) + } + ) + } + + suspend fun getPolls(owner: Ed25519.KeyPair, pollIds: List): Result> { + return runCatching { + api.getPolls(owner, pollIds) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + PollService.GetPollsResponse.Result.OK -> Result.success(response.pollsList) + PollService.GetPollsResponse.Result.DENIED -> Result.failure(GetPollsError.Denied()) + PollService.GetPollsResponse.Result.UNRECOGNIZED -> Result.failure(GetPollsError.Unrecognized()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { GetPollsError.Other(cause = it) }) + } + ) + } + + suspend fun vote( + owner: Ed25519.KeyPair, + pollId: Model.PollId, + optionIndices: List, + ): Result { + return runCatching { + api.vote(owner, pollId, optionIndices) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + PollService.VoteResponse.Result.OK -> Result.success(response.state) + PollService.VoteResponse.Result.DENIED -> Result.failure(VoteError.Denied()) + PollService.VoteResponse.Result.NOT_FOUND -> Result.failure(VoteError.NotFound()) + PollService.VoteResponse.Result.POLL_CLOSED -> Result.failure(VoteError.PollClosed()) + PollService.VoteResponse.Result.INVALID_SELECTION -> Result.failure(VoteError.InvalidSelection()) + PollService.VoteResponse.Result.UNRECOGNIZED -> Result.failure(VoteError.Unrecognized()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { VoteError.Other(cause = it) }) + } + ) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalPollRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalPollRepository.kt new file mode 100644 index 0000000000..ff3c08da78 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalPollRepository.kt @@ -0,0 +1,44 @@ +package com.flipcash.services.internal.repositories + +import com.codeinc.flipcash.gen.poll.v1.Model +import com.flipcash.services.internal.network.services.PollService +import com.flipcash.services.models.CreatePollError +import com.flipcash.services.models.GetPollError +import com.flipcash.services.models.VoteError +import com.flipcash.services.models.chat.ChatId +import com.flipcash.services.repository.PollRepository +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.utils.ErrorUtils + +internal class InternalPollRepository( + private val service: PollService, +) : PollRepository { + override suspend fun createPoll( + owner: KeyPair, + chatId: ChatId, + definition: Model.PollDefinition, + idempotencyKey: Model.IdempotencyKey, + ): Result { + return service.createPoll(owner, chatId, definition, idempotencyKey) + .onFailure { + if (it !is CreatePollError.Denied && it !is CreatePollError.TextModerated) ErrorUtils.handleError(it) + } + } + + override suspend fun getPoll(owner: KeyPair, pollId: Model.PollId): Result { + return service.getPoll(owner, pollId) + .onFailure { if (it !is GetPollError.NotFound) ErrorUtils.handleError(it) } + } + + override suspend fun getPolls(owner: KeyPair, pollIds: List): Result> { + return service.getPolls(owner, pollIds) + .onFailure { ErrorUtils.handleError(it) } + } + + override suspend fun vote(owner: KeyPair, pollId: Model.PollId, optionIndices: List): Result { + return service.vote(owner, pollId, optionIndices) + .onFailure { + if (it !is VoteError.PollClosed && it !is VoteError.NotFound) ErrorUtils.handleError(it) + } + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt index 701372122e..b2ee61d296 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt @@ -905,3 +905,50 @@ class BlobRejectedException(val rejection: BlobRejection) : // Thrown when a blob did not reach READY within the client's polling window. class BlobNotReadyException : Exception("Blob did not become ready in time") + +sealed class CreatePollError( + override val message: String? = null, + override val cause: Throwable? = null +): CodeServerError(message, cause) { + class Denied : CreatePollError("Denied") + class ChatNotSupported : CreatePollError("Chat does not support polls") + class EncryptionNotAllowed : CreatePollError("Encryption not allowed") + class EncryptionRequired : CreatePollError("Encryption required") + class OptionCountMismatch : CreatePollError("Option count mismatch") + class InvalidEndsAt : CreatePollError("Invalid end time") + class InvalidBalanceParameters : CreatePollError("Invalid balance parameters") + class TextModerated(val category: ModerationResult.FlaggedCategory) : CreatePollError("Poll text flagged: $category") + class Unrecognized : CreatePollError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : CreatePollError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class GetPollError( + override val message: String? = null, + override val cause: Throwable? = null +): CodeServerError(message, cause) { + class Denied : GetPollError("Denied") + class NotFound : GetPollError("Not found") + class Unrecognized : GetPollError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : GetPollError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class GetPollsError( + override val message: String? = null, + override val cause: Throwable? = null +): CodeServerError(message, cause) { + class Denied : GetPollsError("Denied") + class Unrecognized : GetPollsError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : GetPollsError(message = cause?.message, cause = cause), NotifiableError +} + +sealed class VoteError( + override val message: String? = null, + override val cause: Throwable? = null +): CodeServerError(message, cause) { + class Denied : VoteError("Denied") + class NotFound : VoteError("Not found") + class PollClosed : VoteError("Poll closed") + class InvalidSelection : VoteError("Invalid selection") + class Unrecognized : VoteError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : VoteError(message = cause?.message, cause = cause), NotifiableError +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/PollRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/PollRepository.kt new file mode 100644 index 0000000000..014d67b143 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/PollRepository.kt @@ -0,0 +1,24 @@ +package com.flipcash.services.repository + +import com.codeinc.flipcash.gen.poll.v1.Model +import com.flipcash.services.models.chat.ChatId +import com.getcode.ed25519.Ed25519.KeyPair + +/** + * Polls in group chats. Poll values are the generated `poll.v1` types for now; a domain model + * is deferred until a UI consumes them. + */ +interface PollRepository { + suspend fun createPoll( + owner: KeyPair, + chatId: ChatId, + definition: Model.PollDefinition, + idempotencyKey: Model.IdempotencyKey, + ): Result + + suspend fun getPoll(owner: KeyPair, pollId: Model.PollId): Result + + suspend fun getPolls(owner: KeyPair, pollIds: List): Result> + + suspend fun vote(owner: KeyPair, pollId: Model.PollId, optionIndices: List): Result +} From e40d3b4e0f223fee9a21555188da27c182c40f21 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 14 Oct 2026 17:14:25 -0400 Subject: [PATCH 3/4] chore(contract): bump ocp-client-protocol to 0.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d92d1fb291..764bdc9f8a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ protovalidate-kt = "0.1.3" # cadence and there is nothing to keep aligned. # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. -ocp-client-protocol = "0.6.0" +ocp-client-protocol = "0.7.0" flipcash2-client-protocol = "0.18.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's From 568dfcea98f87aad4006580481b0e3a0a8673ede Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 14 Oct 2026 17:14:25 -0400 Subject: [PATCH 4/4] chore(contract): bump flipcash2-client-protocol to 0.19.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 764bdc9f8a..765338d545 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,7 +69,7 @@ protovalidate-kt = "0.1.3" # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. ocp-client-protocol = "0.7.0" -flipcash2-client-protocol = "0.18.0" +flipcash2-client-protocol = "0.19.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's # `com.googlecode` artifact used to sit alongside it; the two ship separate copies of the metadata,