Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
syntax = "proto3";

package flipcash.contact.v1;

import "contact/v1/model.proto";
import "common/v1/common.proto";
import "phone/v1/model.proto";
import "validate/validate.proto";

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
option java_package = "com.codeinc.flipcash.gen.contact.v1";
option objc_class_prefix = "FPBContactV1";

// ContactList manages a user's contact list and surfaces which contacts are
// Flipcash users.
//
// Sync model:
// - The client maintains a 32-byte XOR-of-SHA256 checksum over its current
// contact set, and an OS-specific cursor for incremental change discovery.
// The cursor is local-only and never leaves the device.
// - Steady state: client computes a delta from the OS cursor, sends it via
// DeltaUpload with old/new checksums for compare-and-swap.
// - Recovery: on first install, after OS history truncation, or after
// CHECKSUM_DRIFT, the client uses FullUpload to replace the server's state
// wholesale.
service ContactList {
// CheckSync compares the client's checksum to the server's. Cheap, used
// on app foreground to decide whether any upload is needed.
rpc CheckSync(CheckSyncRequest) returns (CheckSyncResponse);

// DeltaUpload applies a delta under compare-and-swap on the checksum.
// Safe to retry indefinitely with the same payload.
rpc DeltaUpload(DeltaUploadRequest) returns (DeltaUploadResponse);

// FullUpload replaces the user's contact set entirely. Used when
// a delta cannot be constructed or CHECKSUM_DRIFT was returned.
rpc FullUpload(stream FullUploadRequest) returns (FullUploadResponse);

// GetFlipcashContacts gets the set of contacts that are on Flipcash
rpc GetFlipcashContacts(GetFlipcashContactsRequest) returns (stream GetFlipcashContactsResponse);
}


message CheckSyncRequest {
common.v1.Auth auth = 1 [(validate.rules).message.required = true];

// XOR-of-SHA256 over the client's current set of normalized E.164 phones.
common.v1.Hash client_checksum = 2 [(validate.rules).message.required = true];
}

message CheckSyncResponse {
enum Result {
OK = 0;
DENIED = 1;
OUT_OF_SYNC = 2;
}
Result result = 1;

// Authoritative server-side checksum. Clients persist this and use it
// as the basis for the next DeltaUpload.old_checksum.
common.v1.Hash server_checksum = 2;
}

message DeltaUploadRequest {
common.v1.Auth auth = 1 [(validate.rules).message.required = true];

repeated phone.v1.PhoneNumber adds = 2 [(validate.rules).repeated.max_items = 1000];

repeated phone.v1.PhoneNumber removes = 3 [(validate.rules).repeated.max_items = 1000];

// The checksum the client expected the server to have *before* applying
// this delta. Server applies only if stored == old_checksum.
common.v1.Hash old_checksum = 4 [(validate.rules).message.required = true];

// The checksum the client computes for the state *after* applying this
// delta. Server persists this on success. Used to detect retries: if
// stored == new_checksum, the server treats the request as a no-op.
common.v1.Hash new_checksum = 5 [(validate.rules).message.required = true];
}

message DeltaUploadResponse {
enum Result {
OK = 0;
DENIED = 1;
// Server's recomputed checksum did not match expected_checksum.
CHECKSUM_MISMATCH = 2;
// Stored checksum matched neither old_checksum nor new_checksum.
// Client should call FullUpload to reconcile.
CHECKSUM_DRIFT = 3;
TOO_MANY_CONTACTS = 4;
}
Result result = 1;
}

message FullUploadRequest {
common.v1.Auth auth = 1 [(validate.rules).message.required = true];

// The complete current contact set. Server replaces stored state with
// this list in one transaction.
repeated phone.v1.PhoneNumber phones = 2 [(validate.rules).repeated.max_items = 1000];

// XOR-of-SHA256 over the client's current set of normalized E.164 phones.
// Sent on the last streamed request to indicate the end of the upload.
common.v1.Hash expected_checksum = 3 [(validate.rules).message.required = true];
}

message FullUploadResponse {
enum Result {
OK = 0;
DENIED = 1;
// Server's recomputed checksum did not match expected_checksum.
CHECKSUM_MISMATCH = 2;
TOO_MANY_CONTACTS = 3;
}
Result result = 1;
}

message GetFlipcashContactsRequest {
common.v1.Auth auth = 1 [(validate.rules).message.required = true];

common.v1.Hash checksum = 2 [(validate.rules).message.required = true];
}

message GetFlipcashContactsResponse {
enum Result {
OK = 0;
DENIED = 1;
NOT_FOUND = 2;
// Server checksum doesn't match client checksum.
CHECKSUM_DRIFT = 3;
}
Result result = 1;

repeated FlipcashContact contacts = 2 [(validate.rules).repeated.max_items = 1000];
}
14 changes: 14 additions & 0 deletions definitions/flipcash/protos/src/main/proto/contact/v1/model.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";

package flipcash.contact.v1;

import "phone/v1/model.proto";
import "validate/validate.proto";

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
option java_package = "com.codeinc.flipcash.gen.contact.v1";
option objc_class_prefix = "FPBContactV1";

message FlipcashContact {
phone.v1.PhoneNumber phone = 1 [(validate.rules).message.required = true];
}
31 changes: 31 additions & 0 deletions definitions/flipcash/protos/src/main/proto/resolver/v1/model.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
syntax = "proto3";

package flipcash.resolver.v1;

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/resolver/v1;resolverpb";
option java_package = "com.codeinc.flipcash.gen.resolver.v1";
option objc_class_prefix = "FPBResolverV1";

import "common/v1/common.proto";
import "phone/v1/model.proto";
import "validate/validate.proto";

// Identifier wraps a real-world identifier that can be resolved to a
// payment destination address.
message Identifier {
oneof kind {
option (validate.required) = true;

phone.v1.PhoneNumber phone = 1;
}
}

// Resolution contains a payment destiation address mapping for an
// Identifier
message Resolution {
oneof kind {
option (validate.required) = true;

common.v1.PublicKey address = 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
syntax = "proto3";

package flipcash.resolver.v1;

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/resolver/v1;resolverpb";
option java_package = "com.codeinc.flipcash.gen.resolver.v1";
option objc_class_prefix = "FPBResolverV1";

import "common/v1/common.proto";
import "resolver/v1/model.proto";
import "validate/validate.proto";

// Resolver maps a real-world identifier (phone number, etc.) to a payment
// destination address.
service Resolver {
// Resolve looks up the payment destination address for the given identifier.
rpc Resolve(ResolveRequest) returns (ResolveResponse);
}

message ResolveRequest {
common.v1.Auth auth = 1 [(validate.rules).message.required = true];

Identifier identifier = 2 [(validate.rules).message.required = true];
}

message ResolveResponse {
Result result = 1;
enum Result {
OK = 0;
NOT_FOUND = 1;
DENIED = 2;
}

// The resolved payment destination address. Set when result == OK.
Resolution resolution = 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ typealias Seed16 = Key16

typealias Seed32 = Key32
typealias Hash = Key32
typealias Checksum = Key32

typealias PrivateKey = Key64

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.flipcash.services.controllers

import com.flipcash.services.models.ContactMethod
import com.flipcash.services.repository.ContactListRepository
import com.flipcash.services.user.UserManager
import com.getcode.solana.keys.Checksum
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class ContactListController @Inject constructor(
private val repository: ContactListRepository,
private val userManager: UserManager,
) {
suspend fun checkSync(clientChecksum: Checksum): Result<Checksum> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.checkSync(owner, clientChecksum)
}

suspend fun deltaUpload(
adds: List<ContactMethod.Phone>,
removes: List<ContactMethod.Phone>,
oldChecksum: Checksum,
newChecksum: Checksum,
): Result<Unit> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.deltaUpload(owner, adds, removes, oldChecksum, newChecksum)
}

suspend fun fullUpload(
phones: Flow<List<ContactMethod.Phone>>,
expectedChecksum: Checksum,
): Result<Unit> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.fullUpload(owner, phones, expectedChecksum)
}

fun getFlipcashContacts(checksum: Checksum): Flow<Result<List<ContactMethod.Phone>>> {
val owner = userManager.accountCluster?.authority?.keyPair
?: throw IllegalStateException("No account cluster in UserManager")
return repository.getFlipcashContacts(owner, checksum)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.flipcash.services.controllers

import com.flipcash.services.models.ContactMethod
import com.flipcash.services.repository.ResolverRepository
import com.flipcash.services.user.UserManager
import com.getcode.solana.keys.PublicKey
import javax.inject.Inject

class ResolverController @Inject constructor(
private val repository: ResolverRepository,
private val userManager: UserManager,
) {
suspend fun resolve(phone: ContactMethod.Phone): Result<PublicKey> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.resolve(owner, phone)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,35 @@ import com.flipcash.services.internal.domain.UserProfileMapper
import com.flipcash.services.internal.network.services.AccountService
import com.flipcash.services.internal.network.services.ActivityFeedService
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.PhoneVerificationService
import com.flipcash.services.internal.network.services.ProfileService
import com.flipcash.services.internal.network.services.PurchaseService
import com.flipcash.services.internal.network.services.PushService
import com.flipcash.services.internal.network.services.ResolverService
import com.flipcash.services.internal.network.services.SettingsService
import com.flipcash.services.internal.network.services.ThirdPartyService
import com.flipcash.services.internal.repositories.InternalAccountRepository
import com.flipcash.services.internal.repositories.InternalActivityFeedRepository
import com.flipcash.services.internal.repositories.InternalContactListRepository
import com.flipcash.services.internal.repositories.InternalContactVerificationRepository
import com.flipcash.services.internal.repositories.InternalModerationRepository
import com.flipcash.services.internal.repositories.InternalProfileRepository
import com.flipcash.services.internal.repositories.InternalPurchaseRepository
import com.flipcash.services.internal.repositories.InternalPushRepository
import com.flipcash.services.internal.repositories.InternalResolverRepository
import com.flipcash.services.internal.repositories.InternalSettingsRepository
import com.flipcash.services.internal.repositories.InternalThirdPartyRepository
import com.flipcash.services.repository.AccountRepository
import com.flipcash.services.repository.ActivityFeedRepository
import com.flipcash.services.repository.ContactListRepository
import com.flipcash.services.repository.ContactVerificationRepository
import com.flipcash.services.repository.ModerationRepository
import com.flipcash.services.repository.ProfileRepository
import com.flipcash.services.repository.PurchaseRepository
import com.flipcash.services.repository.PushRepository
import com.flipcash.services.repository.ResolverRepository
import com.flipcash.services.repository.SettingsRepository
import com.flipcash.services.repository.ThirdPartyRepository
import com.getcode.opencode.ProtocolConfig
Expand Down Expand Up @@ -108,6 +114,16 @@ internal object FlipcashModule {
}
}

@Provides
internal fun providesContactListRepository(
service: ContactListService,
): ContactListRepository = InternalContactListRepository(service)

@Provides
internal fun providesResolverRepository(
service: ResolverService,
): ResolverRepository = InternalResolverRepository(service)

@Provides
internal fun providesAccountRepository(
service: AccountService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.flipcash.services.internal.extensions

import com.getcode.solana.keys.Checksum
import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.PublicKey

internal fun ByteArray.toHash(): com.getcode.solana.keys.Hash {
return com.getcode.solana.keys.Hash(this.toList())
}

internal fun ByteArray.toChecksum(): Checksum {
return Checksum(this.toList())
}

internal fun ByteArray.toPublicKey(): PublicKey {
return PublicKey(this.toList())
}
Expand Down
Loading
Loading