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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
489 changes: 489 additions & 0 deletions dev/bin/eng-297-graphql-smoke.ts

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions src/app/payments/send-intraledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@app/prices"
import { removeDeviceTokens } from "@app/users/remove-device-tokens"
import { validateIsBtcWallet, validateIsUsdWallet } from "@app/wallets"
import { usdWalletAmountFromInput } from "@app/wallets/usd-wallet-amount"

import {
InvalidLightningPaymentFlowBuilderStateError,
Expand All @@ -15,11 +16,12 @@ import {
} from "@domain/payments"
import { AccountLevel, AccountValidator } from "@domain/accounts"
import { DisplayAmountsConverter } from "@domain/fiat"
import { BigIntConversionError, checkedToUsdPaymentAmount, ErrorLevel, paymentAmountFromNumber, USDAmount, ValidationError, WalletCurrency } from "@domain/shared"
import { checkedToUsdPaymentAmount, ErrorLevel, paymentAmountFromNumber, ValidationError, WalletCurrency } from "@domain/shared"
import { PaymentSendStatus } from "@domain/bitcoin/lightning"
import { ResourceExpiredLockServiceError } from "@domain/lock"
import { checkedToWalletId, SettlementMethod } from "@domain/wallets"
import { DeviceTokensNotRegisteredNotificationsServiceError } from "@domain/notifications"
import { MismatchedCurrencyForWalletError } from "@domain/errors"

import { LockService } from "@services/lock"
import { LedgerService } from "@services/ledger"
Expand Down Expand Up @@ -74,8 +76,12 @@ const intraledgerPaymentSendWalletId = async ({
kratosUserId: recipientUserId,
} = recipientAccount

const amount = USDAmount.cents(uncheckedAmount.toString())
if (amount instanceof BigIntConversionError) return amount
if (senderWallet.currency !== recipientWallet.currency) {
return new MismatchedCurrencyForWalletError()
}

const amount = usdWalletAmountFromInput(uncheckedAmount.toString(), senderWallet.currency)
if (amount instanceof Error) return amount
const invoiceResp = await Ibex.addInvoice({
accountId: recipientWalletId,
amount,
Expand Down Expand Up @@ -132,7 +138,7 @@ export const intraledgerPaymentSendWalletIdForBtcWallet = async (
export const intraledgerPaymentSendWalletIdForUsdWallet = async (
args: IntraLedgerPaymentSendWalletIdArgs,
): Promise<PaymentSendStatus | ApplicationError> => {
const validated = await validateIsUsdWallet(args.senderWalletId)
const validated = await validateIsUsdWallet(args.senderWalletId, { includeUsdt: true })
return validated instanceof Error ? validated : intraledgerPaymentSendWalletId(args)
}

Expand Down
7 changes: 5 additions & 2 deletions src/app/payments/send-lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const payNoAmountInvoiceByWalletIdForBtcWallet = async (
export const payNoAmountInvoiceByWalletIdForUsdWallet = async (
args: PayNoAmountInvoiceByWalletIdArgs,
): Promise<PaymentSendStatus | ApplicationError> => {
const validated = await validateIsUsdWallet(args.senderWalletId)
const validated = await validateIsUsdWallet(args.senderWalletId, { includeUsdt: true })
return validated instanceof Error ? validated : payNoAmountInvoiceByWalletId(args)
}

Expand Down Expand Up @@ -278,7 +278,10 @@ const validateNoAmountInvoicePaymentInputs = async <S extends WalletCurrency>({
const inputPaymentAmount =
senderWallet.currency === WalletCurrency.Btc
? checkedToBtcPaymentAmount(amount)
: checkedToUsdPaymentAmount(amount)
: checkedToUsdPaymentAmount(
amount,
senderWallet.currency as typeof WalletCurrency.Usd | typeof WalletCurrency.Usdt,
)
if (inputPaymentAmount instanceof Error) return inputPaymentAmount

return {
Expand Down
14 changes: 9 additions & 5 deletions src/app/wallets/add-invoice-for-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import {
import Ibex from "@services/ibex/client"
import { IbexError, UnexpectedIbexResponse } from "@services/ibex/errors"
import { decodeInvoice } from "@domain/bitcoin/lightning/ln-invoice"
import { USDAmount } from "@domain/shared"

import { AddInvoiceResponse201 } from "ibex-client"

import { usdWalletAmountFromInput } from "./usd-wallet-amount"

import { validateIsBtcWallet, validateIsUsdWallet } from "./validate"

const defaultBtcExpiration = DEFAULT_EXPIRATIONS["BTC"].delayMinutes
Expand All @@ -46,7 +48,9 @@ const addInvoiceForSelf = async ({
const limitOk = await checkSelfWalletIdRateLimits(wallet.accountId)
if (limitOk instanceof Error) return limitOk

const checkedAmount = amount ? USDAmount.cents(amount.toString()) : undefined
const checkedAmount = amount
? usdWalletAmountFromInput(amount.toString(), wallet.currency)
: undefined
if (checkedAmount instanceof Error) return checkedAmount
const resp = await Ibex.addInvoice({
amount: checkedAmount,
Expand Down Expand Up @@ -77,7 +81,7 @@ export const addInvoiceForSelfForUsdWallet = async (
const expiresIn = checkedToMinutes(args.expiresIn || defaultUsdExpiration)
if (expiresIn instanceof Error) return expiresIn

const validated = await validateIsUsdWallet(walletId)
const validated = await validateIsUsdWallet(walletId, { includeUsdt: true })
if (validated instanceof Error) return validated
return addInvoiceForSelf({ ...args, walletId, expiresIn })
}
Expand Down Expand Up @@ -133,7 +137,7 @@ const addInvoiceForRecipient = async ({
const limitOk = await checkRecipientWalletIdRateLimits(wallet.accountId)
if (limitOk instanceof Error) return limitOk

const checkedAmount = USDAmount.cents(amount.toString())
const checkedAmount = usdWalletAmountFromInput(amount.toString(), wallet.currency)
if (checkedAmount instanceof Error) return checkedAmount
const resp = await Ibex.addInvoice({
amount: checkedAmount,
Expand Down Expand Up @@ -163,7 +167,7 @@ export const addInvoiceForRecipientForUsdWallet = async (
const expiresIn = checkedToMinutes(args.expiresIn || defaultUsdExpiration)
if (expiresIn instanceof Error) return expiresIn

const validated = await validateIsUsdWallet(recipientWalletId)
const validated = await validateIsUsdWallet(recipientWalletId, { includeUsdt: true })
if (validated instanceof Error) return validated

return addInvoiceForRecipient({ ...args, recipientWalletId, expiresIn })
Expand Down
33 changes: 32 additions & 1 deletion src/app/wallets/get-transactions-for-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PartialResult } from "@app/partial-result"
import { USDAmount, USDTAmount, WalletCurrency } from "@domain/shared"
import Ibex from "@services/ibex/client"
import { IbexError } from "@services/ibex/errors"
import { baseLogger } from "@services/logger"
Expand Down Expand Up @@ -32,9 +33,39 @@ export const getTransactionsForWallets = async ({
})
}

const currencyFromIbexCurrencyId = (currencyId: number | undefined): WalletCurrency | undefined => {
if (currencyId === USDAmount.currencyId) return WalletCurrency.Usd
if (currencyId === USDTAmount.currencyId) return WalletCurrency.Usdt
return undefined
}

export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[] => {
return ibexResp.map(trx => {
const currency = (trx.currencyId === 3 ? "USD" : "BTC") as WalletCurrency // WalletCurrency: "USD" | "BTC",
const currency = currencyFromIbexCurrencyId(trx.currencyId)

if (!currency) {
baseLogger.error(`Failed to parse Ibex transaction currency. { WalletId: ${trx.accountId}, TransactionId: ${trx.id}, currencyId: ${trx.currencyId} }`)
return {
walletId: (trx.accountId || "") as WalletId,
settlementAmount: 0 as Satoshis,
settlementFee: 0 as Satoshis,
settlementCurrency: WalletCurrency.Usd,
settlementDisplayAmount: `${trx.amount}`,
settlementDisplayFee: `${trx.networkFee}`,
settlementDisplayPrice: {
base: 0n,
offset: 0n,
displayCurrency: "USD" as DisplayCurrency,
walletCurrency: WalletCurrency.Usd,
},
createdAt: trx.createdAt ? new Date(trx.createdAt) : new Date(),
id: trx.id || "null",
status: "success" as TxStatus,
memo: null,
initiationVia: { type: "unknown" },
settlementVia: { type: "unknown" },
} as UnknownTypeTransaction
}

const settlementDisplayPrice: WalletMinorUnitDisplayPrice<WalletCurrency, DisplayCurrency> = {
base: trx.exchangeRateCurrencySats ? BigInt(Math.floor(trx.exchangeRateCurrencySats)) : 0n,
Expand Down
1 change: 1 addition & 0 deletions src/app/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from "./settle-payout-txn"
export * from "./update-legacy-on-chain-receipt"
export * from "./update-pending-invoices"
export * from "./validate"
export * from "./usd-wallet-amount"

import { WalletsRepository } from "@services/mongoose"

Expand Down
9 changes: 5 additions & 4 deletions src/app/wallets/send-on-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { NETWORK } from "@config"
import { PaymentSendStatus } from "@domain/bitcoin/lightning"
import { checkedToOnChainAddress } from "@domain/bitcoin/onchain"
import { UnsupportedCurrencyError } from "@domain/errors"
import { ErrorLevel, USDAmount } from "@domain/shared"
import { ErrorLevel } from "@domain/shared"
import { UsdWalletAmount } from "@services/ibex/types"
import { OnchainUsdPaymentValidator } from "@domain/wallets"

import { AccountsRepository, WalletsRepository } from "@services/mongoose"
Expand All @@ -19,15 +20,15 @@ type PayOnChainByWalletIdWithoutCurrencyArgs = {
address: string
speed: PayoutSpeed
memo: string | null
amount?: number | FractionalCentAmount | USDAmount
amount?: number | FractionalCentAmount | UsdWalletAmount
}

type PayOnChainByUsdArgs = PayOnChainByWalletIdWithoutCurrencyArgs & {
amount: USDAmount
amount: UsdWalletAmount
}

type PayOnChainByWalletIdForUsdWalletArgs = PayOnChainByWalletIdWithoutCurrencyArgs & {
amount: number | FractionalCentAmount | USDAmount
amount: number | FractionalCentAmount | UsdWalletAmount
}

/*
Expand Down
31 changes: 31 additions & 0 deletions src/app/wallets/usd-wallet-amount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { UnsupportedCurrencyError } from "@domain/errors"
import { USDAmount, USDTAmount, WalletCurrency } from "@domain/shared"

import { WalletsRepository } from "@services/mongoose"

export type UsdWalletAmount = USDAmount | USDTAmount

export const usdWalletAmountFromInput = (
amount: string | number,
currency: WalletCurrency,
): UsdWalletAmount | ApplicationError => {
const raw = amount.toString()

if (currency === WalletCurrency.Usd) return USDAmount.cents(raw)
if (currency === WalletCurrency.Usdt) return USDTAmount.smallestUnits(raw)

return new UnsupportedCurrencyError(`USD wallet amount unsupported for ${currency}`)
}

export const usdWalletAmountFromWalletId = async ({
walletId,
amount,
}: {
walletId: WalletId
amount: string | number
}): Promise<UsdWalletAmount | ApplicationError> => {
const wallet = await WalletsRepository().findById(walletId)
if (wallet instanceof Error) return wallet

return usdWalletAmountFromInput(amount, wallet.currency)
}
21 changes: 19 additions & 2 deletions src/app/wallets/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@ export const validateIsBtcWallet = async (
const wallet = await WalletsRepository().findById(walletId)
if (wallet instanceof Error) return wallet

if (wallet.currency === WalletCurrency.Usd) {
if (wallet.currency !== WalletCurrency.Btc) {
return new MismatchedCurrencyForWalletError()
}
return true
}

export const validateIsUsdWallet = async (
walletId: WalletId,
args?: { includeUsdt?: boolean },
): Promise<true | ApplicationError> => {
const wallet = await WalletsRepository().findById(walletId)
if (wallet instanceof Error) return wallet

if (wallet.currency === WalletCurrency.Btc) {
const isAllowed =
wallet.currency === WalletCurrency.Usd ||
(args?.includeUsdt === true && wallet.currency === WalletCurrency.Usdt)

if (!isAllowed) {
return new MismatchedCurrencyForWalletError()
}
return true
}

export const validateIsUsdtWallet = async (
walletId: WalletId,
): Promise<true | ApplicationError> => {
const wallet = await WalletsRepository().findById(walletId)
if (wallet instanceof Error) return wallet

if (wallet.currency !== WalletCurrency.Usdt) {
return new MismatchedCurrencyForWalletError()
}
return true
Expand Down
13 changes: 10 additions & 3 deletions src/domain/shared/amount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,16 @@ export const UsdPaymentAmount = (cents: bigint): UsdPaymentAmount => {
}
}

export const checkedToUsdPaymentAmount = (
export function checkedToUsdPaymentAmount(
amount: number | null,
): UsdPaymentAmount | ValidationError => {
): UsdPaymentAmount | ValidationError
export function checkedToUsdPaymentAmount<
T extends typeof WalletCurrency.Usd | typeof WalletCurrency.Usdt,
>(amount: number | null, currency: T): PaymentAmount<T> | ValidationError
export function checkedToUsdPaymentAmount(
amount: number | null,
currency: typeof WalletCurrency.Usd | typeof WalletCurrency.Usdt = WalletCurrency.Usd,
): UsdPaymentAmount | PaymentAmount<typeof WalletCurrency.Usdt> | ValidationError {
if (amount === null) {
return new InvalidUsdPaymentAmountError()
}
Expand All @@ -105,7 +112,7 @@ export const checkedToUsdPaymentAmount = (
return new InvalidUsdPaymentAmountError()
}
if (!(amount && amount > 0)) return new InvalidUsdPaymentAmountError()
return paymentAmountFromNumber({ amount, currency: WalletCurrency.Usd })
return paymentAmountFromNumber({ amount, currency })
}

export const paymentAmountFromNumber = <T extends WalletCurrency>({
Expand Down
32 changes: 25 additions & 7 deletions src/domain/wallets/payment-input-validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { USDAmount, ValidationError, isUsdWallet, validator } from "@domain/shared"
import {
USDAmount,
USDTAmount,
ValidationError,
WalletCurrency,
validator,
} from "@domain/shared"
import { isActiveAccount, walletBelongsToAccount } from "@domain/accounts"
import { SendOnchainArgs } from "@services/ibex/types"

Expand All @@ -8,23 +14,35 @@ import { SendOnchainArgs } from "@services/ibex/types"
// else return true
// }

const checkOnchainMin = async (o: { amount: USDAmount }) => {
const isUsdWalletForOnChainPayment = async (o: { wallet: Wallet }) => {
if (
o.wallet.currency === WalletCurrency.Usd ||
o.wallet.currency === WalletCurrency.Usdt
) {
return true
}
return new ValidationError(`Expected USD, got ${o.wallet.currency}`)
}

const checkOnchainMin = async (o: { amount: USDAmount | USDTAmount }) => {
// TODO: Currently relying on Ibex to enforce dust limits
// const { dustThreshold } = getOnChainWalletConfig()
// const minBtc = BtcAmount.sats(dustThreshold.toString())
// const btcPrice = await PriceService().getUsdCentRealTimePrice(_)
// if (btcPrice instanceof PriceServiceError) return new ValidationError(btcPrice)
// const minUsd = minBtc.convertAtRate(MoneyAmount.from("50000", WalletCurrency.Usd))
const minUsd = USDAmount.ZERO
return o.amount.isGreaterThan(minUsd)
? true
: new ValidationError(`Amount must be greater than ${minUsd.asDollars()}`)
const isGreaterThanZero =
o.amount instanceof USDTAmount
? o.amount.isGreaterThan(USDTAmount.ZERO)
: o.amount.isGreaterThan(USDAmount.ZERO)

return isGreaterThanZero ? true : new ValidationError("Amount must be greater than 0")
}

type SendOnchainArgsWithContext = SendOnchainArgs & { wallet: Wallet; account: Account }

export const OnchainUsdPaymentValidator = validator<SendOnchainArgsWithContext>([
isUsdWallet,
isUsdWalletForOnChainPayment,
isActiveAccount,
walletBelongsToAccount,
checkOnchainMin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { InvalidFeeProbeStateError } from "@domain/bitcoin/lightning"

// import { Payments } from "@app"
import { usdWalletAmountFromWalletId, UsdWalletAmount } from "@app/wallets"

import { GT } from "@graphql/index"
import WalletId from "@graphql/shared/types/scalar/wallet-id"
import CentAmount from "@graphql/public/types/scalar/cent-amount"
import CentAmountPayload from "@graphql/public/types/payload/cent-amount"
import LnPaymentRequest from "@graphql/shared/types/scalar/ln-payment-request"
import { mapAndParseErrorForGqlResponse } from "@graphql/error-map"

import { normalizePaymentAmount } from "../../../shared/root/mutation"

// FLASH FORK: import ibex dependencies
import Ibex from "@services/ibex/client"

import { IbexError, UnexpectedIbexResponse } from "@services/ibex/errors"
import { GetFeeEstimateResponse200 } from "ibex-client"
import { BigIntConversionError, checkedToUsdPaymentAmount, USDAmount, ValidationError } from "@domain/shared"
import { IbexError } from "@services/ibex/errors"
import FractionalCentAmount from "@graphql/public/types/scalar/cent-amount-fraction"
import { IbexFeeEstimation } from "@services/ibex/types"

Expand Down Expand Up @@ -55,9 +48,14 @@ const LnNoAmountUsdInvoiceFeeProbeMutation = GT.Field({
// })

// TODO: Move Ibex call to Payments interface
const checkedAmount = USDAmount.cents(amount.toString())
if (checkedAmount instanceof BigIntConversionError) return checkedAmount
const resp: IbexFeeEstimation | IbexError = await Ibex.getLnFeeEstimation({
const checkedAmount = await usdWalletAmountFromWalletId({
walletId,
amount: amount.toString(),
})
if (checkedAmount instanceof Error) {
return { errors: [mapAndParseErrorForGqlResponse(checkedAmount)] }
}
const resp: IbexFeeEstimation<UsdWalletAmount> | IbexError = await Ibex.getLnFeeEstimation({
invoice: paymentRequest as Bolt11,
send: checkedAmount,
})
Expand Down
Loading