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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/api/galoy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ fees:
defaultMin: 3000
threshold: 1000000
ratioAsBasisPoints: 30
invitedAccount:
flatUsdCents: 15
merchantDeposit:
defaultMin: 3000
threshold: 1000000
Expand Down
1 change: 1 addition & 0 deletions core/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@aws-sdk/client-s3": "^3.850.0",
"@galoy/gt3-server-node-express-sdk": "workspace:^",
"@google-cloud/storage": "^7.16.0",
"@google-cloud/translate": "^9.2.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.15",
"@ip1sms/disposable-phone-numbers": "^2.1.1299",
Expand Down
53 changes: 46 additions & 7 deletions core/api/src/app/payments/get-protocol-fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ import { PartialResult } from "../partial-result"

import { constructPaymentFlowBuilder, getPriceRatioForLimits } from "./helpers"

import { getFeesConfig } from "@/config"
import { decodeInvoice, defaultTimeToExpiryInSeconds } from "@/domain/bitcoin/lightning"
import { checkedToWalletId } from "@/domain/wallets"
import {
LnPaymentRequestNonZeroAmountRequiredError,
LnPaymentRequestZeroAmountRequiredError,
SkipProbeForPubkeyError,
} from "@/domain/payments"
import { AccountStatus } from "@/domain/accounts"
import { WalletCurrency } from "@/domain/shared"
import { LndService } from "@/services/lnd"

import { WalletsRepository, PaymentFlowStateRepository } from "@/services/mongoose"
import {
WalletsRepository,
PaymentFlowStateRepository,
AccountsRepository,
} from "@/services/mongoose"
import { DealerPriceService } from "@/services/dealer-price"
import { addAttributesToCurrentSpan } from "@/services/tracing"

Expand All @@ -22,6 +29,32 @@ import {
checkWithdrawalLimits,
} from "@/app/accounts"

const feesConfig = getFeesConfig()

const getInvitedFee = async (
baseFee: PaymentAmount<WalletCurrency>,
accountId: AccountId,
dealer: IDealerPriceService,
): Promise<PaymentAmount<WalletCurrency> | ApplicationError> => {
const account = await AccountsRepository().findById(accountId)
if (account instanceof Error || account?.status !== AccountStatus.Invited)
return baseFee

if (baseFee.currency === WalletCurrency.Usd) {
return {
currency: baseFee.currency,
amount: baseFee.amount + feesConfig.invitedAccountFlatUsdCents,
}
}

const sats = await dealer.getSatsFromCentsForFutureBuy({
amount: feesConfig.invitedAccountFlatUsdCents,
currency: WalletCurrency.Usd,
})
if (sats instanceof Error) return sats
return { currency: baseFee.currency, amount: baseFee.amount + sats.amount }
}

const getLightningFeeEstimation = async ({
walletId,
uncheckedPaymentRequest,
Expand Down Expand Up @@ -216,12 +249,14 @@ const estimateLightningFee = async ({
}

PaymentFlowStateRepository(defaultTimeToExpiryInSeconds).persistNew(paymentFlow)

const baseFee = paymentFlow.protocolAndBankFeeInSenderWalletCurrency()
const feeOrErr = await getInvitedFee(baseFee, senderWallet.accountId, dealer)
if (feeOrErr instanceof Error) return PartialResult.err(feeOrErr)

return routeResult instanceof SkipProbeForPubkeyError
? PartialResult.ok(paymentFlow.protocolAndBankFeeInSenderWalletCurrency())
: PartialResult.partial(
paymentFlow.protocolAndBankFeeInSenderWalletCurrency(),
routeResult,
)
? PartialResult.ok(feeOrErr)
: PartialResult.partial(feeOrErr, routeResult)
}

paymentFlow = await builder.withRoute(routeResult)
Expand All @@ -241,5 +276,9 @@ const estimateLightningFee = async ({
persistedPayment,
)

return PartialResult.ok(persistedPayment.protocolAndBankFeeInSenderWalletCurrency())
const baseFee = persistedPayment.protocolAndBankFeeInSenderWalletCurrency()
const feeOrErr = await getInvitedFee(baseFee, senderWallet.accountId, dealer)
if (feeOrErr instanceof Error) return PartialResult.err(feeOrErr)

return PartialResult.ok(feeOrErr)
}
4 changes: 2 additions & 2 deletions core/api/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const env = createEnv({
TWILIO_AUTH_TOKEN: z.string().min(1),
TWILIO_VERIFY_SERVICE_ID: z.string().min(1),
TWILIO_MESSAGING_SERVICE_ID: z.string().min(1).optional(),
TWILIO_WELCOME_CONTENT_SID: z.string().min(1).optional(),
TWILIO_PHONE_PAYMENT_CONTENT_SID: z.string().min(1).optional(),

KRATOS_PUBLIC_API: z.string().url(),
KRATOS_ADMIN_API: z.string().url(),
Expand Down Expand Up @@ -180,7 +180,7 @@ export const env = createEnv({
TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN,
TWILIO_VERIFY_SERVICE_ID: process.env.TWILIO_VERIFY_SERVICE_ID,
TWILIO_MESSAGING_SERVICE_ID: process.env.TWILIO_MESSAGING_SERVICE_ID,
TWILIO_WELCOME_CONTENT_SID: process.env.TWILIO_WELCOME_CONTENT_SID,
TWILIO_PHONE_PAYMENT_CONTENT_SID: process.env.TWILIO_PHONE_PAYMENT_CONTENT_SID,

KRATOS_PUBLIC_API: process.env.KRATOS_PUBLIC_API,
KRATOS_ADMIN_API: process.env.KRATOS_ADMIN_API,
Expand Down
2 changes: 1 addition & 1 deletion core/api/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const TWILIO_ACCOUNT_SID = env.TWILIO_ACCOUNT_SID
export const TWILIO_AUTH_TOKEN = env.TWILIO_AUTH_TOKEN
export const TWILIO_VERIFY_SERVICE_ID = env.TWILIO_VERIFY_SERVICE_ID
export const TWILIO_MESSAGING_SERVICE_ID = env.TWILIO_MESSAGING_SERVICE_ID
export const TWILIO_WELCOME_CONTENT_SID = env.TWILIO_WELCOME_CONTENT_SID
export const TWILIO_PHONE_PAYMENT_CONTENT_SID = env.TWILIO_PHONE_PAYMENT_CONTENT_SID
export const KRATOS_PUBLIC_API = env.KRATOS_PUBLIC_API
export const KRATOS_ADMIN_API = env.KRATOS_ADMIN_API
export const KRATOS_MASTER_USER_PASSWORD = env.KRATOS_MASTER_USER_PASSWORD
Expand Down
13 changes: 12 additions & 1 deletion core/api/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,17 @@ export const configSchema = {
daysLookback: 30,
},
},
invitedAccount: {
type: "object",
properties: {
flatUsdCents: { type: "integer" },
},
required: ["flatUsdCents"],
additionalProperties: false,
default: { flatUsdCents: 15 },
},
},
required: ["withdraw", "deposit"],
required: ["withdraw", "deposit", "invitedAccount"],
additionalProperties: false,
default: {
withdraw: {
Expand All @@ -634,8 +643,10 @@ export const configSchema = {
daysLookback: 30,
},
deposit: { defaultMin: 3000, threshold: 1000000, ratioAsBasisPoints: 30 },
invitedAccount: { flatUsdCents: 15 },
},
},

onChainWallet: {
type: "object",
properties: {
Expand Down
3 changes: 3 additions & 0 deletions core/api/src/config/schema.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ type YamlSchema = {
daysLookback: number
defaultMin: number
}
invitedAccount: {
flatUsdCents: number
}
}
onChainWallet: {
dustThreshold: number
Expand Down
1 change: 1 addition & 0 deletions core/api/src/config/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const getFeesConfig = (feesConfig = yamlConfig.fees): FeesConfig => {
withdrawThreshold: toSats(feesConfig.withdraw.threshold),
withdrawDaysLookback: toDays(feesConfig.withdraw.daysLookback),
withdrawDefaultMin: toSats(feesConfig.withdraw.defaultMin),
invitedAccountFlatUsdCents: BigInt(feesConfig.invitedAccount.flatUsdCents),
}
}

Expand Down
1 change: 1 addition & 0 deletions core/api/src/domain/accounts/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,5 @@ type FeesConfig = {
withdrawThreshold: Satoshis
withdrawDaysLookback: Days
withdrawDefaultMin: Satoshis
invitedAccountFlatUsdCents: bigint
}
1 change: 1 addition & 0 deletions core/api/src/domain/notifications/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type NotificationRecipient = {
level: AccountLevel
status: AccountStatus
phoneNumber?: PhoneNumber
language?: UserLanguageOrEmpty
}

type NotificatioSendTransactionArgs = {
Expand Down
7 changes: 7 additions & 0 deletions core/api/src/domain/phone-provider/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ interface IPhoneProviderService {
contentSid: string
contentVariables: Record<string, string>
}): Promise<true | PhoneProviderServiceError>
sendPlainSMS({
to,
body,
}: {
to: PhoneNumber
body: string
}): Promise<true | PhoneProviderServiceError>
}
2 changes: 1 addition & 1 deletion core/api/src/domain/sms-templates/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./welcome"
export * from "./phone-payment"
7 changes: 1 addition & 6 deletions core/api/src/domain/sms-templates/index.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
type WelcomeTemplateParams = {
type PhonePaymentTemplateParams = {
amount: number
currency: WalletCurrency
phoneNumber: string
}

type SmsTemplateResponse = {
contentSid: string
contentVariables: Record<string, string>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { WalletCurrency } from "@/domain/shared"
import { toSats } from "@/domain/bitcoin"
import { toCents } from "@/domain/fiat"

import { TWILIO_WELCOME_CONTENT_SID } from "@/config"

export const welcomeSmsTemplate = ({
export const phonePaymentSmsTemplate = ({
amount,
currency,
phoneNumber,
}: WelcomeTemplateParams): SmsTemplateResponse => {
}: PhonePaymentTemplateParams): string => {
const currencyAmount =
currency === WalletCurrency.Btc
? Number(toSats(amount))
Expand All @@ -19,11 +17,5 @@ export const welcomeSmsTemplate = ({
? `${currencyAmount} SAT`
: `$${currencyAmount.toFixed(2)}`

return {
contentSid: TWILIO_WELCOME_CONTENT_SID || "",
contentVariables: {
formattedAmount,
phoneNumber,
},
}
return `A friend sent you ~${formattedAmount} in Bitcoin! The funds are in your Blink Wallet in the account with your phone number ${phoneNumber}. If you don't have Blink on this phone get it here: https://get.blink.sv`
}
2 changes: 1 addition & 1 deletion core/api/src/servers/write-sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const envVarsWithDefaults = {
TWILIO_AUTH_TOKEN: "dummy",
TWILIO_VERIFY_SERVICE_ID: "dummy",
TWILIO_MESSAGING_SERVICE_ID: "dummy",
TWILIO_WELCOME_CONTENT_SID: "dummy",
TWILIO_PHONE_PAYMENT_CONTENT_SID: "dummy",
KRATOS_PUBLIC_API: "http://dummy",
KRATOS_ADMIN_API: "http://dummy",
KRATOS_MASTER_USER_PASSWORD: "dummy",
Expand Down
18 changes: 11 additions & 7 deletions core/api/src/services/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
} from "@/domain/notifications"
import { toSats } from "@/domain/bitcoin"
import { AccountLevel, AccountStatus } from "@/domain/accounts"
import { welcomeSmsTemplate } from "@/domain/sms-templates"
import { phonePaymentSmsTemplate } from "@/domain/sms-templates"
import { WalletCurrency } from "@/domain/shared"
import { TxStatus } from "@/domain/wallets/tx-status"
import { CallbackEventType } from "@/domain/callback"
Expand All @@ -60,6 +60,7 @@ import { PubSubService } from "@/services/pubsub"
import { CallbackService } from "@/services/svix"
import { wrapAsyncFunctionsToRunInSpan, wrapAsyncToRunInSpan } from "@/services/tracing"
import { TwilioClient } from "@/services/twilio-service"
import { translateText } from "@/services/translation"

export const NotificationsService = (): INotificationsService => {
const pubsub = PubSubService()
Expand Down Expand Up @@ -261,20 +262,23 @@ export const NotificationsService = (): INotificationsService => {
}: NotificatioSendTransactionArgs): Promise<true | NotificationsServiceError> => {
try {
const { settlementCurrency, settlementAmount } = transaction
const { status, phoneNumber } = recipient
const { status, phoneNumber, language = "en" } = recipient
if (status !== AccountStatus.Invited || !phoneNumber) return true

const { contentSid, contentVariables } = welcomeSmsTemplate({
const templateMessage = phonePaymentSmsTemplate({
currency: settlementCurrency,
amount: settlementAmount,
phoneNumber,
})
if (!contentSid) return true

const result = await TwilioClient().sendTemplatedSMS({
const translatedMessage =
language !== "en"
? await translateText({ text: templateMessage, targetLang: language })
: templateMessage

const result = await TwilioClient().sendPlainSMS({
to: phoneNumber,
contentSid,
contentVariables,
body: translatedMessage,
})
if (result instanceof Error) return result

Expand Down
Loading