diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 4c0006339..a24128e71 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -1132,6 +1132,8 @@ type Mutation lnUsdInvoiceCreateOnBehalfOfRecipient(input: LnUsdInvoiceCreateOnBehalfOfRecipientInput!): LnInvoicePayload! lnUsdInvoiceFeeProbe(input: LnUsdInvoiceFeeProbeInput!): CentAmountPayload! merchantMapSuggest(input: MerchantMapSuggestInput!): MerchantPayload! + newUserEmailRegistrationInitiate(input: NewUserEmailRegistrationInitiateInput!): NewUserEmailRegistrationInitiatePayload! + newUserEmailRegistrationValidate(input: NewUserEmailRegistrationValidateInput!): AuthTokenPayload! onChainAddressCreate(input: OnChainAddressCreateInput!): OnChainAddressPayload! onChainAddressCurrent(input: OnChainAddressCurrentInput!): OnChainAddressPayload! onChainPaymentSend(input: OnChainPaymentSendInput!): PaymentSendPayload! @@ -1181,6 +1183,26 @@ enum Network testnet @join__enumValue(graph: PUBLIC) } +input NewUserEmailRegistrationInitiateInput + @join__type(graph: PUBLIC) +{ + email: EmailAddress! +} + +type NewUserEmailRegistrationInitiatePayload + @join__type(graph: PUBLIC) +{ + emailFlowId: String + errors: [Error!]! +} + +input NewUserEmailRegistrationValidateInput + @join__type(graph: PUBLIC) +{ + code: OneTimeAuthCode! + emailFlowId: String! +} + scalar NotificationCategory @join__type(graph: PUBLIC) diff --git a/dev/ory/email-template.html b/dev/ory/email-template.html new file mode 100644 index 000000000..194d19fbf --- /dev/null +++ b/dev/ory/email-template.html @@ -0,0 +1,93 @@ + + + + + + + + + + +
+
+

Welcome to Flash

+ Welcome Image +
+
+

Confirm Your Access

+

Hi,

+

Confirm access to your Flash account using the following code:

+
{{ .RecoveryCode }}
+

This code will only be used once. Do not share it with anyone.

+
+ +
+ + + \ No newline at end of file diff --git a/dev/ory/kratos.yml b/dev/ory/kratos.yml index 7001bbd1a..572b7cb66 100644 --- a/dev/ory/kratos.yml +++ b/dev/ory/kratos.yml @@ -183,7 +183,7 @@ courier: recovery_code: valid: email: - subject: base64://eW91ciBjb2RlCg== + subject: base64://WW91ciBGbGFzaCB2ZXJpZmljYXRpb24gY29kZQ== body: # courier/template/courier/builtin/templates/recovery_code/valid/email.body.plaintext.gotmpl # Hi, @@ -191,7 +191,7 @@ courier: # {{ .RecoveryCode }} # Don't share this code with anyone. Our employee will never ask for this code plaintext: base64://SGksCgpZb3UgY2FuIGNvbmZpcm0gYWNjZXNzIHRvIHlvdXIgYmxpbmsgYWNjb3VudCBieSBlbnRlcmluZyB0aGUgZm9sbG93aW5nIGNvZGU6Cgp7eyAuUmVjb3ZlcnlDb2RlIH19CgpEb24ndCBzaGFyZSB0aGlzIGNvZGUgd2l0aCBhbnlvbmUuIE91ciBlbXBsb3llZSB3aWxsIG5ldmVyIGFzayBmb3IgdGhpcyBjb2RlCg== - html: base64://SGksCgpZb3UgY2FuIGNvbmZpcm0gYWNjZXNzIHRvIHlvdXIgYmxpbmsgYWNjb3VudCBieSBlbnRlcmluZyB0aGUgZm9sbG93aW5nIGNvZGU6Cgp7eyAuUmVjb3ZlcnlDb2RlIH19CgpEb24ndCBzaGFyZSB0aGlzIGNvZGUgd2l0aCBhbnlvbmUuIE91ciBlbXBsb3llZSB3aWxsIG5ldmVyIGFzayBmb3IgdGhpcyBjb2RlCg== + html: file:///home/ory/email-template.html session: # TODO: check lifespan per schema diff --git a/src/app/accounts/create-account.ts b/src/app/accounts/create-account.ts index 67b28fc04..3f223e28f 100644 --- a/src/app/accounts/create-account.ts +++ b/src/app/accounts/create-account.ts @@ -117,3 +117,25 @@ export const createAccountWithPhoneIdentifier = async ({ return account } + +export const createAccountWithEmailIdentifier = async ({ + newAccountInfo: { kratosUserId, email }, + config, +}: { + newAccountInfo: NewAccountWithEmailIdentifier + config: AccountsConfig +}): Promise => { + const user = await UsersRepository().update({ id: kratosUserId, email }) + if (user instanceof Error) return user + + const accountNew = await AccountsRepository().persistNew(kratosUserId) + if (accountNew instanceof Error) return accountNew + + const account = await initializeCreatedAccount({ + account: accountNew, + config, + }) + if (account instanceof Error) return account + + return account +} diff --git a/src/app/accounts/upgrade-device-account.ts b/src/app/accounts/upgrade-device-account.ts index 671a53af2..816ac110d 100644 --- a/src/app/accounts/upgrade-device-account.ts +++ b/src/app/accounts/upgrade-device-account.ts @@ -30,3 +30,42 @@ export const upgradeAccountFromDeviceToPhone = async ({ return accountUpdated } + +/** + * Upgrades a TRIAL device account to an email-based account + * + * This function updates MongoDB records after Kratos schema has been upgraded + * from username_password_deviceid_v0 to email_no_password_v0 + * + * Changes made: + * - Adds email to User record + * - Upgrades Account level from 0 (TRIAL) to 1 (verified) + * - Preserves deviceId field in User record + * + * @param userId - Kratos user ID + * @param email - Email address to add + * @returns Updated Account or RepositoryError + */ +export const upgradeAccountFromDeviceToEmail = async ({ + userId, + email, +}: { + userId: UserId + email: EmailAddress +}): Promise => { + // TODO: ideally both 1. and 2. should be done in a transaction, + // so that if one fails, the other is rolled back + + // 1. Update user record with email (deviceId is preserved via spread) + const res = await UsersRepository().addEmail(userId, email) + if (res instanceof Error) return res + + // 2. Update account level from TRIAL (0) to verified (1) + const accountDevice = await AccountsRepository().findByUserId(userId) + if (accountDevice instanceof Error) return accountDevice + accountDevice.level = AccountLevel.One + const accountUpdated = await AccountsRepository().update(accountDevice) + if (accountUpdated instanceof Error) return accountUpdated + + return accountUpdated +} diff --git a/src/app/authentication/create-account-from-registration-payload.ts b/src/app/authentication/create-account-from-registration-payload.ts index 1b0eab5a8..e28198581 100644 --- a/src/app/authentication/create-account-from-registration-payload.ts +++ b/src/app/authentication/create-account-from-registration-payload.ts @@ -50,6 +50,9 @@ export const createAccountFromRegistrationPayload = async ({ } const { userId, phone, phoneMetadata } = regPayload + if (!phone) { + return new InvalidPhoneNumber("Phone is required for phone registration") + } const account = await createAccountWithPhoneIdentifier({ newAccountInfo: { phone, kratosUserId: userId }, config: getDefaultAccountsConfig(), diff --git a/src/app/authentication/email.ts b/src/app/authentication/email.ts index efa643172..0ffaca6bf 100644 --- a/src/app/authentication/email.ts +++ b/src/app/authentication/email.ts @@ -1,8 +1,26 @@ import { AccountAlreadyHasEmailError } from "@domain/authentication/errors" -import { AuthWithEmailPasswordlessService } from "@services/kratos" +import { AuthWithEmailPasswordlessService, IdentityRepository } from "@services/kratos" +import { SchemaIdType } from "@services/kratos/schema" import { baseLogger } from "@services/logger" import { UsersRepository } from "@services/mongoose" +import { upgradeAccountFromDeviceToEmail } from "@app/accounts" +/** + * Adds email to an authenticated user's identity + * + * Handles two flows: + * 1. TRIAL device account → Email account (upgrade) + * - Upgrades Kratos schema and MongoDB records + * - Sends verification email + * + * 2. Phone account → Phone+Email account (add email) + * - Adds email to existing phone account + * - Sends verification email + * + * @param email - Email address to add + * @param userId - Authenticated user's ID + * @returns Email registration ID and updated user, or error + */ export const addEmailToIdentity = async ({ email, userId, @@ -12,16 +30,37 @@ export const addEmailToIdentity = async ({ }): Promise => { const authServiceEmail = AuthWithEmailPasswordlessService() + // Prevent duplicate emails const hasEmail = await authServiceEmail.hasEmail({ kratosUserId: userId }) if (hasEmail instanceof Error) return hasEmail if (hasEmail) return new AccountAlreadyHasEmailError() + // Detect if this is a device account upgrade or phone account email addition + const identityRepo = IdentityRepository() + const identity = await identityRepo.getIdentity(userId) + if (identity instanceof Error) return identity + + const isDeviceAccountUpgrade = + identity.schema === SchemaIdType.UsernamePasswordDeviceIdV0 + + // Update Kratos identity (handles schema upgrade internally) const res = await authServiceEmail.addUnverifiedEmailToIdentity({ email, kratosUserId: userId, }) if (res instanceof Error) return res + // For device account upgrades, also update MongoDB (account level, user email) + // For phone accounts, MongoDB is already updated via addUnverifiedEmailToIdentity + if (isDeviceAccountUpgrade) { + const accountUpgradeRes = await upgradeAccountFromDeviceToEmail({ + userId, + email, + }) + if (accountUpgradeRes instanceof Error) return accountUpgradeRes + } + + // Send verification code to email const emailRegistrationId = await authServiceEmail.sendEmailWithCode({ email }) if (emailRegistrationId instanceof Error) return emailRegistrationId diff --git a/src/app/authentication/index.types.d.ts b/src/app/authentication/index.types.d.ts index ebdcc8f29..0106623b2 100644 --- a/src/app/authentication/index.types.d.ts +++ b/src/app/authentication/index.types.d.ts @@ -3,6 +3,11 @@ type NewAccountWithPhoneIdentifier = { phone: PhoneNumber } +type NewAccountWithEmailIdentifier = { + kratosUserId: UserId + email: EmailAddress +} + type LoginDeviceUpgradeWithPhoneResult = { success: true authToken?: AuthToken diff --git a/src/app/authentication/request-code.ts b/src/app/authentication/request-code.ts index f2da66640..6e1075e09 100644 --- a/src/app/authentication/request-code.ts +++ b/src/app/authentication/request-code.ts @@ -127,10 +127,12 @@ export const requestEmailCode = async ({ } const authServiceEmail = AuthWithEmailPasswordlessService() - const flow = await authServiceEmail.sendEmailWithCode({ email }) - if (flow instanceof Error) return flow - return flow + // Use createIdentityForEmailRegistration which handles both new and existing users + const result = await authServiceEmail.createIdentityForEmailRegistration({ email }) + if (result instanceof Error) return result + + return result.flowId } const checkRequestCodeAttemptPerIpLimits = async ( diff --git a/src/domain/authentication/index.types.d.ts b/src/domain/authentication/index.types.d.ts index 8dc709a49..0868d6162 100644 --- a/src/domain/authentication/index.types.d.ts +++ b/src/domain/authentication/index.types.d.ts @@ -72,13 +72,15 @@ type CallbackSecretValidator = { type RegistrationPayload = { userId: UserId - phone: PhoneNumber - phoneMetadata: PhoneMetadata | undefined + phone?: PhoneNumber + email?: EmailAddress + phoneMetadata?: PhoneMetadata | undefined } type RegistrationPayloadValidator = { validate(rawBody: { identity_id?: string phone?: string + email?: string schema_id?: string transient_payload?: { phoneMetadata?: Record> } }): RegistrationPayload | ValidationError @@ -136,6 +138,9 @@ interface IAuthWithEmailPasswordlessService { kratosUserId: UserId email: EmailAddress }): Promise + createIdentityForEmailRegistration(args: { + email: EmailAddress + }): Promise<{ kratosUserId: UserId; flowId: EmailFlowId } | KratosError> sendEmailWithCode(args: { email: EmailAddress }): Promise hasEmail(args: { kratosUserId: UserId }): Promise isEmailVerified(args: { email: EmailAddress }): Promise diff --git a/src/domain/authentication/registration-payload-validator.ts b/src/domain/authentication/registration-payload-validator.ts index e54b2795c..105286bee 100644 --- a/src/domain/authentication/registration-payload-validator.ts +++ b/src/domain/authentication/registration-payload-validator.ts @@ -1,5 +1,10 @@ import { checkedToUserId } from "@domain/accounts" -import { checkedToPhoneNumber, PhoneMetadataValidator } from "@domain/users" +import { + checkedToPhoneNumber, + checkedToEmailAddress, + PhoneMetadataValidator, +} from "@domain/users" +import { SchemaIdType } from "@services/kratos/schema" import { MissingRegistrationPayloadPropertiesError, @@ -12,18 +17,27 @@ export const RegistrationPayloadValidator = ( const validate = (rawBody: { identity_id?: string phone?: string + email?: string schema_id?: string transient_payload?: { phoneMetadata?: Record> } }): RegistrationPayload | ValidationError => { const { identity_id: userIdRaw, phone: phoneRaw, + email: emailRaw, schema_id: schemaIdRaw, transient_payload, } = rawBody - if (!(phoneRaw && userIdRaw && schemaIdRaw)) { - return new MissingRegistrationPayloadPropertiesError() + // Check for required fields based on schema type + if (schemaId === SchemaIdType.EmailNoPasswordV0) { + if (!(emailRaw && userIdRaw && schemaIdRaw)) { + return new MissingRegistrationPayloadPropertiesError() + } + } else { + if (!(phoneRaw && userIdRaw && schemaIdRaw)) { + return new MissingRegistrationPayloadPropertiesError() + } } if (schemaIdRaw !== schemaId) { @@ -33,7 +47,19 @@ export const RegistrationPayloadValidator = ( const userIdChecked = checkedToUserId(userIdRaw) if (userIdChecked instanceof Error) return userIdChecked - const phoneChecked = checkedToPhoneNumber(phoneRaw) + // Handle email schema + if (schemaId === SchemaIdType.EmailNoPasswordV0) { + const emailChecked = checkedToEmailAddress(emailRaw as string) + if (emailChecked instanceof Error) return emailChecked + + return { + userId: userIdChecked, + email: emailChecked, + } + } + + // Handle phone schema (existing logic) + const phoneChecked = checkedToPhoneNumber(phoneRaw as string) if (phoneChecked instanceof Error) return phoneChecked const rawPhoneMetadata = transient_payload?.phoneMetadata diff --git a/src/domain/users/index.types.d.ts b/src/domain/users/index.types.d.ts index 9c13face0..3f553a8b3 100644 --- a/src/domain/users/index.types.d.ts +++ b/src/domain/users/index.types.d.ts @@ -63,9 +63,10 @@ type User = { phoneMetadata: PhoneMetadata | undefined phone?: PhoneNumber | undefined deletedPhones?: PhoneNumber[] + email?: EmailAddress | undefined + deletedEmails?: EmailAddress[] | undefined createdAt: Date deviceId?: DeviceId | undefined - deletedEmails?: EmailAddress[] | undefined } type UserUpdateInput = Omit, "language" | "createdAt"> & { @@ -76,5 +77,7 @@ type UserUpdateInput = Omit, "language" | "createdAt"> & { interface IUsersRepository { findById(id: UserId): Promise findByPhone(phone: PhoneNumber): Promise + findByEmail(email: EmailAddress): Promise update(user: UserUpdateInput): Promise + addEmail(userId: UserId, email: EmailAddress): Promise } diff --git a/src/graphql/public/mutations.ts b/src/graphql/public/mutations.ts index 6f9262d28..aa2615b05 100644 --- a/src/graphql/public/mutations.ts +++ b/src/graphql/public/mutations.ts @@ -39,6 +39,8 @@ import AccountDeleteMutation from "@graphql/public/root/mutation/account-delete" import UserLoginUpgradeMutation from "@graphql/public/root/mutation/user-login-upgrade" import UserEmailRegistrationInitiateMutation from "@graphql/public/root/mutation/user-email-registration-initiate" import UserEmailRegistrationValidateMutation from "@graphql/public/root/mutation/user-email-registration-validate" +import NewUserEmailRegistrationInitiateMutation from "@graphql/public/root/mutation/new-user-email-registration-initiate" +import NewUserEmailRegistrationValidateMutation from "@graphql/public/root/mutation/new-user-email-registration-validate" import FeedbackSubmitMutation from "@graphql/public/root/mutation/feedback-submit" import UserEmailDeleteMutation from "@graphql/public/root/mutation/user-email-delete" import UserPhoneDeleteMutation from "@graphql/public/root/mutation/user-phone-delete" @@ -67,6 +69,8 @@ export const mutationFields = { captchaCreateChallenge: CaptchaCreateChallengeMutation, captchaRequestAuthCode: CaptchaRequestAuthCodeMutation, userLogin: UserLoginMutation, + newUserEmailRegistrationInitiate: NewUserEmailRegistrationInitiateMutation, + newUserEmailRegistrationValidate: NewUserEmailRegistrationValidateMutation, userLogout: UserLogoutMutation, lnInvoiceCreateOnBehalfOfRecipient: LnInvoiceCreateOnBehalfOfRecipientMutation, diff --git a/src/graphql/public/root/mutation/new-user-email-registration-initiate.ts b/src/graphql/public/root/mutation/new-user-email-registration-initiate.ts new file mode 100644 index 000000000..cbff425b1 --- /dev/null +++ b/src/graphql/public/root/mutation/new-user-email-registration-initiate.ts @@ -0,0 +1,64 @@ +import { GT } from "@graphql/index" +import EmailAddress from "@graphql/shared/types/scalar/email-address" +import { Authentication } from "@app" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import IError from "@graphql/shared/types/abstract/error" + +const NewUserEmailRegistrationInitiateInput = GT.Input({ + name: "NewUserEmailRegistrationInitiateInput", + fields: () => ({ + email: { + type: GT.NonNull(EmailAddress), + }, + }), +}) + +const NewUserEmailRegistrationInitiatePayload = GT.Object({ + name: "NewUserEmailRegistrationInitiatePayload", + fields: () => ({ + errors: { type: GT.NonNull(GT.List(GT.NonNull(IError))) }, + emailFlowId: { type: GT.String }, + }), +}) + +const NewUserEmailRegistrationInitiateMutation = GT.Field< + null, + GraphQLPublicContext, + { + input: { + email: EmailAddress | InputValidationError + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(NewUserEmailRegistrationInitiatePayload), + args: { + input: { type: GT.NonNull(NewUserEmailRegistrationInitiateInput) }, + }, + resolve: async (_, args, { ip }) => { + const { email } = args.input + + if (email instanceof Error) { + return { errors: [{ message: email.message }] } + } + + if (ip === undefined) { + return { errors: [{ message: "ip is undefined" }] } + } + + const flowId = await Authentication.requestEmailCode({ + email, + ip, + }) + + if (flowId instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(flowId)] } + } + + return { errors: [], emailFlowId: flowId } + }, +}) + +export default NewUserEmailRegistrationInitiateMutation \ No newline at end of file diff --git a/src/graphql/public/root/mutation/new-user-email-registration-validate.ts b/src/graphql/public/root/mutation/new-user-email-registration-validate.ts new file mode 100644 index 000000000..39c7ab0cc --- /dev/null +++ b/src/graphql/public/root/mutation/new-user-email-registration-validate.ts @@ -0,0 +1,93 @@ +import { GT } from "@graphql/index" +import OneTimeAuthCode from "@graphql/shared/types/scalar/one-time-auth-code" +import AuthTokenPayload from "@graphql/shared/types/payload/auth-token" +import { Authentication, Accounts } from "@app" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import { AuthWithEmailPasswordlessService } from "@services/kratos" +import { AccountsRepository } from "@services/mongoose" +import { getDefaultAccountsConfig } from "@config" + +const NewUserEmailRegistrationValidateInput = GT.Input({ + name: "NewUserEmailRegistrationValidateInput", + fields: () => ({ + emailFlowId: { + type: GT.NonNull(GT.String), + }, + code: { + type: GT.NonNull(OneTimeAuthCode), + }, + }), +}) + +const NewUserEmailRegistrationValidateMutation = GT.Field< + null, + GraphQLPublicContext, + { + input: { + emailFlowId: string + code: EmailCode | InputValidationError + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AuthTokenPayload), + args: { + input: { type: GT.NonNull(NewUserEmailRegistrationValidateInput) }, + }, + resolve: async (_, args, { ip }) => { + const { emailFlowId, code } = args.input + + if (code instanceof Error) { + return { errors: [{ message: code.message }] } + } + + if (ip === undefined) { + return { errors: [{ message: "ip is undefined" }] } + } + + // Validate the code with Kratos + const authService = AuthWithEmailPasswordlessService() + const validateResult = await authService.validateCode({ + emailFlowId: emailFlowId as EmailFlowId, + code, + }) + + if (validateResult instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(validateResult)] } + } + + const { email, kratosUserId, totpRequired } = validateResult + + // Check if account exists, if not create it + const accountsRepo = AccountsRepository() + let account = await accountsRepo.findByUserId(kratosUserId) + + if (account instanceof Error) { + // Account doesn't exist, create new account + const config = getDefaultAccountsConfig() + const accountResult = await Accounts.createAccountWithEmailIdentifier({ + newAccountInfo: { kratosUserId, email }, + config, + }) + + if (accountResult instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(accountResult)] } + } + } + + // Login with email to get auth token + const loginResult = await authService.loginToken({ email }) + + if (loginResult instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(loginResult)] } + } + + const { authToken } = loginResult + + return { errors: [], authToken, totpRequired } + }, +}) + +export default NewUserEmailRegistrationValidateMutation diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index 565b62094..f6bbbf3a9 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -897,6 +897,8 @@ type Mutation { lnUsdInvoiceCreateOnBehalfOfRecipient(input: LnUsdInvoiceCreateOnBehalfOfRecipientInput!): LnInvoicePayload! lnUsdInvoiceFeeProbe(input: LnUsdInvoiceFeeProbeInput!): CentAmountPayload! merchantMapSuggest(input: MerchantMapSuggestInput!): MerchantPayload! + newUserEmailRegistrationInitiate(input: NewUserEmailRegistrationInitiateInput!): NewUserEmailRegistrationInitiatePayload! + newUserEmailRegistrationValidate(input: NewUserEmailRegistrationValidateInput!): AuthTokenPayload! onChainAddressCreate(input: OnChainAddressCreateInput!): OnChainAddressPayload! onChainAddressCurrent(input: OnChainAddressCurrentInput!): OnChainAddressPayload! onChainPaymentSend(input: OnChainPaymentSendInput!): PaymentSendPayload! @@ -942,6 +944,20 @@ enum Network { testnet } +input NewUserEmailRegistrationInitiateInput { + email: EmailAddress! +} + +type NewUserEmailRegistrationInitiatePayload { + emailFlowId: String + errors: [Error!]! +} + +input NewUserEmailRegistrationValidateInput { + code: OneTimeAuthCode! + emailFlowId: String! +} + scalar NotificationCategory enum NotificationChannel { diff --git a/src/services/kratos/auth-email-no-password.ts b/src/services/kratos/auth-email-no-password.ts index f63157bce..dfb0faa6b 100644 --- a/src/services/kratos/auth-email-no-password.ts +++ b/src/services/kratos/auth-email-no-password.ts @@ -2,7 +2,7 @@ import { KRATOS_MASTER_USER_PASSWORD, KRATOS_PG_CON } from "@config" import { wrapAsyncFunctionsToRunInSpan } from "@services/tracing" -import { isAxiosError } from "axios" +import { isAxiosError, AxiosError } from "axios" import { EmailCodeInvalidError, @@ -59,6 +59,55 @@ const getIdentityIdFromFlowId = async (flowId: string) => { export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessService => { const password = KRATOS_MASTER_USER_PASSWORD + // createIdentityForEmailRegistration creates a new identity with email + // and sends an OTP code via recovery flow + const createIdentityForEmailRegistration = async ({ + email + }: { + email: EmailAddress + }): Promise<{ kratosUserId: UserId; flowId: EmailFlowId } | KratosError> => { + try { + let kratosUserId: UserId + + // Check if identity already exists + const existingIdentities = await kratosAdmin.listIdentities({ + credentialsIdentifier: email + }) + + if (existingIdentities.data.length > 0) { + kratosUserId = existingIdentities.data[0].id as UserId + } else { + // Create new identity with email + const createIdentityBody = { + credentials: { password: { config: { password } } }, + state: "active" as const, + traits: { email }, + schema_id: "email_no_password_v0", + } + + const { data: identity } = await kratosAdmin.createIdentity({ createIdentityBody }) + kratosUserId = identity.id as UserId + } + + // Send OTP code via recovery flow + const { data: recoveryFlow } = await kratosPublic.createNativeRecoveryFlow() + await kratosPublic.updateRecoveryFlow({ + flow: recoveryFlow.id, + updateRecoveryFlowBody: { + email, + method: "code", + }, + }) + + return { + kratosUserId, + flowId: recoveryFlow.id as EmailFlowId + } + } catch (err) { + return new UnknownKratosError(err) + } + } + // sendEmailWithCode return a flowId even if the user doesn't exist // this is to avoid account enumeration attacks const sendEmailWithCode = async ({ email }: { email: EmailAddress }) => { @@ -272,6 +321,25 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe } } + /** + * Adds email to an existing Kratos identity and upgrades schema + * + * Supports two upgrade paths: + * 1. Device account → Email account + * - Schema: username_password_deviceid_v0 → email_no_password_v0 + * - Traits: { username } → { email } + * - Note: username trait is REMOVED (not supported by target schema) + * - Note: MongoDB deviceId field is preserved separately + * + * 2. Phone account → Phone+Email account + * - Schema: phone_no_password_v0 → phone_email_no_password_v0 + * - Traits: { phone } → { phone, email } + * - Note: phone trait is PRESERVED + * + * @param kratosUserId - User ID in Kratos + * @param email - Email address to add + * @returns Updated identity or error + */ const addUnverifiedEmailToIdentity = async ({ kratosUserId, email, @@ -301,22 +369,38 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe return new UnknownKratosError(err.message || err) } - if (identity.schema_id !== SchemaIdType.PhoneNoPasswordV0) { + // Validate schema is compatible for email addition/upgrade + if ( + identity.schema_id !== SchemaIdType.PhoneNoPasswordV0 && + identity.schema_id !== SchemaIdType.UsernamePasswordDeviceIdV0 + ) { return new IncompatibleSchemaUpgradeError( - `current schema_id: ${identity.schema_id}, expected: ${SchemaIdType.PhoneNoPasswordV0}`, + `current schema_id: ${identity.schema_id}, expected: ${SchemaIdType.PhoneNoPasswordV0} or ${SchemaIdType.UsernamePasswordDeviceIdV0}`, ) } if (identity.state === undefined) throw new UnknownKratosError("state undefined, probably impossible state") // type issue - identity.traits = { ...identity.traits, email } + // Set traits based on upgrade path: + // - Device account: REPLACE traits (email_no_password_v0 schema only accepts email) + // - Phone account: ADD to existing traits (phone_email_no_password_v0 accepts both) + identity.traits = + identity.schema_id === SchemaIdType.UsernamePasswordDeviceIdV0 + ? { email } // Replace: removes username trait + : { ...identity.traits, email } // Add: keeps phone trait + + // Determine target schema based on current schema + const targetSchemaId = + identity.schema_id === SchemaIdType.UsernamePasswordDeviceIdV0 + ? SchemaIdType.EmailNoPasswordV0 + : SchemaIdType.PhoneEmailNoPasswordV0 const adminIdentity: UpdateIdentityBody = { ...identity, credentials: { password: { config: { password } } }, state: identity.state, - schema_id: SchemaIdType.PhoneEmailNoPasswordV0, + schema_id: targetSchemaId, } try { @@ -488,6 +572,7 @@ export const AuthWithEmailPasswordlessService = (): IAuthWithEmailPasswordlessSe removePhoneFromIdentity, addPhoneToIdentity, addUnverifiedEmailToIdentity, + createIdentityForEmailRegistration, sendEmailWithCode, validateCode, hasEmail, diff --git a/src/services/kratos/tests-but-not-prod.ts b/src/services/kratos/tests-but-not-prod.ts index 034055985..881098d2f 100644 --- a/src/services/kratos/tests-but-not-prod.ts +++ b/src/services/kratos/tests-but-not-prod.ts @@ -1,4 +1,4 @@ -import { IdentityState } from "@ory/client" +import { IdentityStateEnum } from "@ory/client" import { baseLogger } from "@services/logger" @@ -19,7 +19,7 @@ export const activateUser = async (kratosUserId: UserId): Promise { const phoneMetadata = user.phoneMetadata const phone = user.phone as PhoneNumber | undefined const deletedPhones = user.deletedPhones as PhoneNumber[] | undefined + const email = user.email as EmailAddress | undefined + const deletedEmails = user.deletedEmail as EmailAddress[] | undefined const createdAt = user.createdAt const deviceId = user.deviceId as DeviceId | undefined - const deletedEmails = user.deletedEmails as EmailAddress[] | undefined return { id: user.userId as UserId, @@ -21,9 +26,10 @@ export const translateToUser = (user: UserRecord): User => { phoneMetadata, phone, deletedPhones, + email, + deletedEmails, createdAt, deviceId, - deletedEmails, } } @@ -57,15 +63,27 @@ export const UsersRepository = (): IUsersRepository => { } } + const findByEmail = async (email: EmailAddress): Promise => { + try { + const result = await User.findOne({ email: email.toLowerCase() }) + if (!result) return new CouldNotFindUserFromEmailError() + + return translateToUser(result) + } catch (err) { + return parseRepositoryError(err) + } + } + const update = async ({ id, language, deviceTokens, phoneMetadata, phone, + email, deletedPhones, - deviceId, deletedEmails, + deviceId, }: UserUpdateInput): Promise => { const updateObject: Partial & { $unset?: { phone?: number; email?: number } @@ -85,6 +103,14 @@ export const UsersRepository = (): IUsersRepository => { updateObject.phone = phone } + // If the new email is undefined, unset it from the document + if (email === undefined) { + if (!updateObject.$unset) updateObject.$unset = {} + updateObject.$unset.email = 1 + } else { + updateObject.email = email?.toLowerCase() as EmailAddress + } + try { const result = await User.findOneAndUpdate({ userId: id }, updateObject, { new: true, @@ -99,9 +125,30 @@ export const UsersRepository = (): IUsersRepository => { } } + const addEmail = async ( + userId: UserId, + email: EmailAddress, + ): Promise => { + try { + const result = await User.findOneAndUpdate( + { userId }, + { $set: { email: email.toLowerCase() as EmailAddress } }, + { new: true, upsert: true }, + ) + if (!result) { + return new RepositoryError("Couldn't update user with email") + } + return translateToUser(result) + } catch (err) { + return parseRepositoryError(err) + } + } + return { findById, findByPhone, + findByEmail, update, + addEmail, } } diff --git a/test/flash/integration/jest.setup.ts b/test/flash/integration/jest.setup.ts index a0cdbc16b..cde49b67e 100644 --- a/test/flash/integration/jest.setup.ts +++ b/test/flash/integration/jest.setup.ts @@ -1,6 +1,15 @@ import { disconnectAll } from "@services/redis" import { setupMongoConnection } from "@services/mongodb" -import { createMandatoryUsers, createRandomUserAndWallets, createUser, getUser, createUserAndWallet, TestUser, getUsdWalletDescriptorByPhone, getAccountByPhone } from "test/galoy/helpers" +import { + createMandatoryUsers, + createRandomUserAndWallets, + createUser, + getUser, + createUserAndWallet, + TestUser, + getUsdWalletDescriptorByPhone, + getAccountByPhone, +} from "test/galoy/helpers" let mongoose export let flash // : TestUser @@ -8,9 +17,8 @@ export let alice // : TestUser export let bob //: TestUser // Mock prices -jest.mock( - "@app/prices/get-current-price", - () => require("test/flash/mocks/get-current-price"), +jest.mock("@app/prices/get-current-price", () => + require("test/flash/mocks/get-current-price"), ) import Ibex from "@services/ibex/client" @@ -18,13 +26,12 @@ jest.mock( "@services/ibex/client", // () => require("test/flash/mocks/ibex"), ) -let mockedIbex: jest.Mock +export let mockedIbex: jest.Mock beforeAll(async () => { - - mockedIbex = Ibex as jest.Mock + mockedIbex = Ibex as unknown as jest.Mock mockedIbex.mockReturnValue({ - createAccount: jest.fn().mockResolvedValue(createAccount.response), + createAccount: jest.fn().mockResolvedValue({ id: "mock-account-id" }), // addInvoice: jest.fn().mockResolvedValue(addInvoice.response), // payInvoiceV2: jest.fn().mockResolvedValue(payInvoiceV2.response) @@ -32,7 +39,7 @@ beforeAll(async () => { mongoose = await setupMongoConnection(true) const admins = await createMandatoryUsers() - const owner = admins.find(a => a.role === "bankowner") + const owner = admins.find((a) => a.role === "bankowner") if (!owner) throw new Error("Initialization failed: Bank owner not found.") flash = { account: await getAccountByPhone(owner.phone), @@ -40,7 +47,6 @@ beforeAll(async () => { } alice = await createUser() bob = await createUser() - }) // Would be nice to clean-up Ibex accounts, but Ibex API does not have delete diff --git a/test/flash/integration/offers/execute-offer.spec.ts b/test/flash/integration/offers/execute-offer.spec.ts index 6d09a675c..850857b2f 100644 --- a/test/flash/integration/offers/execute-offer.spec.ts +++ b/test/flash/integration/offers/execute-offer.spec.ts @@ -5,11 +5,13 @@ import { RepositoryError } from "@domain/errors" // import { mockedIbex } from "../jest.setup" import * as Mocks from "test/flash/mocks/ibex" import Ibex from "@services/ibex/client" +import { USDAmount } from "@domain/shared" -const send = { - amount: 100n, - currency: "USD" -} as Amount<"USD"> +const send = (() => { + const amount = USDAmount.cents(10000n) + if (amount instanceof Error) throw amount + return amount +})() // jest.mock( // "@services/ibex/client", @@ -19,10 +21,8 @@ const send = { beforeAll(async () => { // Mocking the http call would be more useful, but adds complexity to tests // mockedIbex = Ibex as jest.Mock // move to beforeAll - // await Ibex().getAccountDetails({ accountId: walletId }) // mockedIbex.mockReset() - // jest.spyOn(mockedIbex, 'getAccountDetails').mockImplementation(() => { // }); }) @@ -37,19 +37,18 @@ beforeEach(async () => { }) afterEach(async () => { - jest.clearAllMocks() + jest.clearAllMocks() }) describe("Offers", () => { it("successfully makes and executes an offer", async () => { - const manager = new OffersManager() - const offer = await manager.makeCashoutOffer(alice.usdWalletD.id, send) + const offer = await OffersManager.createCashoutOffer(alice.usdWalletD.id, send) if (offer instanceof Error) throw offer - + const { id } = offer - const status = await manager.executeOffer(id) - + const status = await OffersManager.executeCashout(id, alice.usdWalletD.id) + // make assertions against ledger console.log(`status = ${status}`) }) -}) \ No newline at end of file +}) diff --git a/test/flash/integration/offers/make-cashout-offer.spec.ts b/test/flash/integration/offers/make-cashout-offer.spec.ts index d9b5dd3e4..9cc7801e3 100644 --- a/test/flash/integration/offers/make-cashout-offer.spec.ts +++ b/test/flash/integration/offers/make-cashout-offer.spec.ts @@ -5,11 +5,13 @@ import { RepositoryError } from "@domain/errors" // import { mockedIbex } from "../jest.setup" import * as Mocks from "test/flash/mocks/ibex" import Ibex from "@services/ibex/client" +import { USDAmount } from "@domain/shared" -const send = { - amount: 101n, - currency: "USD" -} as Amount<"USD"> +const send = (() => { + const amount = USDAmount.cents(10100n) + if (amount instanceof Error) throw amount + return amount +})() jest.mock( "@services/ibex/client", @@ -18,7 +20,7 @@ jest.mock( let mockedIbex: jest.Mock beforeAll(async () => { // Mocking the http call would be more useful, but adds complexity to tests - mockedIbex = Ibex as jest.Mock // move to beforeAll + mockedIbex = Ibex as unknown as jest.Mock // move to beforeAll // await Ibex().getAccountDetails({ accountId: walletId }) // mockedIbex.mockReset() @@ -29,7 +31,7 @@ beforeAll(async () => { beforeEach(async () => { const getAccountDetailsMock = jest.fn().mockResolvedValue( - Mocks.account.response // override the balance + Mocks.account.response, // override the balance ) mockedIbex.mockReturnValue({ getAccountDetails: getAccountDetailsMock, @@ -37,20 +39,17 @@ beforeEach(async () => { }) afterEach(async () => { - jest.clearAllMocks() + jest.clearAllMocks() }) describe("Offers", () => { it("successfully makes and persists an offer using default config", async () => { - const offer = await (new OffersManager().makeCashoutOffer(alice.usdWalletD.id, send)) - + const offer = await OffersManager.createCashoutOffer(alice.usdWalletD.id, send) + if (offer instanceof Error) throw offer - expect(offer.ibexTransfer.amount).toEqual(send.amount) - expect(offer.flashFee.amount).toEqual(2n) - expect(offer.flashFee.currency).toEqual("USD") - expect(offer.usdLiability.amount).toEqual(99n) - expect(offer.usdLiability.currency).toEqual("USD") - expect(offer.jmdLiability.amount).toEqual(157n) - expect(offer.jmdLiability.currency).toEqual("JMD") + expect(offer.details.ibexTrx.usd.asCents()).toEqual(send.asCents()) + expect(offer.details.flash.fee.asCents()).toEqual(2n) + expect(offer.details.flash.liability.usd.asCents()).toEqual(99n) + expect(offer.details.flash.liability.jmd.asCents()).toEqual(157n) }) -}) \ No newline at end of file +}) diff --git a/test/flash/unit/domain/ledger/activity-checker.spec.ts b/test/flash/unit/domain/ledger/activity-checker.spec.ts index dd1ba6870..8866a757c 100644 --- a/test/flash/unit/domain/ledger/activity-checker.spec.ts +++ b/test/flash/unit/domain/ledger/activity-checker.spec.ts @@ -18,6 +18,7 @@ beforeAll(() => { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } usdWallet = { @@ -27,6 +28,7 @@ beforeAll(() => { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } }) diff --git a/test/flash/unit/domain/ledger/imbalance-calculator.spec.ts b/test/flash/unit/domain/ledger/imbalance-calculator.spec.ts index 1813a69e3..1f49503d0 100644 --- a/test/flash/unit/domain/ledger/imbalance-calculator.spec.ts +++ b/test/flash/unit/domain/ledger/imbalance-calculator.spec.ts @@ -11,6 +11,7 @@ const btcWallet: Wallet = { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const VolumeAfterLightningReceiptFn = () => diff --git a/test/flash/unit/domain/wallets/payment-input-validator.spec.ts b/test/flash/unit/domain/wallets/payment-input-validator.spec.ts index 522043df4..1a73fd3ad 100644 --- a/test/flash/unit/domain/wallets/payment-input-validator.spec.ts +++ b/test/flash/unit/domain/wallets/payment-input-validator.spec.ts @@ -35,6 +35,7 @@ describe("PaymentInputValidator", () => { quiz: [], kratosUserId: "kratosUserId" as UserId, displayCurrency: UsdDisplayCurrency, + npub: "npub1mock" as Npub, } const dummySenderWallet: Wallet = { @@ -44,6 +45,7 @@ describe("PaymentInputValidator", () => { currency: WalletCurrency.Btc, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const dummyRecipientWallet: Wallet = { @@ -53,6 +55,7 @@ describe("PaymentInputValidator", () => { currency: WalletCurrency.Btc, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const wallets: { [key: WalletId]: Wallet } = {} @@ -113,6 +116,7 @@ describe("PaymentInputValidator", () => { senderAccount: { ...dummyAccount, status: AccountStatus.Locked, + npub: "npub1mock" as Npub, }, recipientWalletId: dummyRecipientWallet.id, }) diff --git a/test/galoy/helpers/lightning.ts b/test/galoy/helpers/lightning.ts index 4ca80e813..407922224 100644 --- a/test/galoy/helpers/lightning.ts +++ b/test/galoy/helpers/lightning.ts @@ -412,8 +412,14 @@ export const fundWalletIdFromLightning = async ({ const invoice = wallet.currency === WalletCurrency.Btc - ? await Wallets.addInvoiceForSelfForBtcWallet({ walletId, amount }) - : await Wallets.addInvoiceForSelfForUsdWallet({ walletId, amount }) + ? await Wallets.addInvoiceForSelfForUsdWallet({ + walletId, + amount: amount as FractionalCentAmount, + }) + : await Wallets.addInvoiceForSelfForUsdWallet({ + walletId, + amount: amount as FractionalCentAmount, + }) if (invoice instanceof Error) return invoice safePay({ lnd: lndOutside1, request: invoice.paymentRequest }) diff --git a/test/galoy/helpers/user.ts b/test/galoy/helpers/user.ts index cc7008411..e41a0a9f8 100644 --- a/test/galoy/helpers/user.ts +++ b/test/galoy/helpers/user.ts @@ -3,7 +3,7 @@ import { addWalletIfNonexistent } from "@app/accounts/add-wallet" import { getAdminAccounts, getDefaultAccountsConfig } from "@config" import { CouldNotFindAccountFromKratosIdError, CouldNotFindError } from "@domain/errors" -import { UsdWalletDescriptor, WalletCurrency } from "@domain/shared" +import { BtcWalletDescriptor, UsdWalletDescriptor, WalletCurrency } from "@domain/shared" import { WalletType } from "@domain/wallets" import { @@ -231,20 +231,27 @@ export const getUser = async (walletD: WalletDescripto export const createRandomUserAndWallets = async (): Promise<{ usdWalletDescriptor: WalletDescriptor<"USD"> - // btcWalletDescriptor: WalletDescriptor<"BTC"> + btcWalletDescriptor: WalletDescriptor<"BTC"> }> => { const phone = randomPhone() - const btcWalletDescriptor = await createUserAndWallet(phone) + const defaultWalletDescriptor = await createUserAndWallet(phone) const usdWallet = await addWalletIfNonexistent({ currency: WalletCurrency.Usd, - accountId: btcWalletDescriptor.accountId, + accountId: defaultWalletDescriptor.accountId, type: WalletType.Checking, }) if (usdWallet instanceof Error) throw usdWallet + const btcWallet = await addWalletIfNonexistent({ + currency: WalletCurrency.Btc, + accountId: defaultWalletDescriptor.accountId, + type: WalletType.Checking, + }) + if (btcWallet instanceof Error) throw btcWallet + return { - // btcWalletDescriptor, + btcWalletDescriptor: BtcWalletDescriptor(btcWallet.id), usdWalletDescriptor: { id: usdWallet.id, currency: WalletCurrency.Usd, @@ -361,11 +368,11 @@ export const createAndFundNewWallet = async ({ if (balanceAmount.amount === 0n) return wallet const addInvoiceFn = wallet.currency === WalletCurrency.Btc - ? Wallets.addInvoiceForSelfForBtcWallet + ? Wallets.addInvoiceForSelfForUsdWallet : Wallets.addInvoiceForSelfForUsdWallet const lnInvoice = await addInvoiceFn({ walletId: wallet.id, - amount: Number(balanceAmount.amount), + amount: Number(balanceAmount.amount) as FractionalCentAmount, memo: `Fund new wallet ${wallet.id}`, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -406,11 +413,11 @@ export const fundWallet = async ({ const addInvoiceFn = wallet.currency === WalletCurrency.Btc - ? Wallets.addInvoiceForSelfForBtcWallet + ? Wallets.addInvoiceForSelfForUsdWallet : Wallets.addInvoiceForSelfForUsdWallet const lnInvoice = await addInvoiceFn({ walletId: wallet.id, - amount: Number(balanceAmount.amount), + amount: Number(balanceAmount.amount) as FractionalCentAmount, memo: `Fund new wallet ${wallet.id}`, }) if (lnInvoice instanceof Error) throw lnInvoice diff --git a/test/galoy/helpers/wallet.ts b/test/galoy/helpers/wallet.ts index cae452dca..07456620e 100644 --- a/test/galoy/helpers/wallet.ts +++ b/test/galoy/helpers/wallet.ts @@ -10,7 +10,7 @@ export const getBalanceHelper = async ( ): Promise => { const balance = await getBalanceForWallet({ walletId }) if (balance instanceof Error) throw balance - return balance + return balance as unknown as CurrencyBaseAmount } export const getTransactionsForWalletId = async ( @@ -19,7 +19,9 @@ export const getTransactionsForWalletId = async ( const wallets = WalletsRepository() const wallet = await wallets.findById(walletId) if (wallet instanceof RepositoryError) return PartialResult.err(wallet) - return getTransactionsForWallets({ wallets: [wallet] }) + return getTransactionsForWallets({ wallets: [wallet] }) as unknown as Promise< + PartialResult> + > } // This is to test detection of funds coming in on legacy addresses diff --git a/test/galoy/legacy-integration/02-user-wallet/02-a-user-setup.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-a-user-setup.spec.ts index 8ef1c5e97..766cb3553 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-a-user-setup.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-a-user-setup.spec.ts @@ -5,7 +5,7 @@ import { setUsername } from "@app/accounts" import { UsernameIsImmutableError, UsernameNotAvailableError } from "@domain/accounts" import { ValidationError } from "@domain/shared" import { CsvWalletsExport } from "@services/ledger/csv-wallet-export" -import { AccountsRepository } from "@services/mongoose" +import { AccountsRepository, WalletsRepository } from "@services/mongoose" import { Account } from "@services/mongoose/schema" import { @@ -149,7 +149,10 @@ describe("UserWallet", () => { "id,walletId,type,credit,debit,fee,currency,timestamp,pendingConfirmation,journalId,lnMemo,usd,feeUsd,recipientWalletId,username,memoFromPayer,paymentHash,pubkey,feeKnownInAdvance,address,txHash" it("exports to csv", async () => { const csv = new CsvWalletsExport() - await csv.addWallet(walletIdA) + const walletsRepo = WalletsRepository() + const wallet = await walletsRepo.findById(walletIdA) + if (wallet instanceof Error) throw wallet + await csv.addWallets([wallet]) const base64Data = csv.getBase64() expect(typeof base64Data).toBe("string") const data = Buffer.from(base64Data, "base64") diff --git a/test/galoy/legacy-integration/02-user-wallet/02-bria.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-bria.spec.ts index 96e30d441..96854acf4 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-bria.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-bria.spec.ts @@ -1,6 +1,7 @@ import { Wallets } from "@app" import { sat2btc, toSats } from "@domain/bitcoin" +import { checkedToOnChainAddress } from "@domain/bitcoin/onchain" import { UnknownRepositoryError } from "@domain/errors" import { utxoSettledEventHandler } from "@servers/event-handlers/bria" @@ -49,11 +50,13 @@ describe("BriaSubscriber", () => { it("receives utxo events", async () => { const amountSats = toSats(5_000) // Receive onchain - const address = await Wallets.createOnChainAddress({ + const addressStr = await Wallets.createOnChainAddress({ walletId: walletIdA, }) + if (addressStr instanceof Error) throw addressStr + expect(addressStr.substring(0, 4)).toBe("bcrt") + const address = checkedToOnChainAddress({ network: "regtest", value: addressStr }) if (address instanceof Error) throw address - expect(address.substring(0, 4)).toBe("bcrt") let expectedTxId: string | Error = "" expectedTxId = await sendToAddressAndConfirm({ @@ -117,11 +120,13 @@ describe("BriaSubscriber", () => { const amountSats = toSats(5_000) // Receive onchain - const address = await Wallets.createOnChainAddress({ + const addressStr = await Wallets.createOnChainAddress({ walletId: walletIdA, }) + if (addressStr instanceof Error) throw addressStr + expect(addressStr.substring(0, 4)).toBe("bcrt") + const address = checkedToOnChainAddress({ network: "regtest", value: addressStr }) if (address instanceof Error) throw address - expect(address.substring(0, 4)).toBe("bcrt") const expectedTxId = await sendToAddressAndConfirm({ walletClient: bitcoindOutside, diff --git a/test/galoy/legacy-integration/02-user-wallet/02-receive-lightning.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-receive-lightning.spec.ts index 02caf1684..8dbbd1834 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-receive-lightning.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-receive-lightning.spec.ts @@ -75,9 +75,9 @@ describe("UserWallet - Lightning", () => { const sats = 500_000 const memo = "myMemo" - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdB, - amount: toSats(sats), + amount: toSats(sats) as unknown as FractionalCentAmount, memo, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -130,7 +130,7 @@ describe("UserWallet - Lightning", () => { const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdUsdB as WalletId, - amount: toCents(cents), + amount: toCents(cents) as unknown as FractionalCentAmount, memo, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -208,9 +208,9 @@ describe("UserWallet - Lightning", () => { const sats = 25000 const memo = "myBtcMemo" - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdB as WalletId, - amount: toSats(sats), + amount: toSats(sats) as unknown as FractionalCentAmount, memo, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -284,9 +284,9 @@ describe("Invoice handling from trigger", () => { it("should process held invoice when trigger comes back up", async () => { // Create invoice for self - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdF, - amount: sats, + amount: sats as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) if (lnInvoice instanceof Error) throw lnInvoice @@ -324,9 +324,9 @@ describe("Invoice handling from trigger", () => { it("should process new invoice payment when trigger comes back up", async () => { // Create invoice for self - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdF, - amount: sats, + amount: sats as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) if (lnInvoice instanceof Error) throw lnInvoice @@ -358,7 +358,7 @@ describe("Invoice handling from trigger", () => { // Create invoice for self const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdUsdF, - amount: cents, + amount: cents as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) if (lnInvoice instanceof Error) throw lnInvoice @@ -398,7 +398,7 @@ describe("Invoice handling from trigger", () => { // Create invoice for self const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdUsdF, - amount: cents, + amount: cents as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) if (lnInvoice instanceof Error) throw lnInvoice @@ -444,7 +444,7 @@ describe("Invoice handling from trigger", () => { // Create invoice for self const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdUsdF, - amount: cents, + amount: cents as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) if (lnInvoice instanceof Error) throw lnInvoice diff --git a/test/galoy/legacy-integration/02-user-wallet/02-send-lightning-limits.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-send-lightning-limits.spec.ts index 29a844342..df8cc7dce 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-send-lightning-limits.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-send-lightning-limits.spec.ts @@ -153,9 +153,9 @@ describe("UserWallet Limits - Lightning Pay", () => { ) if (senderAccount instanceof Error) throw senderAccount - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: otherBtcWallet.id, - amount: Number(btcThresholdAmount.amount), + amount: Number(btcThresholdAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -211,7 +211,7 @@ describe("UserWallet Limits - Lightning Pay", () => { { const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: usdWalletDescriptor.id, - amount: Number(usdPaymentAmount.amount), + amount: Number(usdPaymentAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -231,9 +231,9 @@ describe("UserWallet Limits - Lightning Pay", () => { // Test USD -> BTC limits { - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: btcWalletDescriptor.id, - amount: Number(btcThresholdAmount.amount), + amount: Number(btcThresholdAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequestBtc } = lnInvoice @@ -314,9 +314,9 @@ describe("UserWallet Limits - Lightning Pay", () => { numPayments, }) for (let i = 0; i < numPayments; i++) { - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: otherBtcWallet.id, - amount: Number(partialBtcSendAmount.amount), + amount: Number(partialBtcSendAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -340,9 +340,11 @@ describe("UserWallet Limits - Lightning Pay", () => { { // Fails for payment just above limit - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: otherBtcWallet.id, - amount: Number(btcAmountAboveThreshold.amount), + amount: Number( + btcAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -393,7 +395,9 @@ describe("UserWallet Limits - Lightning Pay", () => { // Succeeds for same payment just above tradeIntraAccount limit const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: usdWalletDescriptor.id, - amount: Number(usdAmountAboveThreshold.amount), + amount: Number( + usdAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -446,7 +450,7 @@ describe("UserWallet Limits - Lightning Pay", () => { { const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: usdWalletDescriptor.id, - amount: Number(partialUsdSendAmount.amount), + amount: Number(partialUsdSendAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -461,9 +465,9 @@ describe("UserWallet Limits - Lightning Pay", () => { expect(paymentResult).toBe(PaymentSendStatus.Success) } { - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: btcWalletDescriptor.id, - amount: Number(partialBtcSendAmount.amount), + amount: Number(partialBtcSendAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -489,7 +493,9 @@ describe("UserWallet Limits - Lightning Pay", () => { // Fails for payment just above limit, from btc const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: usdWalletDescriptor.id, - amount: Number(usdAmountAboveThreshold.amount), + amount: Number( + usdAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -509,9 +515,11 @@ describe("UserWallet Limits - Lightning Pay", () => { { // Fails for payment just above limit, from usd - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: btcWalletDescriptor.id, - amount: Number(btcAmountAboveThreshold.amount), + amount: Number( + btcAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -548,9 +556,11 @@ describe("UserWallet Limits - Lightning Pay", () => { { // Succeeds for same payment just above intraledger limit - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: otherBtcWallet.id, - amount: Number(btcAmountAboveThreshold.amount), + amount: Number( + btcAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -645,9 +655,11 @@ describe("UserWallet Limits - Lightning Pay", () => { { // Succeeds for same payment just above intraledger limit - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: otherBtcWallet.id, - amount: Number(btcAmountAboveThreshold.amount), + amount: Number( + btcAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice @@ -666,7 +678,9 @@ describe("UserWallet Limits - Lightning Pay", () => { // Succeeds for same payment just above tradeIntraAccount limit const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: usdWalletDescriptor.id, - amount: Number(usdAmountAboveThreshold.amount), + amount: Number( + usdAmountAboveThreshold.amount, + ) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: uncheckedPaymentRequest } = lnInvoice diff --git a/test/galoy/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 3f899496c..6625ce929 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -14,7 +14,7 @@ import { UsdDisplayCurrency, displayAmountFromNumber } from "@domain/fiat" import { updateDisplayCurrency } from "@app/accounts" -import { PayoutSpeed } from "@domain/bitcoin/onchain" +import { PayoutSpeed, checkedToOnChainAddress } from "@domain/bitcoin/onchain" import { translateToLedgerTx } from "@services/ledger" import { MainBook, Transaction } from "@services/ledger/books" @@ -212,9 +212,9 @@ describe("Display properties on transactions", () => { // Receive payment const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: recipientWalletId, - amount: amountInvoice, + amount: amountInvoice as unknown as FractionalCentAmount, memo, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -285,9 +285,9 @@ describe("Display properties on transactions", () => { // Send payment const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: recipientWalletId, - amount: amountInvoice, + amount: amountInvoice as unknown as FractionalCentAmount, memo, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -650,11 +650,13 @@ describe("Display properties on transactions", () => { amountSats: Satoshis walletId: WalletId }) => { - const address = await Wallets.createOnChainAddress({ + const addressStr = await Wallets.createOnChainAddress({ walletId, }) + if (addressStr instanceof Error) throw addressStr + expect(addressStr.substring(0, 4)).toBe("bcrt") + const address = checkedToOnChainAddress({ network: "regtest", value: addressStr }) if (address instanceof Error) throw address - expect(address.substring(0, 4)).toBe("bcrt") const txId = await sendToAddressAndConfirm({ walletClient: bitcoindOutside, diff --git a/test/galoy/legacy-integration/02-user-wallet/02-tx-onchain-fees.spec.ts b/test/galoy/legacy-integration/02-user-wallet/02-tx-onchain-fees.spec.ts index fe67eedc5..7248788d4 100644 --- a/test/galoy/legacy-integration/02-user-wallet/02-tx-onchain-fees.spec.ts +++ b/test/galoy/legacy-integration/02-user-wallet/02-tx-onchain-fees.spec.ts @@ -6,7 +6,7 @@ import { sat2btc, toSats } from "@domain/bitcoin" import { LessThanDustThresholdError } from "@domain/errors" import { toCents } from "@domain/fiat" import { WalletCurrency, paymentAmountFromNumber } from "@domain/shared" -import { PayoutSpeed } from "@domain/bitcoin/onchain" +import { PayoutSpeed, checkedToOnChainAddress } from "@domain/bitcoin/onchain" import { DealerPriceService } from "@services/dealer-price" import { AccountsRepository, WalletsRepository } from "@services/mongoose" @@ -140,9 +140,11 @@ describe("UserWallet - getOnchainFee", () => { }) it("returns zero for an on us address", async () => { - const address = await Wallets.createOnChainAddress({ + const addressStr = await Wallets.createOnChainAddress({ walletId: walletIdB, }) + if (addressStr instanceof Error) throw addressStr + const address = checkedToOnChainAddress({ network: "regtest", value: addressStr }) if (address instanceof Error) throw address const feeAmount = await Wallets.getOnChainFeeForBtcWallet({ walletId: walletIdA, @@ -259,9 +261,14 @@ describe("UserWallet - getOnchainFee", () => { }) it("returns zero for an on us address", async () => { - const address = await Wallets.createOnChainAddress({ + const addressStr = await Wallets.createOnChainAddress({ walletId: walletIdB, }) + if (addressStr instanceof Error) throw addressStr + const address = checkedToOnChainAddress({ + network: "regtest", + value: addressStr, + }) if (address instanceof Error) throw address const getFeeArgs = { diff --git a/test/galoy/legacy-integration/app/wallets/invoice.spec.ts b/test/galoy/legacy-integration/app/wallets/invoice.spec.ts index e5e568db7..ef16dfce0 100644 --- a/test/galoy/legacy-integration/app/wallets/invoice.spec.ts +++ b/test/galoy/legacy-integration/app/wallets/invoice.spec.ts @@ -59,9 +59,9 @@ describe("Wallet - addInvoice BTC", () => { it("add a self generated invoice", async () => { const amountInput = 1000 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdBtc, - amount: amountInput, + amount: amountInput as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -105,9 +105,9 @@ describe("Wallet - addInvoice BTC", () => { it("adds a public with amount invoice", async () => { const amountInput = 10 - const lnInvoice = await Wallets.addInvoiceForRecipientForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForRecipientForUsdWallet({ recipientWalletId: walletIdBtc, - amount: amountInput, + amount: amountInput as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -163,7 +163,7 @@ describe("Wallet - addInvoice USD", () => { const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdUsd, - amount: toCents(centsInput), + amount: toCents(centsInput) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -216,7 +216,7 @@ describe("Wallet - addInvoice USD", () => { const lnInvoice = await Wallets.addInvoiceForRecipientForUsdWallet({ recipientWalletId: walletIdUsd, - amount: toCents(centsInput), + amount: toCents(centsInput) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice const { paymentRequest: request } = lnInvoice @@ -274,9 +274,9 @@ describe("Wallet - rate limiting test", () => { const promises: Promise[] = [] for (let i = 0; i < limitsNum; i++) { - const lnInvoicePromise = Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoicePromise = Wallets.addInvoiceForSelfForUsdWallet({ walletId: walletIdBtc, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) promises.push(lnInvoicePromise) } @@ -319,9 +319,9 @@ describe("Wallet - rate limiting test", () => { const limitsNum = getInvoiceCreateForRecipientAttemptLimits().points const promises: Promise[] = [] for (let i = 0; i < limitsNum; i++) { - const lnInvoicePromise = Wallets.addInvoiceForRecipientForBtcWallet({ + const lnInvoicePromise = Wallets.addInvoiceForRecipientForUsdWallet({ recipientWalletId: walletIdBtc, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) promises.push(lnInvoicePromise) } @@ -369,9 +369,9 @@ const testPastSelfInvoiceLimits = async ({ accountId: AccountId }) => { // Test that first invoice past the limit fails - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) expect(lnInvoice).toBeInstanceOf(InvoiceCreateRateLimiterExceededError) @@ -381,9 +381,9 @@ const testPastSelfInvoiceLimits = async ({ expect(lnNoAmountInvoice).toBeInstanceOf(InvoiceCreateRateLimiterExceededError) // Test that recipient invoices still work - const lnRecipientInvoice = await Wallets.addInvoiceForRecipientForBtcWallet({ + const lnRecipientInvoice = await Wallets.addInvoiceForRecipientForUsdWallet({ recipientWalletId: walletId, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) expect(lnRecipientInvoice).not.toBeInstanceOf(Error) expect(lnRecipientInvoice).toHaveProperty("paymentRequest") @@ -409,9 +409,9 @@ const testPastRecipientInvoiceLimits = async ({ accountId: AccountId }) => { // Test that first invoice past the limit fails - const lnRecipientInvoice = await Wallets.addInvoiceForRecipientForBtcWallet({ + const lnRecipientInvoice = await Wallets.addInvoiceForRecipientForUsdWallet({ recipientWalletId: walletId, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) expect(lnRecipientInvoice).toBeInstanceOf( InvoiceCreateForRecipientRateLimiterExceededError, @@ -425,9 +425,9 @@ const testPastRecipientInvoiceLimits = async ({ ) // Test that recipient invoices still work - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId, - amount: 1000, + amount: 1000 as unknown as FractionalCentAmount, }) expect(lnInvoice).not.toBeInstanceOf(Error) expect(lnInvoice).toHaveProperty("paymentRequest") diff --git a/test/galoy/legacy-integration/services/dealer/hedge-price.spec.ts b/test/galoy/legacy-integration/services/dealer/hedge-price.spec.ts index 1a423d151..c62735127 100644 --- a/test/galoy/legacy-integration/services/dealer/hedge-price.spec.ts +++ b/test/galoy/legacy-integration/services/dealer/hedge-price.spec.ts @@ -8,7 +8,11 @@ import { AmountCalculator, paymentAmountFromNumber, WalletCurrency } from "@doma import { baseLogger } from "@services/logger" import { AccountsRepository } from "@services/mongoose" -import { createAccount, createAndFundNewWallet, getBalanceHelper } from "test/galoy/helpers" +import { + createAccount, + createAndFundNewWallet, + getBalanceHelper, +} from "test/galoy/helpers" class ZeroAmountForUsdRecipientError extends Error {} @@ -122,9 +126,9 @@ const getUsdEquivalentForWithAmountInvoiceSendToBtc = async ({ accountAndWallets: AccountAndWallets }): Promise => { const { newBtcWallet, newUsdWallet, newAccount } = accountAndWallets - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(btcPaymentAmount.amount), + amount: toSats(btcPaymentAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -149,9 +153,9 @@ const getUsdEquivalentForWithAmountInvoiceProbeAndSendToBtc = async ({ }): Promise => { const { newBtcWallet, newUsdWallet, newAccount } = accountAndWallets - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(btcPaymentAmount.amount), + amount: toSats(btcPaymentAmount.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -401,9 +405,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -419,7 +423,7 @@ describe("arbitrage strategies", () => { // Step 3: Replenish USD from BTC wallet with $0.01 invoice const lnInvoiceUsd = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newUsdWallet.id, - amount: toCents(1), + amount: toCents(1) as unknown as FractionalCentAmount, }) if (lnInvoiceUsd instanceof Error) throw lnInvoiceUsd @@ -465,9 +469,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -483,7 +487,7 @@ describe("arbitrage strategies", () => { // Step 3: Replenish USD from BTC wallet with $0.01 invoice const lnInvoiceUsd = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newUsdWallet.id, - amount: toCents(1), + amount: toCents(1) as unknown as FractionalCentAmount, }) if (lnInvoiceUsd instanceof Error) throw lnInvoiceUsd @@ -546,9 +550,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -618,9 +622,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -690,9 +694,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -765,9 +769,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -789,7 +793,7 @@ describe("arbitrage strategies", () => { // Step 3: Replenish USD from BTC wallet with $0.01 invoice const lnInvoiceUsd = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newUsdWallet.id, - amount: toCents(1), + amount: toCents(1) as unknown as FractionalCentAmount, }) if (lnInvoiceUsd instanceof Error) throw lnInvoiceUsd @@ -835,9 +839,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -859,7 +863,7 @@ describe("arbitrage strategies", () => { // Step 3: Replenish USD from BTC wallet with $0.01 invoice const lnInvoiceUsd = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newUsdWallet.id, - amount: toCents(1), + amount: toCents(1) as unknown as FractionalCentAmount, }) if (lnInvoiceUsd instanceof Error) throw lnInvoiceUsd @@ -922,9 +926,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -1000,9 +1004,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -1078,9 +1082,9 @@ describe("arbitrage strategies", () => { const usdBalanceBefore = await getBalanceHelper(newUsdWallet.id) // Step 1: Create invoice from BTC Wallet using discovered 'maxBtcAmountToEarn' from $0.01 - const lnInvoice = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoice = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoice instanceof Error) throw lnInvoice @@ -1427,9 +1431,9 @@ describe("arbitrage strategies", () => { if (paid instanceof Error) throw paid // Step 2: Pay back $0.01 from USD to BTC wallet - const lnInvoiceBtc = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoiceBtc = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoiceBtc instanceof Error) throw lnInvoiceBtc @@ -1498,9 +1502,9 @@ describe("arbitrage strategies", () => { if (paid instanceof Error) throw paid // Step 2: Pay back $0.01 from USD to BTC wallet - const lnInvoiceBtc = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoiceBtc = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoiceBtc instanceof Error) throw lnInvoiceBtc @@ -1788,9 +1792,9 @@ describe("arbitrage strategies", () => { if (paid instanceof Error) throw paid // Step 2: Pay back $0.01 from USD to BTC wallet - const lnInvoiceBtc = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoiceBtc = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoiceBtc instanceof Error) throw lnInvoiceBtc @@ -1866,9 +1870,9 @@ describe("arbitrage strategies", () => { if (paid instanceof Error) throw paid // Step 2: Pay back $0.01 from USD to BTC wallet - const lnInvoiceBtc = await Wallets.addInvoiceForSelfForBtcWallet({ + const lnInvoiceBtc = await Wallets.addInvoiceForSelfForUsdWallet({ walletId: newBtcWallet.id, - amount: toSats(maxBtcAmountToEarn.amount), + amount: toSats(maxBtcAmountToEarn.amount) as unknown as FractionalCentAmount, }) if (lnInvoiceBtc instanceof Error) throw lnInvoiceBtc diff --git a/test/galoy/mocks/index.ts b/test/galoy/mocks/index.ts index d9d61a456..cb35198df 100644 --- a/test/galoy/mocks/index.ts +++ b/test/galoy/mocks/index.ts @@ -1,7 +1,3 @@ -import SendToAddressV2 from "./ibex/send-to-address-v2" - export default { - ibex: { - SendToAddressV2, - }, + ibex: {}, } diff --git a/test/galoy/unit/app/wallets/get-transactions-for-wallets.spec.ts b/test/galoy/unit/app/wallets/get-transactions-for-wallets.spec.ts index a5ec2c2cd..98bcfd974 100644 --- a/test/galoy/unit/app/wallets/get-transactions-for-wallets.spec.ts +++ b/test/galoy/unit/app/wallets/get-transactions-for-wallets.spec.ts @@ -2,36 +2,36 @@ import { toWalletTransactions } from "@app/wallets" const ibex_data = [ { - "id": "f2fa0473-43b4-4101-8e19-11f1caaeb011", - "createdAt": "2024-01-31T17:27:20.718984Z", - "settledAt": "2024-01-31T17:27:21.422794Z", - "accountId": "e24b85d1-9f61-47da-acb9-fe9d069de2fc", - "amount": 0.045584045584, - "networkFee": 0.000898969976, - "onChainSendFee": 0, - "exchangeRateCurrencySats": 2281.5, - "currencyId": 3, - "transactionTypeId": 2 + id: "f2fa0473-43b4-4101-8e19-11f1caaeb011", + createdAt: "2024-01-31T17:27:20.718984Z", + settledAt: "2024-01-31T17:27:21.422794Z", + accountId: "e24b85d1-9f61-47da-acb9-fe9d069de2fc", + amount: 0.045584045584, + networkFee: 0.000898969976, + onChainSendFee: 0, + exchangeRateCurrencySats: 2281.5, + currencyId: 3, + transactionTypeId: 2, }, { - "id": "d3a61722-c212-4232-9af0-6c6360ee3ad9", - "createdAt": "2024-01-31T17:24:23.446563Z", - "settledAt": "2024-01-31T17:24:44.423267Z", - "accountId": "e24b85d1-9f61-47da-acb9-fe9d069de2fc", - "amount": 0.100425509811, - "networkFee": 0, - "onChainSendFee": 0, - "exchangeRateCurrencySats": 2310.17, - "currencyId": 3, - "transactionTypeId": 1 - } + id: "d3a61722-c212-4232-9af0-6c6360ee3ad9", + createdAt: "2024-01-31T17:24:23.446563Z", + settledAt: "2024-01-31T17:24:44.423267Z", + accountId: "e24b85d1-9f61-47da-acb9-fe9d069de2fc", + amount: 0.100425509811, + networkFee: 0, + onChainSendFee: 0, + exchangeRateCurrencySats: 2310.17, + currencyId: 3, + transactionTypeId: 1, + }, ] describe("Test transformation of IbexResponse to WalletTransaction[]", () => { - it("should set the settlementAmount to negative on send", async () => { - const result: WalletTransaction[] = toWalletTransactions(ibex_data) + it("should set the settlementAmount to negative on send", async () => { + const result: IbexTransaction[] = toWalletTransactions(ibex_data) - expect(result[0].settlementAmount).toEqual(-0.045584045584) - expect(result[1].settlementAmount).toEqual(0.100425509811) - }) -}) \ No newline at end of file + expect(result[0].settlementAmount).toEqual(-0.045584045584) + expect(result[1].settlementAmount).toEqual(0.100425509811) + }) +}) diff --git a/test/galoy/unit/domain/ledger/activity-checker.spec.ts b/test/galoy/unit/domain/ledger/activity-checker.spec.ts index dd1ba6870..8866a757c 100644 --- a/test/galoy/unit/domain/ledger/activity-checker.spec.ts +++ b/test/galoy/unit/domain/ledger/activity-checker.spec.ts @@ -18,6 +18,7 @@ beforeAll(() => { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } usdWallet = { @@ -27,6 +28,7 @@ beforeAll(() => { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } }) diff --git a/test/galoy/unit/domain/ledger/imbalance-calculator.spec.ts b/test/galoy/unit/domain/ledger/imbalance-calculator.spec.ts index 1813a69e3..1f49503d0 100644 --- a/test/galoy/unit/domain/ledger/imbalance-calculator.spec.ts +++ b/test/galoy/unit/domain/ledger/imbalance-calculator.spec.ts @@ -11,6 +11,7 @@ const btcWallet: Wallet = { accountId: "a1" as AccountId, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const VolumeAfterLightningReceiptFn = () => diff --git a/test/galoy/unit/domain/wallets/payment-input-validator.spec.ts b/test/galoy/unit/domain/wallets/payment-input-validator.spec.ts index 522043df4..a54a8faee 100644 --- a/test/galoy/unit/domain/wallets/payment-input-validator.spec.ts +++ b/test/galoy/unit/domain/wallets/payment-input-validator.spec.ts @@ -35,6 +35,7 @@ describe("PaymentInputValidator", () => { quiz: [], kratosUserId: "kratosUserId" as UserId, displayCurrency: UsdDisplayCurrency, + npub: "npub1mock" as Npub, } const dummySenderWallet: Wallet = { @@ -44,6 +45,7 @@ describe("PaymentInputValidator", () => { currency: WalletCurrency.Btc, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const dummyRecipientWallet: Wallet = { @@ -53,6 +55,7 @@ describe("PaymentInputValidator", () => { currency: WalletCurrency.Btc, onChainAddressIdentifiers: [], onChainAddresses: () => [], + lnurlp: "lnurlmock" as Lnurl, } const wallets: { [key: WalletId]: Wallet } = {}