From 54fd51e7c1df711ddda3479f3871189cd97100f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 28 Apr 2025 12:38:02 -0600 Subject: [PATCH 01/19] feat(core): add ContactsRepository with persist, find and update methods --- core/api/src/domain/errors.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/api/src/domain/errors.ts b/core/api/src/domain/errors.ts index bd1307394e..16ec90f8c3 100644 --- a/core/api/src/domain/errors.ts +++ b/core/api/src/domain/errors.ts @@ -73,6 +73,9 @@ export class CouldNotFindMerchantFromIdError extends CouldNotFindError {} export class CouldNotFindAccountFromPhoneError extends CouldNotFindError {} export class CouldNotFindTransactionsForAccountError extends CouldNotFindError {} export class CouldNotFindAccountFromKratosIdError extends CouldNotFindError {} +export class CouldNotFindContactFromAccountIdError extends CouldNotFindError {} +export class CouldNotFindContactFromContactIdError extends CouldNotFindError {} +export class CouldNotUpdateContactError extends RepositoryError {} export class QuizAlreadyPresentError extends DomainError {} From ebf7f835edd04ef3c455aaed25f3c4c51d758397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 30 Apr 2025 12:56:44 -0600 Subject: [PATCH 02/19] feat: graphql accountContactUpsert mutation, initial version --- apps/consent/app/graphql/generated.ts | 20 +++++++ apps/map/services/galoy/graphql/generated.ts | 20 +++++++ apps/pay/lib/graphql/generated.ts | 20 +++++++ core/api/src/app/contacts/contact-upsert.ts | 35 ++++++++++++ core/api/src/app/contacts/index.ts | 1 + core/api/src/app/index.ts | 3 + .../root/mutation/account-contact-upsert.ts | 56 +++++++++++++++++++ core/api/src/graphql/public/schema.graphql | 10 ++++ .../types/object/account-contact-upsert.ts | 39 +++++++++++++ .../types/payload/account-contact-upsert.ts | 18 ++++++ .../shared/types/scalar/contact-identifier.ts | 30 ++++++++++ .../apollo-federation/supergraph.graphql | 13 +++++ 12 files changed, 265 insertions(+) create mode 100644 core/api/src/app/contacts/contact-upsert.ts create mode 100644 core/api/src/app/contacts/index.ts create mode 100644 core/api/src/graphql/public/root/mutation/account-contact-upsert.ts create mode 100644 core/api/src/graphql/public/types/object/account-contact-upsert.ts create mode 100644 core/api/src/graphql/public/types/payload/account-contact-upsert.ts create mode 100644 core/api/src/graphql/shared/types/scalar/contact-identifier.ts diff --git a/apps/consent/app/graphql/generated.ts b/apps/consent/app/graphql/generated.ts index ccd48d06bf..72e0db63ca 100644 --- a/apps/consent/app/graphql/generated.ts +++ b/apps/consent/app/graphql/generated.ts @@ -44,6 +44,8 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } + /** Unique value used to identify a contact (e.g., username or lnAddress) */ + Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -141,6 +143,12 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; +export type AccountContactUpsertInput = { + readonly alias?: InputMaybe; + readonly identifier?: InputMaybe; + readonly type: ContactType; +}; + export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -458,6 +466,12 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; +export type ContactUpdateOrCreatePayload = { + readonly __typename: 'ContactUpdateOrCreatePayload'; + readonly contact?: Maybe; + readonly errors: ReadonlyArray; +}; + export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -929,6 +943,7 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; + readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1058,6 +1073,11 @@ export type Mutation = { }; +export type MutationAccountContactUpsertArgs = { + input: AccountContactUpsertInput; +}; + + export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; diff --git a/apps/map/services/galoy/graphql/generated.ts b/apps/map/services/galoy/graphql/generated.ts index a8ed2e98cd..523d217da2 100644 --- a/apps/map/services/galoy/graphql/generated.ts +++ b/apps/map/services/galoy/graphql/generated.ts @@ -44,6 +44,8 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } + /** Unique value used to identify a contact (e.g., username or lnAddress) */ + Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -141,6 +143,12 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; +export type AccountContactUpsertInput = { + readonly alias?: InputMaybe; + readonly identifier?: InputMaybe; + readonly type: ContactType; +}; + export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -458,6 +466,12 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; +export type ContactUpdateOrCreatePayload = { + readonly __typename: 'ContactUpdateOrCreatePayload'; + readonly contact?: Maybe; + readonly errors: ReadonlyArray; +}; + export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -929,6 +943,7 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; + readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1058,6 +1073,11 @@ export type Mutation = { }; +export type MutationAccountContactUpsertArgs = { + input: AccountContactUpsertInput; +}; + + export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; diff --git a/apps/pay/lib/graphql/generated.ts b/apps/pay/lib/graphql/generated.ts index 3e75846c50..6bfcb36cb0 100644 --- a/apps/pay/lib/graphql/generated.ts +++ b/apps/pay/lib/graphql/generated.ts @@ -44,6 +44,8 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } + /** Unique value used to identify a contact (e.g., username or lnAddress) */ + Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -142,6 +144,12 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; +export type AccountContactUpsertInput = { + readonly alias?: InputMaybe; + readonly identifier?: InputMaybe; + readonly type: ContactType; +}; + export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -459,6 +467,12 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; +export type ContactUpdateOrCreatePayload = { + readonly __typename: 'ContactUpdateOrCreatePayload'; + readonly contact?: Maybe; + readonly errors: ReadonlyArray; +}; + export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -930,6 +944,7 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; + readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1059,6 +1074,11 @@ export type Mutation = { }; +export type MutationAccountContactUpsertArgs = { + input: AccountContactUpsertInput; +}; + + export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; diff --git a/core/api/src/app/contacts/contact-upsert.ts b/core/api/src/app/contacts/contact-upsert.ts new file mode 100644 index 0000000000..00c969b425 --- /dev/null +++ b/core/api/src/app/contacts/contact-upsert.ts @@ -0,0 +1,35 @@ +import { CouldNotFindContactFromAccountIdError } from "@/domain/errors" +import { ContactsRepository } from "@/services/mongoose" + +export const upserContact = async ({ + accountId, + identifier, + alias, + type, +}: { + accountId: AccountId + identifier: string + type: ContactType + alias: string +}): Promise => { + const contactsRepo = ContactsRepository() + + const existing = await contactsRepo.findContact({ accountId, identifier }) + if (existing instanceof CouldNotFindContactFromAccountIdError) { + return contactsRepo.persistNew({ + accountId, + identifier, + type, + alias, + transactionsCount: 1, + }) + } + + if (existing instanceof Error) return existing + + return contactsRepo.update({ + ...existing, + alias, + transactionsCount: existing.transactionsCount + 1, + }) +} diff --git a/core/api/src/app/contacts/index.ts b/core/api/src/app/contacts/index.ts new file mode 100644 index 0000000000..f633d4534c --- /dev/null +++ b/core/api/src/app/contacts/index.ts @@ -0,0 +1 @@ +export * from "./contact-upsert" diff --git a/core/api/src/app/index.ts b/core/api/src/app/index.ts index bf7b93c005..9634128e1b 100644 --- a/core/api/src/app/index.ts +++ b/core/api/src/app/index.ts @@ -3,6 +3,7 @@ import * as AuthenticationMod from "./authentication" import * as AdminMod from "./admin" import * as CallbackMod from "./callback" import * as CommMod from "./comm" +import * as ContactsMod from "./contacts" import * as QuizMod from "./quiz" import * as LightningMod from "./lightning" import * as OnChainMod from "./on-chain" @@ -22,6 +23,7 @@ const allFunctions = { Admin: { ...AdminMod }, Callback: { ...CallbackMod }, Comm: { ...CommMod }, + Contacts: { ...ContactsMod }, Quiz: { ...QuizMod }, Lightning: { ...LightningMod }, OnChain: { ...OnChainMod }, @@ -53,6 +55,7 @@ export const { Admin, Callback, Comm, + Contacts, Quiz, Lightning, OnChain, diff --git a/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts b/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts new file mode 100644 index 0000000000..3546e7c6e0 --- /dev/null +++ b/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts @@ -0,0 +1,56 @@ +import AccountContactUpsertPayload from "@/graphql/public/types/payload/account-contact-upsert" +import ContactIdentifier from "@/graphql/shared/types/scalar/contact-identifier" +import ContactAlias from "@/graphql/public/types/scalar/contact-alias" +import ContactType from "@/graphql/shared/types/scalar/contact-type" +import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" +import { GT } from "@/graphql/index" + +import { Contacts } from "@/app" + +const AccountContactUpsertInput = GT.Input({ + name: "AccountContactUpsertInput", + fields: () => ({ + identifier: { type: ContactIdentifier }, + alias: { type: ContactAlias }, + type: { type: GT.NonNull(ContactType) }, + }), +}) + +const AccountContactUpsertMutation = GT.Field({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountContactUpsertPayload), + args: { + input: { type: GT.NonNull(AccountContactUpsertInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { identifier, alias, type } = args.input + + if (type instanceof Error) { + return { errors: [{ message: type.message }] } + } + + if (alias instanceof Error) { + return { errors: [{ message: alias.message }] } + } + + const result = await Contacts.upserContact({ + accountId: domainAccount.id, + identifier, + alias, + type, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { + errors: [], + contact: result, + } + }, +}) + +export default AccountContactUpsertMutation diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index a9418e8ec1..9d2b434ab3 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -43,6 +43,12 @@ interface Account { wallets: [Wallet!]! } +input AccountContactUpsertInput { + alias: ContactAlias + identifier: Identifier + type: ContactType! +} + type AccountDeletePayload { errors: [Error!]! success: Boolean! @@ -496,6 +502,9 @@ type GraphQLApplicationError implements Error { """Hex-encoded string of 32 bytes""" scalar Hex32Bytes +"""Unique value used to identify a contact (e.g., username or lnAddress)""" +scalar Identifier + union InitiationVia = InitiationViaIntraLedger | InitiationViaLn | InitiationViaOnChain type InitiationViaIntraLedger { @@ -922,6 +931,7 @@ type MobileVersions { } type Mutation { + accountContactUpsert(input: AccountContactUpsertInput!): ContactUpdateOrCreatePayload! accountDelete: AccountDeletePayload! accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! diff --git a/core/api/src/graphql/public/types/object/account-contact-upsert.ts b/core/api/src/graphql/public/types/object/account-contact-upsert.ts new file mode 100644 index 0000000000..6d1f39f5a8 --- /dev/null +++ b/core/api/src/graphql/public/types/object/account-contact-upsert.ts @@ -0,0 +1,39 @@ +import ContactId from "@/graphql/shared/types/scalar/contact-id" +import ContactType from "@/graphql/shared/types/scalar/contact-type" +import Identifier from "@/graphql/shared/types/scalar/contact-identifier" +import ContactAlias from "@/graphql/public/types/scalar/contact-alias" +import Timestamp from "@/graphql/shared/types/scalar/timestamp" +import { GT } from "@/graphql/index" + +const Contact = GT.Object({ + name: "Contact", + fields: () => ({ + id: { + type: GT.NonNull(ContactId), + description: "ID of the contact user or external identifier.", + }, + type: { + type: GT.NonNull(ContactType), + description: "Type of the contact (intraledger, lnaddress, etc.).", + }, + identifier: { + type: GT.NonNull(Identifier), + description: "Username or lnAddress that identifies the contact.", + }, + alias: { + type: ContactAlias, + description: "Alias name the user assigns to the contact.", + }, + transactionsCount: { + type: GT.NonNull(GT.Int), + description: "Total number of transactions with this contact.", + }, + createdAt: { + type: GT.NonNull(Timestamp), + description: + "Unix timestamp (number of seconds elapsed since January 1, 1970 00:00:00 UTC)", + }, + }), +}) + +export default Contact diff --git a/core/api/src/graphql/public/types/payload/account-contact-upsert.ts b/core/api/src/graphql/public/types/payload/account-contact-upsert.ts new file mode 100644 index 0000000000..1f8496a4a7 --- /dev/null +++ b/core/api/src/graphql/public/types/payload/account-contact-upsert.ts @@ -0,0 +1,18 @@ +import Contact from "../object/account-contact-upsert" + +import IError from "@/graphql/shared/types/abstract/error" +import { GT } from "@/graphql/index" + +const ContactUpdateOrCreatePayload = GT.Object({ + name: "ContactUpdateOrCreatePayload", + fields: () => ({ + errors: { + type: GT.NonNullList(IError), + }, + contact: { + type: Contact, + }, + }), +}) + +export default ContactUpdateOrCreatePayload diff --git a/core/api/src/graphql/shared/types/scalar/contact-identifier.ts b/core/api/src/graphql/shared/types/scalar/contact-identifier.ts new file mode 100644 index 0000000000..ef3a9b2e26 --- /dev/null +++ b/core/api/src/graphql/shared/types/scalar/contact-identifier.ts @@ -0,0 +1,30 @@ +import { checkedToIdentifier } from "@/domain/contacts" +import { InputValidationError } from "@/graphql/error" +import { GT } from "@/graphql/index" + +const Identifier = GT.Scalar({ + name: "Identifier", + description: "Unique value used to identify a contact (e.g., username or lnAddress)", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ message: "Invalid type for Identifier" }) + } + return validIdentifierValue(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validIdentifierValue(ast.value) + } + return new InputValidationError({ message: "Invalid type for Identifier" }) + }, +}) + +function validIdentifierValue(value: string): string | InputValidationError { + const checked = checkedToIdentifier(value) + if (checked instanceof Error) { + return new InputValidationError({ message: "Invalid value for Identifier" }) + } + return checked +} + +export default Identifier diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index af3cfad19a..dffdfa64eb 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -68,6 +68,14 @@ interface Account wallets: [Wallet!]! } +input AccountContactUpsertInput + @join__type(graph: PUBLIC) +{ + alias: ContactAlias + identifier: Identifier + type: ContactType! +} + type AccountDeletePayload @join__type(graph: PUBLIC) { @@ -721,6 +729,10 @@ enum Icon REFRESH @join__enumValue(graph: NOTIFICATIONS) } +"""Unique value used to identify a contact (e.g., username or lnAddress)""" +scalar Identifier + @join__type(graph: PUBLIC) + union InitiationVia @join__type(graph: PUBLIC) @join__unionMember(graph: PUBLIC, member: "InitiationViaIntraLedger") @@ -1275,6 +1287,7 @@ type Mutation apiKeyCreate(input: ApiKeyCreateInput!): ApiKeyCreatePayload! @join__field(graph: API_KEYS) apiKeyRevoke(input: ApiKeyRevokeInput!): ApiKeyRevokePayload! @join__field(graph: API_KEYS) statefulNotificationAcknowledge(input: StatefulNotificationAcknowledgeInput!): StatefulNotificationAcknowledgePayload! @join__field(graph: NOTIFICATIONS) + accountContactUpsert(input: AccountContactUpsertInput!): ContactUpdateOrCreatePayload! @join__field(graph: PUBLIC) accountDelete: AccountDeletePayload! @join__field(graph: PUBLIC) accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) From ce36e1a8dbb294080a65b108889883378980bf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 30 Apr 2025 15:39:16 -0600 Subject: [PATCH 03/19] chore: file to updated and add contact renamed --- core/api/src/app/contacts/index.ts | 2 +- .../src/app/contacts/{contact-upsert.ts => upsert-contact.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename core/api/src/app/contacts/{contact-upsert.ts => upsert-contact.ts} (100%) diff --git a/core/api/src/app/contacts/index.ts b/core/api/src/app/contacts/index.ts index f633d4534c..d9f1040ad5 100644 --- a/core/api/src/app/contacts/index.ts +++ b/core/api/src/app/contacts/index.ts @@ -1 +1 @@ -export * from "./contact-upsert" +export * from "./upsert-contact" diff --git a/core/api/src/app/contacts/contact-upsert.ts b/core/api/src/app/contacts/upsert-contact.ts similarity index 100% rename from core/api/src/app/contacts/contact-upsert.ts rename to core/api/src/app/contacts/upsert-contact.ts From 944a87f7fe281619f6e15394919f20be72846afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 7 May 2025 13:17:02 -0600 Subject: [PATCH 04/19] refactor: move contact storage from accounts collection to dedicated contacts collection --- bats/helpers/user.bash | 25 +++++++++++++ core/api/src/app/contacts/index.ts | 1 + .../contacts/upsert-intraledger-contact.ts | 37 +++++++++++++++++++ core/api/src/app/payments/send-lightning.ts | 1 + 4 files changed, 64 insertions(+) create mode 100644 core/api/src/app/contacts/upsert-intraledger-contact.ts diff --git a/bats/helpers/user.bash b/bats/helpers/user.bash index 7e79c28323..52e7ff23fd 100644 --- a/bats/helpers/user.bash +++ b/bats/helpers/user.bash @@ -134,6 +134,7 @@ ensure_username_is_present() { is_contact() { local owner_token_name="$1" +<<<<<<< HEAD local other_token_or_handle="$2" local contact_handle @@ -156,4 +157,28 @@ is_contact() { match=$(graphql_output ".data.me.contacts[] | select(.username == \"$contact_handle\")") [[ -n "$match" ]] +======= + local other_token_name="$2" + + local owner_account_id + owner_account_id=$(read_value "$owner_token_name.account_id") + [[ -n "$owner_account_id" ]] || return 1 + + local contact_identifier + contact_identifier=$(read_value "$other_token_name.username") + [[ -n "$contact_identifier" ]] || return 1 + + local mongo_query + mongo_query=$(echo "db.contacts.findOne( + { + accountId: \"$owner_account_id\", + identifier: \"$contact_identifier\" + } + );" | tr -d '[:space:]') + + local result + result=$(mongo_cli "$mongo_query" 2>&1) + + [[ "$result" != "null" && -n "$result" ]] +>>>>>>> e8ca37ca6 (refactor: move contact storage from accounts collection to dedicated contacts collection) } diff --git a/core/api/src/app/contacts/index.ts b/core/api/src/app/contacts/index.ts index d9f1040ad5..bee13e4f88 100644 --- a/core/api/src/app/contacts/index.ts +++ b/core/api/src/app/contacts/index.ts @@ -1 +1,2 @@ export * from "./upsert-contact" +export * from "./upsert-intraledger-contact" diff --git a/core/api/src/app/contacts/upsert-intraledger-contact.ts b/core/api/src/app/contacts/upsert-intraledger-contact.ts new file mode 100644 index 0000000000..3d89d02de7 --- /dev/null +++ b/core/api/src/app/contacts/upsert-intraledger-contact.ts @@ -0,0 +1,37 @@ +import { upserContact } from "./upsert-contact" + +import { ContactType } from "@/domain/contacts" + +export const upsertIntraledgerContacts = async ({ + senderAccount, + recipientAccount, +}: { + senderAccount: Account + recipientAccount: Account +}): Promise => { + if (!(senderAccount.contactEnabled && recipientAccount.contactEnabled)) { + return true + } + + if (recipientAccount.username) { + const contactToPayerResult = await upserContact({ + accountId: senderAccount.id, + identifier: recipientAccount.username, + alias: recipientAccount.username, + type: ContactType.IntraLedger, + }) + if (contactToPayerResult instanceof Error) return contactToPayerResult + } + + if (senderAccount.username) { + const contactToPayeeResult = await upserContact({ + accountId: recipientAccount.id, + identifier: senderAccount.username, + alias: senderAccount.username, + type: ContactType.IntraLedger, + }) + if (contactToPayeeResult instanceof Error) return contactToPayeeResult + } + + return true +} diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 57dea4ad31..3a5eb72a6e 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -67,6 +67,7 @@ import { checkWithdrawalLimits, createIntraledgerContact, } from "@/app/accounts" +import { upsertIntraledgerContacts } from "@/app/contacts" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" import { getTransactionForWalletByJournalId, From d4211b6606b69c7860a022081840dec8f215496b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 7 May 2025 19:37:04 -0600 Subject: [PATCH 05/19] feat(core): migrate-contacts-to-collection --- core/api/src/migrations/20221010162913-add-self-trade-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/api/src/migrations/20221010162913-add-self-trade-type.ts b/core/api/src/migrations/20221010162913-add-self-trade-type.ts index fb5144c67a..5b690e27df 100644 --- a/core/api/src/migrations/20221010162913-add-self-trade-type.ts +++ b/core/api/src/migrations/20221010162913-add-self-trade-type.ts @@ -78,7 +78,7 @@ module.exports = { // Fetch accountId from db if not cached const wallet = await walletsCollection.findOne({ id: walletId }) if (!wallet) continue - accountId = wallet._accountId.toString() + accountId = wallet._accountId?.toString?.() if (!accountId) continue accountIdsByWalletId[walletId] = accountId From d7a1b6bb68899f166c6008fd534ea17db9d2a32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Thu, 8 May 2025 13:55:31 -0600 Subject: [PATCH 06/19] test(core|e2e): test upsert contact mutation --- bats/core/api/contacts.bats | 64 +++++++++++++++++++++++++++++ bats/gql/account-contact-upsert.gql | 15 +++++++ bats/helpers/user.bash | 25 ----------- 3 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 bats/core/api/contacts.bats create mode 100644 bats/gql/account-contact-upsert.gql diff --git a/bats/core/api/contacts.bats b/bats/core/api/contacts.bats new file mode 100644 index 0000000000..28eb5a3db7 --- /dev/null +++ b/bats/core/api/contacts.bats @@ -0,0 +1,64 @@ +load "../../helpers/user.bash" + +ALICE="alice" +BOB="bob" + +setup_file() { + clear_cache + create_user "$ALICE" + user_update_username "$ALICE" + create_user "$BOB" + user_update_username "$BOB" +} + +@test "contact-upsert: add intraledger contact and verify" { + local identifier="$(read_value "$BOB.username")" + local alias="Intraledger Username" + + variables=$(jq -n \ + --arg identifier "$identifier" \ + --arg type "INTRALEDGER" \ + --arg alias "$alias" \ + '{input: {identifier: $identifier, type: $type, alias: $alias}}' + ) + + # Call GraphQL mutation + exec_graphql "$ALICE" "account-contact-upsert" "$variables" + + # Validate GraphQL response + contact_id="$(graphql_output '.data.accountContactUpsert.contact.id')" + [[ -n "$contact_id" ]] || fail "Expected contact to be created" + + contact_alias="$(graphql_output '.data.accountContactUpsert.contact.alias')" + [[ "$contact_alias" == "$alias" ]] || fail "Expected identifier to be $alias" + + # Validate contains the contact + run is_contact "$ALICE" "$BOB" + [[ "$status" == 0 ]] || fail "Contact not found" +} + +@test "contact-upsert: add lnaddress contact and verify" { + local identifier="lnaddress@example.com" + local alias="ln contact alias" + + variables=$(jq -n \ + --arg identifier "$identifier" \ + --arg type "LNADDRESS" \ + --arg alias "$alias" \ + '{input: {identifier: $identifier, type: $type, alias: $alias}}' + ) + + # Call GraphQL mutation + exec_graphql "$ALICE" "account-contact-upsert" "$variables" + + # Validate GraphQL response + contact_id="$(graphql_output '.data.accountContactUpsert.contact.id')" + [[ -n "$contact_id" ]] || fail "Expected contact to be created" + + contact_alias="$(graphql_output '.data.accountContactUpsert.contact.alias')" + [[ "$contact_alias" == "$alias" ]] || fail "Expected type to be $alias" + + # Verify contact is persisted + run is_contact "$ALICE" "$identifier" + [[ "$status" == "0" ]] || fail "Contact not found" +} diff --git a/bats/gql/account-contact-upsert.gql b/bats/gql/account-contact-upsert.gql new file mode 100644 index 0000000000..3b8f8e75c2 --- /dev/null +++ b/bats/gql/account-contact-upsert.gql @@ -0,0 +1,15 @@ +mutation accountContactUpsert($input: AccountContactUpsertInput!) { + accountContactUpsert(input: $input) { + errors { + message + } + contact { + id + identifier + type + alias + transactionsCount + createdAt + } + } +} diff --git a/bats/helpers/user.bash b/bats/helpers/user.bash index 52e7ff23fd..7e79c28323 100644 --- a/bats/helpers/user.bash +++ b/bats/helpers/user.bash @@ -134,7 +134,6 @@ ensure_username_is_present() { is_contact() { local owner_token_name="$1" -<<<<<<< HEAD local other_token_or_handle="$2" local contact_handle @@ -157,28 +156,4 @@ is_contact() { match=$(graphql_output ".data.me.contacts[] | select(.username == \"$contact_handle\")") [[ -n "$match" ]] -======= - local other_token_name="$2" - - local owner_account_id - owner_account_id=$(read_value "$owner_token_name.account_id") - [[ -n "$owner_account_id" ]] || return 1 - - local contact_identifier - contact_identifier=$(read_value "$other_token_name.username") - [[ -n "$contact_identifier" ]] || return 1 - - local mongo_query - mongo_query=$(echo "db.contacts.findOne( - { - accountId: \"$owner_account_id\", - identifier: \"$contact_identifier\" - } - );" | tr -d '[:space:]') - - local result - result=$(mongo_cli "$mongo_query" 2>&1) - - [[ "$result" != "null" && -n "$result" ]] ->>>>>>> e8ca37ca6 (refactor: move contact storage from accounts collection to dedicated contacts collection) } From 63e5ba333e3c2142d93eaef3e25d583b67617176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Fri, 9 May 2025 13:38:32 -0600 Subject: [PATCH 07/19] chore: renaming mutation accountContactUpsert to contactCreate --- apps/consent/app/graphql/generated.ts | 22 +------------------ apps/map/services/galoy/graphql/generated.ts | 22 +------------------ apps/pay/lib/graphql/generated.ts | 22 +------------------ bats/core/api/contacts.bats | 8 +++---- bats/gql/account-contact-upsert.gql | 4 ++-- .../root/mutation/account-contact-upsert.ts | 14 ++++++------ core/api/src/graphql/public/schema.graphql | 7 ------ .../apollo-federation/supergraph.graphql | 9 -------- 8 files changed, 16 insertions(+), 92 deletions(-) diff --git a/apps/consent/app/graphql/generated.ts b/apps/consent/app/graphql/generated.ts index 72e0db63ca..108647cdb6 100644 --- a/apps/consent/app/graphql/generated.ts +++ b/apps/consent/app/graphql/generated.ts @@ -44,8 +44,6 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } - /** Unique value used to identify a contact (e.g., username or lnAddress) */ - Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -143,12 +141,6 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; -export type AccountContactUpsertInput = { - readonly alias?: InputMaybe; - readonly identifier?: InputMaybe; - readonly type: ContactType; -}; - export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -466,12 +458,6 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; -export type ContactUpdateOrCreatePayload = { - readonly __typename: 'ContactUpdateOrCreatePayload'; - readonly contact?: Maybe; - readonly errors: ReadonlyArray; -}; - export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -943,7 +929,6 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; - readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1073,11 +1058,6 @@ export type Mutation = { }; -export type MutationAccountContactUpsertArgs = { - input: AccountContactUpsertInput; -}; - - export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; @@ -2366,4 +2346,4 @@ export function useGetUserIdSuspenseQuery(baseOptions?: Apollo.SkipToken | Apoll export type GetUserIdQueryHookResult = ReturnType; export type GetUserIdLazyQueryHookResult = ReturnType; export type GetUserIdSuspenseQueryHookResult = ReturnType; -export type GetUserIdQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetUserIdQueryResult = Apollo.QueryResult; diff --git a/apps/map/services/galoy/graphql/generated.ts b/apps/map/services/galoy/graphql/generated.ts index 523d217da2..37fa8b5092 100644 --- a/apps/map/services/galoy/graphql/generated.ts +++ b/apps/map/services/galoy/graphql/generated.ts @@ -44,8 +44,6 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } - /** Unique value used to identify a contact (e.g., username or lnAddress) */ - Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -143,12 +141,6 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; -export type AccountContactUpsertInput = { - readonly alias?: InputMaybe; - readonly identifier?: InputMaybe; - readonly type: ContactType; -}; - export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -466,12 +458,6 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; -export type ContactUpdateOrCreatePayload = { - readonly __typename: 'ContactUpdateOrCreatePayload'; - readonly contact?: Maybe; - readonly errors: ReadonlyArray; -}; - export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -943,7 +929,6 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; - readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1073,11 +1058,6 @@ export type Mutation = { }; -export type MutationAccountContactUpsertArgs = { - input: AccountContactUpsertInput; -}; - - export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; @@ -2380,4 +2360,4 @@ export function useBusinessMapMarkersSuspenseQuery(baseOptions?: Apollo.SkipToke export type BusinessMapMarkersQueryHookResult = ReturnType; export type BusinessMapMarkersLazyQueryHookResult = ReturnType; export type BusinessMapMarkersSuspenseQueryHookResult = ReturnType; -export type BusinessMapMarkersQueryResult = Apollo.QueryResult; \ No newline at end of file +export type BusinessMapMarkersQueryResult = Apollo.QueryResult; diff --git a/apps/pay/lib/graphql/generated.ts b/apps/pay/lib/graphql/generated.ts index 6bfcb36cb0..86f2e4fb98 100644 --- a/apps/pay/lib/graphql/generated.ts +++ b/apps/pay/lib/graphql/generated.ts @@ -44,8 +44,6 @@ export type Scalars = { Feedback: { input: string; output: string; } /** Hex-encoded string of 32 bytes */ Hex32Bytes: { input: string; output: string; } - /** Unique value used to identify a contact (e.g., username or lnAddress) */ - Identifier: { input: string; output: string; } Language: { input: string; output: string; } LnPaymentPreImage: { input: string; output: string; } /** BOLT11 lightning invoice payment request with the amount included */ @@ -144,12 +142,6 @@ export type AccountWalletByIdArgs = { walletId: Scalars['WalletId']['input']; }; -export type AccountContactUpsertInput = { - readonly alias?: InputMaybe; - readonly identifier?: InputMaybe; - readonly type: ContactType; -}; - export type AccountDeletePayload = { readonly __typename: 'AccountDeletePayload'; readonly errors: ReadonlyArray; @@ -467,12 +459,6 @@ export const ContactType = { } as const; export type ContactType = typeof ContactType[keyof typeof ContactType]; -export type ContactUpdateOrCreatePayload = { - readonly __typename: 'ContactUpdateOrCreatePayload'; - readonly contact?: Maybe; - readonly errors: ReadonlyArray; -}; - export type Coordinates = { readonly __typename: 'Coordinates'; readonly latitude: Scalars['Float']['output']; @@ -944,7 +930,6 @@ export type MobileVersions = { export type Mutation = { readonly __typename: 'Mutation'; - readonly accountContactUpsert: ContactUpdateOrCreatePayload; readonly accountDelete: AccountDeletePayload; readonly accountDisableNotificationCategory: AccountUpdateNotificationSettingsPayload; readonly accountDisableNotificationChannel: AccountUpdateNotificationSettingsPayload; @@ -1074,11 +1059,6 @@ export type Mutation = { }; -export type MutationAccountContactUpsertArgs = { - input: AccountContactUpsertInput; -}; - - export type MutationAccountDisableNotificationCategoryArgs = { input: AccountDisableNotificationCategoryInput; }; @@ -3036,4 +3016,4 @@ export function usePriceSubscription(baseOptions: Apollo.SubscriptionHookOptions return Apollo.useSubscription(PriceDocument, options); } export type PriceSubscriptionHookResult = ReturnType; -export type PriceSubscriptionResult = Apollo.SubscriptionResult; \ No newline at end of file +export type PriceSubscriptionResult = Apollo.SubscriptionResult; diff --git a/bats/core/api/contacts.bats b/bats/core/api/contacts.bats index 28eb5a3db7..6d10f751c0 100644 --- a/bats/core/api/contacts.bats +++ b/bats/core/api/contacts.bats @@ -26,10 +26,10 @@ setup_file() { exec_graphql "$ALICE" "account-contact-upsert" "$variables" # Validate GraphQL response - contact_id="$(graphql_output '.data.accountContactUpsert.contact.id')" + contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" - contact_alias="$(graphql_output '.data.accountContactUpsert.contact.alias')" + contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" [[ "$contact_alias" == "$alias" ]] || fail "Expected identifier to be $alias" # Validate contains the contact @@ -52,10 +52,10 @@ setup_file() { exec_graphql "$ALICE" "account-contact-upsert" "$variables" # Validate GraphQL response - contact_id="$(graphql_output '.data.accountContactUpsert.contact.id')" + contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" - contact_alias="$(graphql_output '.data.accountContactUpsert.contact.alias')" + contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" [[ "$contact_alias" == "$alias" ]] || fail "Expected type to be $alias" # Verify contact is persisted diff --git a/bats/gql/account-contact-upsert.gql b/bats/gql/account-contact-upsert.gql index 3b8f8e75c2..407db41ff8 100644 --- a/bats/gql/account-contact-upsert.gql +++ b/bats/gql/account-contact-upsert.gql @@ -1,5 +1,5 @@ -mutation accountContactUpsert($input: AccountContactUpsertInput!) { - accountContactUpsert(input: $input) { +mutation contactCreate($input: ContactCreateInput!) { + contactCreate(input: $input) { errors { message } diff --git a/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts b/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts index 3546e7c6e0..98d221c8a0 100644 --- a/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts +++ b/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts @@ -1,4 +1,4 @@ -import AccountContactUpsertPayload from "@/graphql/public/types/payload/account-contact-upsert" +import ContactCreatePayload from "@/graphql/public/types/payload/account-contact-upsert" import ContactIdentifier from "@/graphql/shared/types/scalar/contact-identifier" import ContactAlias from "@/graphql/public/types/scalar/contact-alias" import ContactType from "@/graphql/shared/types/scalar/contact-type" @@ -7,8 +7,8 @@ import { GT } from "@/graphql/index" import { Contacts } from "@/app" -const AccountContactUpsertInput = GT.Input({ - name: "AccountContactUpsertInput", +const ContactCreateInput = GT.Input({ + name: "ContactCreateInput", fields: () => ({ identifier: { type: ContactIdentifier }, alias: { type: ContactAlias }, @@ -16,13 +16,13 @@ const AccountContactUpsertInput = GT.Input({ }), }) -const AccountContactUpsertMutation = GT.Field({ +const ContactCreateMutation = GT.Field({ extensions: { complexity: 120, }, - type: GT.NonNull(AccountContactUpsertPayload), + type: GT.NonNull(ContactCreatePayload), args: { - input: { type: GT.NonNull(AccountContactUpsertInput) }, + input: { type: GT.NonNull(ContactCreateInput) }, }, resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { const { identifier, alias, type } = args.input @@ -53,4 +53,4 @@ const AccountContactUpsertMutation = GT.Field({ }, }) -export default AccountContactUpsertMutation +export default ContactCreateMutation diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index 9d2b434ab3..39a427ca09 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -43,12 +43,6 @@ interface Account { wallets: [Wallet!]! } -input AccountContactUpsertInput { - alias: ContactAlias - identifier: Identifier - type: ContactType! -} - type AccountDeletePayload { errors: [Error!]! success: Boolean! @@ -931,7 +925,6 @@ type MobileVersions { } type Mutation { - accountContactUpsert(input: AccountContactUpsertInput!): ContactUpdateOrCreatePayload! accountDelete: AccountDeletePayload! accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index dffdfa64eb..b68a980995 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -68,14 +68,6 @@ interface Account wallets: [Wallet!]! } -input AccountContactUpsertInput - @join__type(graph: PUBLIC) -{ - alias: ContactAlias - identifier: Identifier - type: ContactType! -} - type AccountDeletePayload @join__type(graph: PUBLIC) { @@ -1287,7 +1279,6 @@ type Mutation apiKeyCreate(input: ApiKeyCreateInput!): ApiKeyCreatePayload! @join__field(graph: API_KEYS) apiKeyRevoke(input: ApiKeyRevokeInput!): ApiKeyRevokePayload! @join__field(graph: API_KEYS) statefulNotificationAcknowledge(input: StatefulNotificationAcknowledgeInput!): StatefulNotificationAcknowledgePayload! @join__field(graph: NOTIFICATIONS) - accountContactUpsert(input: AccountContactUpsertInput!): ContactUpdateOrCreatePayload! @join__field(graph: PUBLIC) accountDelete: AccountDeletePayload! @join__field(graph: PUBLIC) accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) From 573bf20a82c28374c2d1411d412e775ea82d6741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Fri, 9 May 2025 14:02:39 -0600 Subject: [PATCH 08/19] chore: renamin contact with upsert references --- bats/core/api/contact.bats | 37 +++++++++++ bats/core/api/contacts.bats | 64 ------------------- bats/gql/account-contact-upsert.gql | 15 ----- bats/gql/contact-create.gql | 6 ++ .../{upsert-contact.ts => contact-create.ts} | 2 +- core/api/src/app/contacts/index.ts | 4 +- ...ntact.ts => intraledger-contact-create.ts} | 8 +-- core/api/src/app/payments/send-lightning.ts | 2 +- .../root/mutation/account-contact-upsert.ts | 56 ---------------- ...nt-contact-upsert.ts => contact-create.ts} | 0 ...nt-contact-upsert.ts => contact-create.ts} | 2 +- 11 files changed, 52 insertions(+), 144 deletions(-) delete mode 100644 bats/core/api/contacts.bats delete mode 100644 bats/gql/account-contact-upsert.gql rename core/api/src/app/contacts/{upsert-contact.ts => contact-create.ts} (95%) rename core/api/src/app/contacts/{upsert-intraledger-contact.ts => intraledger-contact-create.ts} (80%) delete mode 100644 core/api/src/graphql/public/root/mutation/account-contact-upsert.ts rename core/api/src/graphql/public/types/object/{account-contact-upsert.ts => contact-create.ts} (100%) rename core/api/src/graphql/public/types/payload/{account-contact-upsert.ts => contact-create.ts} (86%) diff --git a/bats/core/api/contact.bats b/bats/core/api/contact.bats index 4e3dc5bbd3..722052ba91 100644 --- a/bats/core/api/contact.bats +++ b/bats/core/api/contact.bats @@ -11,6 +11,7 @@ setup_file() { user_update_username "$BOB" } +<<<<<<< HEAD @test "contact: add intraledger contact" { local handle="$(read_value "$BOB.username")" local displayName="Intraledger Username" @@ -20,6 +21,17 @@ setup_file() { --arg type "INTRALEDGER" \ --arg displayName "$displayName" \ '{input: {handle: $handle, type: $type, displayName: $displayName}}' +======= +@test "contact: add intraledger contact and verify" { + local identifier="$(read_value "$BOB.username")" + local alias="Intraledger Username" + + variables=$(jq -n \ + --arg identifier "$identifier" \ + --arg type "INTRALEDGER" \ + --arg alias "$alias" \ + '{input: {identifier: $identifier, type: $type, alias: $alias}}' +>>>>>>> 350269fcf (chore: renamin contact with upsert references) ) # Call GraphQL mutation @@ -29,14 +41,20 @@ setup_file() { contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" +<<<<<<< HEAD contact_display_name="$(graphql_output '.data.contactCreate.contact.displayName')" [[ "$contact_display_name" == "$displayName" ]] || fail "Expected handle to be $displayName" +======= + contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" + [[ "$contact_alias" == "$alias" ]] || fail "Expected identifier to be $alias" +>>>>>>> 350269fcf (chore: renamin contact with upsert references) # Validate contains the contact run is_contact "$ALICE" "$BOB" [[ "$status" == 0 ]] || fail "Contact not found" } +<<<<<<< HEAD @test "contact: add lnaddress contact" { local handle="lnaddress@example.com" local displayName="ln contact displayName" @@ -46,6 +64,17 @@ setup_file() { --arg type "LNADDRESS" \ --arg displayName "$displayName" \ '{input: {handle: $handle, type: $type, displayName: $displayName}}' +======= +@test "contact: add lnaddress contact and verify" { + local identifier="lnaddress@example.com" + local alias="ln contact alias" + + variables=$(jq -n \ + --arg identifier "$identifier" \ + --arg type "LNADDRESS" \ + --arg alias "$alias" \ + '{input: {identifier: $identifier, type: $type, alias: $alias}}' +>>>>>>> 350269fcf (chore: renamin contact with upsert references) ) # Call GraphQL mutation @@ -55,10 +84,18 @@ setup_file() { contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" +<<<<<<< HEAD contact_display_name="$(graphql_output '.data.contactCreate.contact.displayName')" [[ "$contact_display_name" == "$displayName" ]] || fail "Expected type to be $displayName" # Verify contact is persisted run is_contact "$ALICE" "$handle" +======= + contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" + [[ "$contact_alias" == "$alias" ]] || fail "Expected type to be $alias" + + # Verify contact is persisted + run is_contact "$ALICE" "$identifier" +>>>>>>> 350269fcf (chore: renamin contact with upsert references) [[ "$status" == "0" ]] || fail "Contact not found" } diff --git a/bats/core/api/contacts.bats b/bats/core/api/contacts.bats deleted file mode 100644 index 6d10f751c0..0000000000 --- a/bats/core/api/contacts.bats +++ /dev/null @@ -1,64 +0,0 @@ -load "../../helpers/user.bash" - -ALICE="alice" -BOB="bob" - -setup_file() { - clear_cache - create_user "$ALICE" - user_update_username "$ALICE" - create_user "$BOB" - user_update_username "$BOB" -} - -@test "contact-upsert: add intraledger contact and verify" { - local identifier="$(read_value "$BOB.username")" - local alias="Intraledger Username" - - variables=$(jq -n \ - --arg identifier "$identifier" \ - --arg type "INTRALEDGER" \ - --arg alias "$alias" \ - '{input: {identifier: $identifier, type: $type, alias: $alias}}' - ) - - # Call GraphQL mutation - exec_graphql "$ALICE" "account-contact-upsert" "$variables" - - # Validate GraphQL response - contact_id="$(graphql_output '.data.contactCreate.contact.id')" - [[ -n "$contact_id" ]] || fail "Expected contact to be created" - - contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" - [[ "$contact_alias" == "$alias" ]] || fail "Expected identifier to be $alias" - - # Validate contains the contact - run is_contact "$ALICE" "$BOB" - [[ "$status" == 0 ]] || fail "Contact not found" -} - -@test "contact-upsert: add lnaddress contact and verify" { - local identifier="lnaddress@example.com" - local alias="ln contact alias" - - variables=$(jq -n \ - --arg identifier "$identifier" \ - --arg type "LNADDRESS" \ - --arg alias "$alias" \ - '{input: {identifier: $identifier, type: $type, alias: $alias}}' - ) - - # Call GraphQL mutation - exec_graphql "$ALICE" "account-contact-upsert" "$variables" - - # Validate GraphQL response - contact_id="$(graphql_output '.data.contactCreate.contact.id')" - [[ -n "$contact_id" ]] || fail "Expected contact to be created" - - contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" - [[ "$contact_alias" == "$alias" ]] || fail "Expected type to be $alias" - - # Verify contact is persisted - run is_contact "$ALICE" "$identifier" - [[ "$status" == "0" ]] || fail "Contact not found" -} diff --git a/bats/gql/account-contact-upsert.gql b/bats/gql/account-contact-upsert.gql deleted file mode 100644 index 407db41ff8..0000000000 --- a/bats/gql/account-contact-upsert.gql +++ /dev/null @@ -1,15 +0,0 @@ -mutation contactCreate($input: ContactCreateInput!) { - contactCreate(input: $input) { - errors { - message - } - contact { - id - identifier - type - alias - transactionsCount - createdAt - } - } -} diff --git a/bats/gql/contact-create.gql b/bats/gql/contact-create.gql index f92d2f51ca..71899099da 100644 --- a/bats/gql/contact-create.gql +++ b/bats/gql/contact-create.gql @@ -5,9 +5,15 @@ mutation contactCreate($input: ContactCreateInput!) { } contact { id +<<<<<<< HEAD handle type displayName +======= + identifier + type + alias +>>>>>>> 350269fcf (chore: renamin contact with upsert references) transactionsCount createdAt } diff --git a/core/api/src/app/contacts/upsert-contact.ts b/core/api/src/app/contacts/contact-create.ts similarity index 95% rename from core/api/src/app/contacts/upsert-contact.ts rename to core/api/src/app/contacts/contact-create.ts index 00c969b425..43563b3793 100644 --- a/core/api/src/app/contacts/upsert-contact.ts +++ b/core/api/src/app/contacts/contact-create.ts @@ -1,7 +1,7 @@ import { CouldNotFindContactFromAccountIdError } from "@/domain/errors" import { ContactsRepository } from "@/services/mongoose" -export const upserContact = async ({ +export const contactCreate = async ({ accountId, identifier, alias, diff --git a/core/api/src/app/contacts/index.ts b/core/api/src/app/contacts/index.ts index bee13e4f88..e47bc49af4 100644 --- a/core/api/src/app/contacts/index.ts +++ b/core/api/src/app/contacts/index.ts @@ -1,2 +1,2 @@ -export * from "./upsert-contact" -export * from "./upsert-intraledger-contact" +export * from "./contact-create" +export * from "./intraledger-contact-create" diff --git a/core/api/src/app/contacts/upsert-intraledger-contact.ts b/core/api/src/app/contacts/intraledger-contact-create.ts similarity index 80% rename from core/api/src/app/contacts/upsert-intraledger-contact.ts rename to core/api/src/app/contacts/intraledger-contact-create.ts index 3d89d02de7..8cb4f9b84f 100644 --- a/core/api/src/app/contacts/upsert-intraledger-contact.ts +++ b/core/api/src/app/contacts/intraledger-contact-create.ts @@ -1,8 +1,8 @@ -import { upserContact } from "./upsert-contact" +import { contactCreate } from "./contact-create" import { ContactType } from "@/domain/contacts" -export const upsertIntraledgerContacts = async ({ +export const IntraledgerContactCreate = async ({ senderAccount, recipientAccount, }: { @@ -14,7 +14,7 @@ export const upsertIntraledgerContacts = async ({ } if (recipientAccount.username) { - const contactToPayerResult = await upserContact({ + const contactToPayerResult = await contactCreate({ accountId: senderAccount.id, identifier: recipientAccount.username, alias: recipientAccount.username, @@ -24,7 +24,7 @@ export const upsertIntraledgerContacts = async ({ } if (senderAccount.username) { - const contactToPayeeResult = await upserContact({ + const contactToPayeeResult = await contactCreate({ accountId: recipientAccount.id, identifier: senderAccount.username, alias: senderAccount.username, diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 3a5eb72a6e..5a2a5dca4d 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -67,7 +67,7 @@ import { checkWithdrawalLimits, createIntraledgerContact, } from "@/app/accounts" -import { upsertIntraledgerContacts } from "@/app/contacts" +import { IntraledgerContactCreate } from "@/app/contacts" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" import { getTransactionForWalletByJournalId, diff --git a/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts b/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts deleted file mode 100644 index 98d221c8a0..0000000000 --- a/core/api/src/graphql/public/root/mutation/account-contact-upsert.ts +++ /dev/null @@ -1,56 +0,0 @@ -import ContactCreatePayload from "@/graphql/public/types/payload/account-contact-upsert" -import ContactIdentifier from "@/graphql/shared/types/scalar/contact-identifier" -import ContactAlias from "@/graphql/public/types/scalar/contact-alias" -import ContactType from "@/graphql/shared/types/scalar/contact-type" -import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" -import { GT } from "@/graphql/index" - -import { Contacts } from "@/app" - -const ContactCreateInput = GT.Input({ - name: "ContactCreateInput", - fields: () => ({ - identifier: { type: ContactIdentifier }, - alias: { type: ContactAlias }, - type: { type: GT.NonNull(ContactType) }, - }), -}) - -const ContactCreateMutation = GT.Field({ - extensions: { - complexity: 120, - }, - type: GT.NonNull(ContactCreatePayload), - args: { - input: { type: GT.NonNull(ContactCreateInput) }, - }, - resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { - const { identifier, alias, type } = args.input - - if (type instanceof Error) { - return { errors: [{ message: type.message }] } - } - - if (alias instanceof Error) { - return { errors: [{ message: alias.message }] } - } - - const result = await Contacts.upserContact({ - accountId: domainAccount.id, - identifier, - alias, - type, - }) - - if (result instanceof Error) { - return { errors: [mapAndParseErrorForGqlResponse(result)] } - } - - return { - errors: [], - contact: result, - } - }, -}) - -export default ContactCreateMutation diff --git a/core/api/src/graphql/public/types/object/account-contact-upsert.ts b/core/api/src/graphql/public/types/object/contact-create.ts similarity index 100% rename from core/api/src/graphql/public/types/object/account-contact-upsert.ts rename to core/api/src/graphql/public/types/object/contact-create.ts diff --git a/core/api/src/graphql/public/types/payload/account-contact-upsert.ts b/core/api/src/graphql/public/types/payload/contact-create.ts similarity index 86% rename from core/api/src/graphql/public/types/payload/account-contact-upsert.ts rename to core/api/src/graphql/public/types/payload/contact-create.ts index 1f8496a4a7..321dfa8b8b 100644 --- a/core/api/src/graphql/public/types/payload/account-contact-upsert.ts +++ b/core/api/src/graphql/public/types/payload/contact-create.ts @@ -1,4 +1,4 @@ -import Contact from "../object/account-contact-upsert" +import Contact from "../object/contact-create" import IError from "@/graphql/shared/types/abstract/error" import { GT } from "@/graphql/index" From 9ddf452e7c96aae33eab73f6ea9f75a4d7bd87bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 12 May 2025 10:30:38 -0600 Subject: [PATCH 09/19] refactor: referencing the contacts collection in accounts object --- core/api/src/services/mongoose/accounts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/api/src/services/mongoose/accounts.ts b/core/api/src/services/mongoose/accounts.ts index 86eadec6c7..7b4ed98c66 100644 --- a/core/api/src/services/mongoose/accounts.ts +++ b/core/api/src/services/mongoose/accounts.ts @@ -1,4 +1,5 @@ import { parseRepositoryError } from "./utils" +import { ContactsRepository } from "./contacts" import { AccountStatus } from "@/domain/accounts" import { From 5b1db89a8854e99c4600d761507aba7b4fc7d624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Thu, 19 Jun 2025 17:55:33 -0600 Subject: [PATCH 10/19] refactor(graphql): rename Contact.identifier to handle and Contact.alias to displayName --- bats/core/api/contact.bats | 37 ------------------- bats/gql/contact-create.gql | 6 --- core/api/src/app/contacts/contact-create.ts | 16 ++++---- .../contacts/intraledger-contact-create.ts | 8 ++-- core/api/src/app/payments/send-lightning.ts | 1 - .../public/root/mutation/contact-create.ts | 4 ++ core/api/src/graphql/public/schema.graphql | 3 -- .../public/types/object/contact-create.ts | 16 ++++---- .../shared/types/scalar/contact-identifier.ts | 30 --------------- .../apollo-federation/supergraph.graphql | 4 -- 10 files changed, 24 insertions(+), 101 deletions(-) delete mode 100644 core/api/src/graphql/shared/types/scalar/contact-identifier.ts diff --git a/bats/core/api/contact.bats b/bats/core/api/contact.bats index 722052ba91..4e3dc5bbd3 100644 --- a/bats/core/api/contact.bats +++ b/bats/core/api/contact.bats @@ -11,7 +11,6 @@ setup_file() { user_update_username "$BOB" } -<<<<<<< HEAD @test "contact: add intraledger contact" { local handle="$(read_value "$BOB.username")" local displayName="Intraledger Username" @@ -21,17 +20,6 @@ setup_file() { --arg type "INTRALEDGER" \ --arg displayName "$displayName" \ '{input: {handle: $handle, type: $type, displayName: $displayName}}' -======= -@test "contact: add intraledger contact and verify" { - local identifier="$(read_value "$BOB.username")" - local alias="Intraledger Username" - - variables=$(jq -n \ - --arg identifier "$identifier" \ - --arg type "INTRALEDGER" \ - --arg alias "$alias" \ - '{input: {identifier: $identifier, type: $type, alias: $alias}}' ->>>>>>> 350269fcf (chore: renamin contact with upsert references) ) # Call GraphQL mutation @@ -41,20 +29,14 @@ setup_file() { contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" -<<<<<<< HEAD contact_display_name="$(graphql_output '.data.contactCreate.contact.displayName')" [[ "$contact_display_name" == "$displayName" ]] || fail "Expected handle to be $displayName" -======= - contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" - [[ "$contact_alias" == "$alias" ]] || fail "Expected identifier to be $alias" ->>>>>>> 350269fcf (chore: renamin contact with upsert references) # Validate contains the contact run is_contact "$ALICE" "$BOB" [[ "$status" == 0 ]] || fail "Contact not found" } -<<<<<<< HEAD @test "contact: add lnaddress contact" { local handle="lnaddress@example.com" local displayName="ln contact displayName" @@ -64,17 +46,6 @@ setup_file() { --arg type "LNADDRESS" \ --arg displayName "$displayName" \ '{input: {handle: $handle, type: $type, displayName: $displayName}}' -======= -@test "contact: add lnaddress contact and verify" { - local identifier="lnaddress@example.com" - local alias="ln contact alias" - - variables=$(jq -n \ - --arg identifier "$identifier" \ - --arg type "LNADDRESS" \ - --arg alias "$alias" \ - '{input: {identifier: $identifier, type: $type, alias: $alias}}' ->>>>>>> 350269fcf (chore: renamin contact with upsert references) ) # Call GraphQL mutation @@ -84,18 +55,10 @@ setup_file() { contact_id="$(graphql_output '.data.contactCreate.contact.id')" [[ -n "$contact_id" ]] || fail "Expected contact to be created" -<<<<<<< HEAD contact_display_name="$(graphql_output '.data.contactCreate.contact.displayName')" [[ "$contact_display_name" == "$displayName" ]] || fail "Expected type to be $displayName" # Verify contact is persisted run is_contact "$ALICE" "$handle" -======= - contact_alias="$(graphql_output '.data.contactCreate.contact.alias')" - [[ "$contact_alias" == "$alias" ]] || fail "Expected type to be $alias" - - # Verify contact is persisted - run is_contact "$ALICE" "$identifier" ->>>>>>> 350269fcf (chore: renamin contact with upsert references) [[ "$status" == "0" ]] || fail "Contact not found" } diff --git a/bats/gql/contact-create.gql b/bats/gql/contact-create.gql index 71899099da..f92d2f51ca 100644 --- a/bats/gql/contact-create.gql +++ b/bats/gql/contact-create.gql @@ -5,15 +5,9 @@ mutation contactCreate($input: ContactCreateInput!) { } contact { id -<<<<<<< HEAD handle type displayName -======= - identifier - type - alias ->>>>>>> 350269fcf (chore: renamin contact with upsert references) transactionsCount createdAt } diff --git a/core/api/src/app/contacts/contact-create.ts b/core/api/src/app/contacts/contact-create.ts index 43563b3793..cb3a8ea99a 100644 --- a/core/api/src/app/contacts/contact-create.ts +++ b/core/api/src/app/contacts/contact-create.ts @@ -3,24 +3,24 @@ import { ContactsRepository } from "@/services/mongoose" export const contactCreate = async ({ accountId, - identifier, - alias, + handle, + displayName, type, }: { accountId: AccountId - identifier: string + handle: string type: ContactType - alias: string + displayName: string }): Promise => { const contactsRepo = ContactsRepository() - const existing = await contactsRepo.findContact({ accountId, identifier }) + const existing = await contactsRepo.findContact({ accountId, handle }) if (existing instanceof CouldNotFindContactFromAccountIdError) { return contactsRepo.persistNew({ accountId, - identifier, + handle, type, - alias, + displayName, transactionsCount: 1, }) } @@ -29,7 +29,7 @@ export const contactCreate = async ({ return contactsRepo.update({ ...existing, - alias, + displayName, transactionsCount: existing.transactionsCount + 1, }) } diff --git a/core/api/src/app/contacts/intraledger-contact-create.ts b/core/api/src/app/contacts/intraledger-contact-create.ts index 8cb4f9b84f..4ad901ecb6 100644 --- a/core/api/src/app/contacts/intraledger-contact-create.ts +++ b/core/api/src/app/contacts/intraledger-contact-create.ts @@ -16,8 +16,8 @@ export const IntraledgerContactCreate = async ({ if (recipientAccount.username) { const contactToPayerResult = await contactCreate({ accountId: senderAccount.id, - identifier: recipientAccount.username, - alias: recipientAccount.username, + handle: recipientAccount.username, + displayName: recipientAccount.username, type: ContactType.IntraLedger, }) if (contactToPayerResult instanceof Error) return contactToPayerResult @@ -26,8 +26,8 @@ export const IntraledgerContactCreate = async ({ if (senderAccount.username) { const contactToPayeeResult = await contactCreate({ accountId: recipientAccount.id, - identifier: senderAccount.username, - alias: senderAccount.username, + handle: senderAccount.username, + displayName: senderAccount.username, type: ContactType.IntraLedger, }) if (contactToPayeeResult instanceof Error) return contactToPayeeResult diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 5a2a5dca4d..57dea4ad31 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -67,7 +67,6 @@ import { checkWithdrawalLimits, createIntraledgerContact, } from "@/app/accounts" -import { IntraledgerContactCreate } from "@/app/contacts" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" import { getTransactionForWalletByJournalId, diff --git a/core/api/src/graphql/public/root/mutation/contact-create.ts b/core/api/src/graphql/public/root/mutation/contact-create.ts index 275395c998..34e6932c9d 100644 --- a/core/api/src/graphql/public/root/mutation/contact-create.ts +++ b/core/api/src/graphql/public/root/mutation/contact-create.ts @@ -1,4 +1,8 @@ +<<<<<<< HEAD import ContactPayload from "@/graphql/public/types/payload/contact" +======= +import ContactCreatePayload from "@/graphql/public/types/payload/contact-create" +>>>>>>> a946da599 (refactor(graphql): rename Contact.identifier to handle and Contact.alias to displayName) import ContactHandle from "@/graphql/shared/types/scalar/contact-handle" import ContactDisplayName from "@/graphql/public/types/scalar/contact-alias" import ContactType from "@/graphql/shared/types/scalar/contact-type" diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index 39a427ca09..a9418e8ec1 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -496,9 +496,6 @@ type GraphQLApplicationError implements Error { """Hex-encoded string of 32 bytes""" scalar Hex32Bytes -"""Unique value used to identify a contact (e.g., username or lnAddress)""" -scalar Identifier - union InitiationVia = InitiationViaIntraLedger | InitiationViaLn | InitiationViaOnChain type InitiationViaIntraLedger { diff --git a/core/api/src/graphql/public/types/object/contact-create.ts b/core/api/src/graphql/public/types/object/contact-create.ts index 6d1f39f5a8..adb329ab56 100644 --- a/core/api/src/graphql/public/types/object/contact-create.ts +++ b/core/api/src/graphql/public/types/object/contact-create.ts @@ -1,7 +1,7 @@ import ContactId from "@/graphql/shared/types/scalar/contact-id" import ContactType from "@/graphql/shared/types/scalar/contact-type" -import Identifier from "@/graphql/shared/types/scalar/contact-identifier" -import ContactAlias from "@/graphql/public/types/scalar/contact-alias" +import Handle from "@/graphql/shared/types/scalar/contact-handle" +import ContactDisplayName from "@/graphql/public/types/scalar/contact-display-name" import Timestamp from "@/graphql/shared/types/scalar/timestamp" import { GT } from "@/graphql/index" @@ -10,19 +10,19 @@ const Contact = GT.Object({ fields: () => ({ id: { type: GT.NonNull(ContactId), - description: "ID of the contact user or external identifier.", + description: "ID of the contact user or external handle.", }, type: { type: GT.NonNull(ContactType), description: "Type of the contact (intraledger, lnaddress, etc.).", }, - identifier: { - type: GT.NonNull(Identifier), + handle: { + type: GT.NonNull(Handle), description: "Username or lnAddress that identifies the contact.", }, - alias: { - type: ContactAlias, - description: "Alias name the user assigns to the contact.", + displayName: { + type: ContactDisplayName, + description: "DisplayName name the user assigns to the contact.", }, transactionsCount: { type: GT.NonNull(GT.Int), diff --git a/core/api/src/graphql/shared/types/scalar/contact-identifier.ts b/core/api/src/graphql/shared/types/scalar/contact-identifier.ts deleted file mode 100644 index ef3a9b2e26..0000000000 --- a/core/api/src/graphql/shared/types/scalar/contact-identifier.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { checkedToIdentifier } from "@/domain/contacts" -import { InputValidationError } from "@/graphql/error" -import { GT } from "@/graphql/index" - -const Identifier = GT.Scalar({ - name: "Identifier", - description: "Unique value used to identify a contact (e.g., username or lnAddress)", - parseValue(value) { - if (typeof value !== "string") { - return new InputValidationError({ message: "Invalid type for Identifier" }) - } - return validIdentifierValue(value) - }, - parseLiteral(ast) { - if (ast.kind === GT.Kind.STRING) { - return validIdentifierValue(ast.value) - } - return new InputValidationError({ message: "Invalid type for Identifier" }) - }, -}) - -function validIdentifierValue(value: string): string | InputValidationError { - const checked = checkedToIdentifier(value) - if (checked instanceof Error) { - return new InputValidationError({ message: "Invalid value for Identifier" }) - } - return checked -} - -export default Identifier diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index b68a980995..af3cfad19a 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -721,10 +721,6 @@ enum Icon REFRESH @join__enumValue(graph: NOTIFICATIONS) } -"""Unique value used to identify a contact (e.g., username or lnAddress)""" -scalar Identifier - @join__type(graph: PUBLIC) - union InitiationVia @join__type(graph: PUBLIC) @join__unionMember(graph: PUBLIC, member: "InitiationViaIntraLedger") From 10d91fefefaacb49f617efcce4a81b127735380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 23 Jun 2025 12:12:32 -0600 Subject: [PATCH 11/19] refactor(contacts): clean schema and rename repository methods --- core/api/src/app/contacts/contact-create.ts | 2 +- core/api/src/graphql/public/root/mutation/contact-create.ts | 4 ---- core/api/src/services/mongoose/accounts.ts | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/core/api/src/app/contacts/contact-create.ts b/core/api/src/app/contacts/contact-create.ts index cb3a8ea99a..c5a237140d 100644 --- a/core/api/src/app/contacts/contact-create.ts +++ b/core/api/src/app/contacts/contact-create.ts @@ -14,7 +14,7 @@ export const contactCreate = async ({ }): Promise => { const contactsRepo = ContactsRepository() - const existing = await contactsRepo.findContact({ accountId, handle }) + const existing = await contactsRepo.findByHandle({ accountId, handle }) if (existing instanceof CouldNotFindContactFromAccountIdError) { return contactsRepo.persistNew({ accountId, diff --git a/core/api/src/graphql/public/root/mutation/contact-create.ts b/core/api/src/graphql/public/root/mutation/contact-create.ts index 34e6932c9d..275395c998 100644 --- a/core/api/src/graphql/public/root/mutation/contact-create.ts +++ b/core/api/src/graphql/public/root/mutation/contact-create.ts @@ -1,8 +1,4 @@ -<<<<<<< HEAD import ContactPayload from "@/graphql/public/types/payload/contact" -======= -import ContactCreatePayload from "@/graphql/public/types/payload/contact-create" ->>>>>>> a946da599 (refactor(graphql): rename Contact.identifier to handle and Contact.alias to displayName) import ContactHandle from "@/graphql/shared/types/scalar/contact-handle" import ContactDisplayName from "@/graphql/public/types/scalar/contact-alias" import ContactType from "@/graphql/shared/types/scalar/contact-type" diff --git a/core/api/src/services/mongoose/accounts.ts b/core/api/src/services/mongoose/accounts.ts index 7b4ed98c66..86eadec6c7 100644 --- a/core/api/src/services/mongoose/accounts.ts +++ b/core/api/src/services/mongoose/accounts.ts @@ -1,5 +1,4 @@ import { parseRepositoryError } from "./utils" -import { ContactsRepository } from "./contacts" import { AccountStatus } from "@/domain/accounts" import { From f8ed4ee7b15219f48c2bf5cdf0bd1a4c33c3202b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 23 Jun 2025 14:09:39 -0600 Subject: [PATCH 12/19] refactor(contacts): clean up embedded contacts after migration --- .../src/migrations/20221010162913-add-self-trade-type.ts | 2 +- .../20250507231004-migrate-contacts-to-collection.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/api/src/migrations/20221010162913-add-self-trade-type.ts b/core/api/src/migrations/20221010162913-add-self-trade-type.ts index 5b690e27df..fb5144c67a 100644 --- a/core/api/src/migrations/20221010162913-add-self-trade-type.ts +++ b/core/api/src/migrations/20221010162913-add-self-trade-type.ts @@ -78,7 +78,7 @@ module.exports = { // Fetch accountId from db if not cached const wallet = await walletsCollection.findOne({ id: walletId }) if (!wallet) continue - accountId = wallet._accountId?.toString?.() + accountId = wallet._accountId.toString() if (!accountId) continue accountIdsByWalletId[walletId] = accountId diff --git a/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts b/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts index 9f39263c75..9e16c22a4e 100644 --- a/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts +++ b/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts @@ -54,6 +54,13 @@ module.exports = { } } + // Clean up embedded contacts from all accounts + const result = await db.collection("accounts").updateMany( + {}, + { $unset: { contacts: "" } } + ) + console.log(`Unset contacts field in ${result.modifiedCount} accounts`) + console.log(`Migration completed. Total contacts migrated: ${migratedCount}`) if (failedAccounts.length > 0) { console.warn("Some accounts failed to migrate:", failedAccounts) From 4c37475a23850b5c5c298939b94cee6a989ccb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 23 Jun 2025 14:43:23 -0600 Subject: [PATCH 13/19] refactor(graphql): rename contact-create.ts to contact.ts to match object name --- apps/dashboard/services/graphql/generated.ts | 8 ++++ .../public/types/object/contact-create.ts | 39 ------------------- .../public/types/payload/contact-create.ts | 18 --------- 3 files changed, 8 insertions(+), 57 deletions(-) delete mode 100644 core/api/src/graphql/public/types/object/contact-create.ts delete mode 100644 core/api/src/graphql/public/types/payload/contact-create.ts diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index 712af87310..34b94255b9 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -3615,7 +3615,11 @@ export type ResolversTypes = { ContactDisplayName: ResolverTypeWrapper; ContactHandle: ResolverTypeWrapper; ContactId: ResolverTypeWrapper; +<<<<<<< HEAD ContactPayload: ResolverTypeWrapper & { errors: ReadonlyArray }>; +======= + ContactPayload: ResolverTypeWrapper; +>>>>>>> a8af2567c (refactor(graphql): rename contact-create.ts to contact.ts to match object name) ContactType: ContactType; Coordinates: ResolverTypeWrapper; Float: ResolverTypeWrapper; @@ -3854,7 +3858,11 @@ export type ResolversParentTypes = { ContactDisplayName: Scalars['ContactDisplayName']['output']; ContactHandle: Scalars['ContactHandle']['output']; ContactId: Scalars['ContactId']['output']; +<<<<<<< HEAD ContactPayload: Omit & { errors: ReadonlyArray }; +======= + ContactPayload: ContactPayload; +>>>>>>> a8af2567c (refactor(graphql): rename contact-create.ts to contact.ts to match object name) Coordinates: Coordinates; Float: Scalars['Float']['output']; Country: Country; diff --git a/core/api/src/graphql/public/types/object/contact-create.ts b/core/api/src/graphql/public/types/object/contact-create.ts deleted file mode 100644 index adb329ab56..0000000000 --- a/core/api/src/graphql/public/types/object/contact-create.ts +++ /dev/null @@ -1,39 +0,0 @@ -import ContactId from "@/graphql/shared/types/scalar/contact-id" -import ContactType from "@/graphql/shared/types/scalar/contact-type" -import Handle from "@/graphql/shared/types/scalar/contact-handle" -import ContactDisplayName from "@/graphql/public/types/scalar/contact-display-name" -import Timestamp from "@/graphql/shared/types/scalar/timestamp" -import { GT } from "@/graphql/index" - -const Contact = GT.Object({ - name: "Contact", - fields: () => ({ - id: { - type: GT.NonNull(ContactId), - description: "ID of the contact user or external handle.", - }, - type: { - type: GT.NonNull(ContactType), - description: "Type of the contact (intraledger, lnaddress, etc.).", - }, - handle: { - type: GT.NonNull(Handle), - description: "Username or lnAddress that identifies the contact.", - }, - displayName: { - type: ContactDisplayName, - description: "DisplayName name the user assigns to the contact.", - }, - transactionsCount: { - type: GT.NonNull(GT.Int), - description: "Total number of transactions with this contact.", - }, - createdAt: { - type: GT.NonNull(Timestamp), - description: - "Unix timestamp (number of seconds elapsed since January 1, 1970 00:00:00 UTC)", - }, - }), -}) - -export default Contact diff --git a/core/api/src/graphql/public/types/payload/contact-create.ts b/core/api/src/graphql/public/types/payload/contact-create.ts deleted file mode 100644 index 321dfa8b8b..0000000000 --- a/core/api/src/graphql/public/types/payload/contact-create.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Contact from "../object/contact-create" - -import IError from "@/graphql/shared/types/abstract/error" -import { GT } from "@/graphql/index" - -const ContactUpdateOrCreatePayload = GT.Object({ - name: "ContactUpdateOrCreatePayload", - fields: () => ({ - errors: { - type: GT.NonNullList(IError), - }, - contact: { - type: Contact, - }, - }), -}) - -export default ContactUpdateOrCreatePayload From 600fb23bfc5133e4e7d882cbdb51a6a18f9ab8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Mon, 23 Jun 2025 18:22:51 -0600 Subject: [PATCH 14/19] refactor(contacts): unify contact creation logic and align with app conventions --- apps/dashboard/services/graphql/generated.ts | 8 ---- bats/helpers/user.bash | 7 ++++ core/api/src/app/contacts/contact-create.ts | 35 ------------------ core/api/src/app/contacts/index.ts | 2 - .../contacts/intraledger-contact-create.ts | 37 ------------------- core/api/src/app/index.ts | 3 -- 6 files changed, 7 insertions(+), 85 deletions(-) delete mode 100644 core/api/src/app/contacts/contact-create.ts delete mode 100644 core/api/src/app/contacts/index.ts delete mode 100644 core/api/src/app/contacts/intraledger-contact-create.ts diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index 34b94255b9..712af87310 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -3615,11 +3615,7 @@ export type ResolversTypes = { ContactDisplayName: ResolverTypeWrapper; ContactHandle: ResolverTypeWrapper; ContactId: ResolverTypeWrapper; -<<<<<<< HEAD ContactPayload: ResolverTypeWrapper & { errors: ReadonlyArray }>; -======= - ContactPayload: ResolverTypeWrapper; ->>>>>>> a8af2567c (refactor(graphql): rename contact-create.ts to contact.ts to match object name) ContactType: ContactType; Coordinates: ResolverTypeWrapper; Float: ResolverTypeWrapper; @@ -3858,11 +3854,7 @@ export type ResolversParentTypes = { ContactDisplayName: Scalars['ContactDisplayName']['output']; ContactHandle: Scalars['ContactHandle']['output']; ContactId: Scalars['ContactId']['output']; -<<<<<<< HEAD ContactPayload: Omit & { errors: ReadonlyArray }; -======= - ContactPayload: ContactPayload; ->>>>>>> a8af2567c (refactor(graphql): rename contact-create.ts to contact.ts to match object name) Coordinates: Coordinates; Float: Scalars['Float']['output']; Country: Country; diff --git a/bats/helpers/user.bash b/bats/helpers/user.bash index 7e79c28323..bed450972f 100644 --- a/bats/helpers/user.bash +++ b/bats/helpers/user.bash @@ -155,5 +155,12 @@ is_contact() { local match match=$(graphql_output ".data.me.contacts[] | select(.username == \"$contact_handle\")") +<<<<<<< HEAD [[ -n "$match" ]] +======= + local result + result=$(mongo_cli "$mongo_query") + + [[ "$result" != "null" && -n "$result" ]] +>>>>>>> 20418ae4e (refactor(contacts): unify contact creation logic and align with app conventions) } diff --git a/core/api/src/app/contacts/contact-create.ts b/core/api/src/app/contacts/contact-create.ts deleted file mode 100644 index c5a237140d..0000000000 --- a/core/api/src/app/contacts/contact-create.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CouldNotFindContactFromAccountIdError } from "@/domain/errors" -import { ContactsRepository } from "@/services/mongoose" - -export const contactCreate = async ({ - accountId, - handle, - displayName, - type, -}: { - accountId: AccountId - handle: string - type: ContactType - displayName: string -}): Promise => { - const contactsRepo = ContactsRepository() - - const existing = await contactsRepo.findByHandle({ accountId, handle }) - if (existing instanceof CouldNotFindContactFromAccountIdError) { - return contactsRepo.persistNew({ - accountId, - handle, - type, - displayName, - transactionsCount: 1, - }) - } - - if (existing instanceof Error) return existing - - return contactsRepo.update({ - ...existing, - displayName, - transactionsCount: existing.transactionsCount + 1, - }) -} diff --git a/core/api/src/app/contacts/index.ts b/core/api/src/app/contacts/index.ts deleted file mode 100644 index e47bc49af4..0000000000 --- a/core/api/src/app/contacts/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./contact-create" -export * from "./intraledger-contact-create" diff --git a/core/api/src/app/contacts/intraledger-contact-create.ts b/core/api/src/app/contacts/intraledger-contact-create.ts deleted file mode 100644 index 4ad901ecb6..0000000000 --- a/core/api/src/app/contacts/intraledger-contact-create.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { contactCreate } from "./contact-create" - -import { ContactType } from "@/domain/contacts" - -export const IntraledgerContactCreate = async ({ - senderAccount, - recipientAccount, -}: { - senderAccount: Account - recipientAccount: Account -}): Promise => { - if (!(senderAccount.contactEnabled && recipientAccount.contactEnabled)) { - return true - } - - if (recipientAccount.username) { - const contactToPayerResult = await contactCreate({ - accountId: senderAccount.id, - handle: recipientAccount.username, - displayName: recipientAccount.username, - type: ContactType.IntraLedger, - }) - if (contactToPayerResult instanceof Error) return contactToPayerResult - } - - if (senderAccount.username) { - const contactToPayeeResult = await contactCreate({ - accountId: recipientAccount.id, - handle: senderAccount.username, - displayName: senderAccount.username, - type: ContactType.IntraLedger, - }) - if (contactToPayeeResult instanceof Error) return contactToPayeeResult - } - - return true -} diff --git a/core/api/src/app/index.ts b/core/api/src/app/index.ts index 9634128e1b..bf7b93c005 100644 --- a/core/api/src/app/index.ts +++ b/core/api/src/app/index.ts @@ -3,7 +3,6 @@ import * as AuthenticationMod from "./authentication" import * as AdminMod from "./admin" import * as CallbackMod from "./callback" import * as CommMod from "./comm" -import * as ContactsMod from "./contacts" import * as QuizMod from "./quiz" import * as LightningMod from "./lightning" import * as OnChainMod from "./on-chain" @@ -23,7 +22,6 @@ const allFunctions = { Admin: { ...AdminMod }, Callback: { ...CallbackMod }, Comm: { ...CommMod }, - Contacts: { ...ContactsMod }, Quiz: { ...QuizMod }, Lightning: { ...LightningMod }, OnChain: { ...OnChainMod }, @@ -55,7 +53,6 @@ export const { Admin, Callback, Comm, - Contacts, Quiz, Lightning, OnChain, From 7a9751e89fb51248615e9d64bb7e7d7e14afabd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Tue, 24 Jun 2025 09:08:58 -0600 Subject: [PATCH 15/19] refactor(account): remove contact query from translateToAccount --- .../app/accounts/get-contacts-by-account.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 core/api/src/app/accounts/get-contacts-by-account.ts diff --git a/core/api/src/app/accounts/get-contacts-by-account.ts b/core/api/src/app/accounts/get-contacts-by-account.ts new file mode 100644 index 0000000000..702433aab8 --- /dev/null +++ b/core/api/src/app/accounts/get-contacts-by-account.ts @@ -0,0 +1,40 @@ +import { NoContactForUsernameError } from "@/domain/errors" + +import { ContactsRepository } from "@/services/mongoose" + +export const getContactByUsername = async ({ + account, + contactUsername, +}: { + account: Account + contactUsername: string +}): Promise => { + const contacts = await ContactsRepository().listByAccountId(account.id) + if (contacts instanceof Error) return contacts + + const contact = contacts.find( + (contact) => contact.handle.toLowerCase() === contactUsername.toLowerCase(), + ) + if (!contact) return new NoContactForUsernameError() + + return { + id: contact.handle as Username, + username: contact.handle as Username, + alias: contact.displayName as ContactAlias, + transactionsCount: contact.transactionsCount, + } +} + +export const getContactsByAccountId = async ( + accountId: AccountId, +): Promise => { + const contacts = await ContactsRepository().listByAccountId(accountId) + if (contacts instanceof Error) return contacts + + return contacts.map((contact) => ({ + id: contact.handle as Username, + username: contact.handle as Username, + alias: contact.displayName as ContactAlias, + transactionsCount: contact.transactionsCount, + })) +} From 89ae4b866d4afbae80ba01aa9325eaf9878b69a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 2 Jul 2025 21:10:27 -0600 Subject: [PATCH 16/19] chore(migration): move contacts unset operation to separate migration --- ...0507231004-migrate-contacts-to-collection.ts | 7 ------- .../20250702204019-remove-embedded-contacts.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 core/api/src/migrations/20250702204019-remove-embedded-contacts.ts diff --git a/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts b/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts index 9e16c22a4e..9f39263c75 100644 --- a/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts +++ b/core/api/src/migrations/20250507231004-migrate-contacts-to-collection.ts @@ -54,13 +54,6 @@ module.exports = { } } - // Clean up embedded contacts from all accounts - const result = await db.collection("accounts").updateMany( - {}, - { $unset: { contacts: "" } } - ) - console.log(`Unset contacts field in ${result.modifiedCount} accounts`) - console.log(`Migration completed. Total contacts migrated: ${migratedCount}`) if (failedAccounts.length > 0) { console.warn("Some accounts failed to migrate:", failedAccounts) diff --git a/core/api/src/migrations/20250702204019-remove-embedded-contacts.ts b/core/api/src/migrations/20250702204019-remove-embedded-contacts.ts new file mode 100644 index 0000000000..adac7c7f8d --- /dev/null +++ b/core/api/src/migrations/20250702204019-remove-embedded-contacts.ts @@ -0,0 +1,17 @@ +// @ts-nocheck +module.exports = { + async up(db) { + console.log("Removing embedded contacts from accounts...") + + const result = await db.collection("accounts").updateMany( + {}, + { $unset: { contacts: "" } } + ) + + console.log(`Unset contacts field in ${result.modifiedCount} accounts`) + }, + + async down() { + return true + }, +} From 255e73b51f31dcde559bc24e41054fe490b17e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Wed, 2 Jul 2025 21:11:59 -0600 Subject: [PATCH 17/19] refactor(domain): unify handle validation and remove casts from domain layer --- core/api/src/app/accounts/create-contact.ts | 9 ++++--- .../app/accounts/get-contacts-by-account.ts | 26 +++++++++---------- core/api/src/domain/errors.ts | 3 --- core/api/src/graphql/error-map.ts | 2 ++ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/api/src/app/accounts/create-contact.ts b/core/api/src/app/accounts/create-contact.ts index dc6e6a6e63..dd8370f1b7 100644 --- a/core/api/src/app/accounts/create-contact.ts +++ b/core/api/src/app/accounts/create-contact.ts @@ -12,9 +12,10 @@ export const createContact = async ({ handle, displayName, type, + incrementTxs = true, }: { accountId: AccountId - handle: string + handle: Username type: ContactType displayName: ContactAlias }): Promise => { @@ -32,7 +33,7 @@ export const createContact = async ({ handle: validatedHandle, type, displayName, - transactionsCount: 1, + transactionsCount: incrementTxs ? 1 : 0, }) } @@ -41,7 +42,9 @@ export const createContact = async ({ return contactsRepo.update({ ...existing, displayName, - transactionsCount: existing.transactionsCount + 1, + transactionsCount: incrementTxs + ? existing.transactionsCount + 1 + : existing.transactionsCount, }) } diff --git a/core/api/src/app/accounts/get-contacts-by-account.ts b/core/api/src/app/accounts/get-contacts-by-account.ts index 702433aab8..2f7b6df151 100644 --- a/core/api/src/app/accounts/get-contacts-by-account.ts +++ b/core/api/src/app/accounts/get-contacts-by-account.ts @@ -7,20 +7,18 @@ export const getContactByUsername = async ({ contactUsername, }: { account: Account - contactUsername: string + contactUsername: Username }): Promise => { - const contacts = await ContactsRepository().listByAccountId(account.id) - if (contacts instanceof Error) return contacts - - const contact = contacts.find( - (contact) => contact.handle.toLowerCase() === contactUsername.toLowerCase(), - ) - if (!contact) return new NoContactForUsernameError() + const contact = await ContactsRepository().findByHandle({ + accountId: account.id, + handle: contactUsername, + }) + if (contact instanceof Error) return new NoContactForUsernameError() return { - id: contact.handle as Username, - username: contact.handle as Username, - alias: contact.displayName as ContactAlias, + id: contact.handle, + username: contact.handle, + alias: contact.displayName, transactionsCount: contact.transactionsCount, } } @@ -32,9 +30,9 @@ export const getContactsByAccountId = async ( if (contacts instanceof Error) return contacts return contacts.map((contact) => ({ - id: contact.handle as Username, - username: contact.handle as Username, - alias: contact.displayName as ContactAlias, + id: contact.handle, + username: contact.handle, + alias: contact.displayName, transactionsCount: contact.transactionsCount, })) } diff --git a/core/api/src/domain/errors.ts b/core/api/src/domain/errors.ts index 16ec90f8c3..bd1307394e 100644 --- a/core/api/src/domain/errors.ts +++ b/core/api/src/domain/errors.ts @@ -73,9 +73,6 @@ export class CouldNotFindMerchantFromIdError extends CouldNotFindError {} export class CouldNotFindAccountFromPhoneError extends CouldNotFindError {} export class CouldNotFindTransactionsForAccountError extends CouldNotFindError {} export class CouldNotFindAccountFromKratosIdError extends CouldNotFindError {} -export class CouldNotFindContactFromAccountIdError extends CouldNotFindError {} -export class CouldNotFindContactFromContactIdError extends CouldNotFindError {} -export class CouldNotUpdateContactError extends RepositoryError {} export class QuizAlreadyPresentError extends DomainError {} diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index 6c9efdd21b..f674029999 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -726,6 +726,8 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "InvalidAccountLevelError": case "InvalidAccountLimitTypeError": case "InvalidWithdrawFeeError": + case "InvalidHandleError": + case "InvalidContactIdError": case "InvalidUsdCents": case "NonIntegerError": case "FeeDifferenceError": From c541cd633213ea0492c5c50a3d121dc2b8a8751f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Thu, 7 Aug 2025 09:37:53 -0600 Subject: [PATCH 18/19] fix: rebase solve conflicts --- core/api/src/app/accounts/create-contact.ts | 9 ++--- .../app/accounts/get-contacts-by-account.ts | 33 ++++++++++++------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/core/api/src/app/accounts/create-contact.ts b/core/api/src/app/accounts/create-contact.ts index dd8370f1b7..dc6e6a6e63 100644 --- a/core/api/src/app/accounts/create-contact.ts +++ b/core/api/src/app/accounts/create-contact.ts @@ -12,10 +12,9 @@ export const createContact = async ({ handle, displayName, type, - incrementTxs = true, }: { accountId: AccountId - handle: Username + handle: string type: ContactType displayName: ContactAlias }): Promise => { @@ -33,7 +32,7 @@ export const createContact = async ({ handle: validatedHandle, type, displayName, - transactionsCount: incrementTxs ? 1 : 0, + transactionsCount: 1, }) } @@ -42,9 +41,7 @@ export const createContact = async ({ return contactsRepo.update({ ...existing, displayName, - transactionsCount: incrementTxs - ? existing.transactionsCount + 1 - : existing.transactionsCount, + transactionsCount: existing.transactionsCount + 1, }) } diff --git a/core/api/src/app/accounts/get-contacts-by-account.ts b/core/api/src/app/accounts/get-contacts-by-account.ts index 2f7b6df151..e0432233a6 100644 --- a/core/api/src/app/accounts/get-contacts-by-account.ts +++ b/core/api/src/app/accounts/get-contacts-by-account.ts @@ -1,37 +1,48 @@ +import { checkedToHandle } from "@/domain/contacts" import { NoContactForUsernameError } from "@/domain/errors" +import { InvalidHandleError } from "@/domain/contacts/errors" import { ContactsRepository } from "@/services/mongoose" -export const getContactByUsername = async ({ - account, - contactUsername, +export const getContactByHandle = async ({ + accountId, + handle, }: { - account: Account - contactUsername: Username + accountId: AccountId + handle: string }): Promise => { + const validatedHandle = checkedToHandle(handle) + if (validatedHandle instanceof InvalidHandleError) { + return validatedHandle + } + const contact = await ContactsRepository().findByHandle({ - accountId: account.id, - handle: contactUsername, + accountId, + handle: validatedHandle, }) if (contact instanceof Error) return new NoContactForUsernameError() return { id: contact.handle, username: contact.handle, + handle: contact.handle, alias: contact.displayName, transactionsCount: contact.transactionsCount, } } -export const getContactsByAccountId = async ( - accountId: AccountId, -): Promise => { - const contacts = await ContactsRepository().listByAccountId(accountId) +export const getContactsByAccountId = async ({ + accountId, +}: { + accountId: AccountId +}): Promise => { + const contacts = await ContactsRepository().listByAccountId({ accountId }) if (contacts instanceof Error) return contacts return contacts.map((contact) => ({ id: contact.handle, username: contact.handle, + handle: contact.handle, alias: contact.displayName, transactionsCount: contact.transactionsCount, })) From edb4119c1e5505e943dc8c91e4b08a16d61e050e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esa=C3=BA=20G=C3=B3mez?= Date: Thu, 7 Aug 2025 09:54:42 -0600 Subject: [PATCH 19/19] fix(apps): solve conflicts --- bats/helpers/user.bash | 7 --- .../app/accounts/get-contacts-by-account.ts | 49 ------------------- core/api/src/graphql/error-map.ts | 2 - 3 files changed, 58 deletions(-) delete mode 100644 core/api/src/app/accounts/get-contacts-by-account.ts diff --git a/bats/helpers/user.bash b/bats/helpers/user.bash index bed450972f..7e79c28323 100644 --- a/bats/helpers/user.bash +++ b/bats/helpers/user.bash @@ -155,12 +155,5 @@ is_contact() { local match match=$(graphql_output ".data.me.contacts[] | select(.username == \"$contact_handle\")") -<<<<<<< HEAD [[ -n "$match" ]] -======= - local result - result=$(mongo_cli "$mongo_query") - - [[ "$result" != "null" && -n "$result" ]] ->>>>>>> 20418ae4e (refactor(contacts): unify contact creation logic and align with app conventions) } diff --git a/core/api/src/app/accounts/get-contacts-by-account.ts b/core/api/src/app/accounts/get-contacts-by-account.ts deleted file mode 100644 index e0432233a6..0000000000 --- a/core/api/src/app/accounts/get-contacts-by-account.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { checkedToHandle } from "@/domain/contacts" -import { NoContactForUsernameError } from "@/domain/errors" -import { InvalidHandleError } from "@/domain/contacts/errors" - -import { ContactsRepository } from "@/services/mongoose" - -export const getContactByHandle = async ({ - accountId, - handle, -}: { - accountId: AccountId - handle: string -}): Promise => { - const validatedHandle = checkedToHandle(handle) - if (validatedHandle instanceof InvalidHandleError) { - return validatedHandle - } - - const contact = await ContactsRepository().findByHandle({ - accountId, - handle: validatedHandle, - }) - if (contact instanceof Error) return new NoContactForUsernameError() - - return { - id: contact.handle, - username: contact.handle, - handle: contact.handle, - alias: contact.displayName, - transactionsCount: contact.transactionsCount, - } -} - -export const getContactsByAccountId = async ({ - accountId, -}: { - accountId: AccountId -}): Promise => { - const contacts = await ContactsRepository().listByAccountId({ accountId }) - if (contacts instanceof Error) return contacts - - return contacts.map((contact) => ({ - id: contact.handle, - username: contact.handle, - handle: contact.handle, - alias: contact.displayName, - transactionsCount: contact.transactionsCount, - })) -} diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index f674029999..6c9efdd21b 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -726,8 +726,6 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "InvalidAccountLevelError": case "InvalidAccountLimitTypeError": case "InvalidWithdrawFeeError": - case "InvalidHandleError": - case "InvalidContactIdError": case "InvalidUsdCents": case "NonIntegerError": case "FeeDifferenceError":